blob: 07da0a761c32955973a4ff8a5be2187110df1fbc [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::{
response::{self, Envelope, ToolDelta, Usage},
tools::Tracker,
};
use crate::{
Error, Result,
output::{Event, Finish, ProviderError, ReasoningField, Stop, TextKind},
sse,
};
use serde::Deserialize;
use serde_json::{Value, json};
use std::collections::HashSet;
#[derive(Default, PartialEq)]
enum Phase {
#[default]
Receiving,
TransportEnded,
Finished,
}
/// One lane owns prose lifecycle; reasoning and visible text cannot be active
/// simultaneously. Tool calls have their own identities and may overlap prose.
#[derive(Default)]
struct Prose(Option<TextKind>);
impl Prose {
fn id(kind: TextKind) -> &'static str {
match kind {
TextKind::Text => "txt-0",
TextKind::Reasoning => "reasoning-0",
}
}
fn close(&mut self, events: &mut Vec<Event>) {
if let Some(kind) = self.0.take() {
events.push(Event::TextEnd {
id: Self::id(kind).into(),
kind,
citations: vec![],
extensions: None,
});
}
}
fn append(
&mut self,
kind: TextKind,
text: String,
reasoning_field: Option<ReasoningField>,
events: &mut Vec<Event>,
) {
if text.is_empty() && reasoning_field.is_none() {
return;
}
if self.0 != Some(kind) {
self.close(events);
self.0 = Some(kind);
events.push(Event::TextStart {
id: Self::id(kind).into(),
kind,
extensions: None,
});
}
events.push(Event::TextDelta {
reasoning_field,
id: Self::id(kind).into(),
kind,
text,
extensions: None,
});
}
}
#[derive(Deserialize)]
struct ErrorBody {
error: ErrorDetail,
}
#[derive(Deserialize, serde::Serialize)]
struct ErrorDetail {
message: String,
#[serde(
rename = "type",
default,
skip_serializing_if = "crate::Field::is_missing"
)]
kind: crate::Field<Value>,
#[serde(default, skip_serializing_if = "crate::Field::is_missing")]
param: crate::Field<Value>,
#[serde(default, skip_serializing_if = "crate::Field::is_missing")]
code: crate::Field<Value>,
}
/// Transport-independent Chat Completions decoding. Every incoming record is
/// parsed here, including error frames; hosts receive only native events.
pub struct Stream {
decoder: sse::Decoder,
namespace: String,
raw: bool,
observe_reasoning: bool,
phase: Phase,
metadata_emitted: bool,
prose: Prose,
finish: Option<(Stop, Option<String>)>,
usage: Option<(Usage, Value)>,
pending: Vec<(f64, ToolDelta)>,
forwarded: HashSet<String>,
tools: Tracker,
}
impl Stream {
pub fn new(namespace: String) -> Self {
Self {
decoder: Default::default(),
namespace,
raw: false,
observe_reasoning: false,
phase: Phase::Receiving,
metadata_emitted: false,
prose: Prose::default(),
finish: None,
usage: None,
pending: vec![],
forwarded: HashSet::new(),
tools: Tracker::default(),
}
}
/// Preserve the carrier and explicit empty reasoning fields for replay.
pub fn with_reasoning_observations(mut self, enabled: bool) -> Self {
self.observe_reasoning = enabled;
self
}
pub fn with_raw_records(mut self, enabled: bool) -> Self {
self.raw = enabled;
self
}
pub fn terminal(&self) -> bool {
self.phase != Phase::Receiving
}
pub fn push(
&mut self,
bytes: &[u8],
generate_id: &mut dyn FnMut() -> String,
) -> Result<Vec<Event>> {
let mut events = vec![];
if self.terminal() {
return Ok(events);
}
for record in self.decoder.push(bytes)? {
if record == "[DONE]" {
self.phase = Phase::TransportEnded;
break;
}
let raw: Value = match serde_json::from_str(&record) {
Ok(value) => value,
Err(error) => {
if self.raw {
events.push(Event::Raw(json!(record)));
}
self.finish = Some((Stop::Error, None));
events.push(Event::Error(Error::json(&record, error)));
continue;
}
};
if self.raw {
events.push(Event::Raw(raw.clone()));
}
// A normal chunk wins if a provider includes an unrelated error
// extension. Otherwise a complete error envelope is a protocol event.
let envelope = match Envelope::parse(&raw, true) {
Ok(value) => value,
Err(error) => {
self.finish = Some((Stop::Error, None));
if let Ok(error) = serde_json::from_value::<ErrorBody>(raw.clone()) {
let data =
serde_json::to_value(&error.error).expect("error fields are JSON");
events.push(Event::ProviderError(ProviderError {
message: error.error.message,
kind: data["type"].as_str().map(str::to_owned),
code: data.get("code").cloned(),
status: None,
retryable: None,
data,
}));
} else {
events.push(Event::Error(error));
}
continue;
}
};
let timestamp = envelope.timestamp();
if !self.metadata_emitted
&& (timestamp.is_some()
|| envelope.id.as_ref().is_some_and(|v| !v.is_empty())
|| envelope.model.as_ref().is_some_and(|v| !v.is_empty()))
{
events.push(Event::Response {
id: envelope.id,
model: envelope.model,
created_at_unix_seconds: timestamp,
});
self.metadata_emitted = true;
}
if let Some(usage) = envelope.usage {
self.usage = Some((usage, raw["usage"].clone()));
}
let Some(choice) = envelope.choices.into_iter().next() else {
continue;
};
if let Some(reason) = choice.finish_reason {
self.finish = Some((response::stop(Some(&reason)), Some(reason)));
}
let Some(delta) = choice.delta else {
continue;
};
let reasoning = delta
.reasoning_content
.map(|text| (ReasoningField::Content, text))
.or_else(|| {
delta
.reasoning
.map(|text| (ReasoningField::Reasoning, text))
});
if let Some((field, text)) = reasoning {
self.prose.append(
TextKind::Reasoning,
text,
self.observe_reasoning.then_some(field),
&mut events,
);
}
for (kind, text) in delta.content.into_iter().flat_map(|v| v.text()) {
self.prose.append(kind, text, None, &mut events);
}
if delta.tool_calls.as_ref().is_some_and(|v| !v.is_empty())
&& self.prose.0 == Some(TextKind::Reasoning)
{
self.prose.close(&mut events);
}
for delta in delta.tool_calls.into_iter().flatten() {
self.tool(delta, generate_id, &mut events)?;
}
}
Ok(events)
}
fn tool(
&mut self,
delta: ToolDelta,
generate_id: &mut dyn FnMut() -> String,
events: &mut Vec<Event>,
) -> Result<()> {
let Some(index) = delta
.index
.filter(|i| !self.forwarded.contains(&i.to_string()))
else {
events.extend(self.tools.push(&delta, &self.namespace, generate_id)?);
return Ok(());
};
let position = if let Some(p) = self.pending.iter().position(|(i, _)| *i == index) {
p
} else {
self.pending.push((
index,
ToolDelta {
index: Some(index),
..Default::default()
},
));
self.pending.len() - 1
};
let pending = &mut self.pending[position].1;
if pending.id.is_none() {
pending.id = delta.id;
}
if pending.extra_content.is_none() {
pending.extra_content = delta.extra_content;
}
pending
.function
.arguments
.get_or_insert_with(String::new)
.push_str(delta.function.arguments.as_deref().unwrap_or(""));
if delta.function.name.is_some() {
pending.function.name = delta.function.name;
let (_, pending) = self.pending.remove(position);
events.extend(self.tools.push(&pending, &self.namespace, generate_id)?);
self.forwarded.insert(index.to_string());
}
Ok(())
}
pub fn finish(&mut self, generate_id: &mut dyn FnMut() -> String) -> Result<Vec<Event>> {
if self.phase == Phase::Finished {
return Ok(vec![]);
}
self.phase = Phase::Finished;
let mut events = vec![];
self.prose.close(&mut events);
for (_, pending) in self.pending.drain(..) {
events.extend(self.tools.push(&pending, &self.namespace, generate_id)?);
}
events.extend(self.tools.finish());
let (reason, raw_reason) = self.finish.take().unwrap_or_else(|| {
events.push(Event::Error(Error::invalid(
"Response stream ended without a finish reason.",
Value::Null,
)));
(Stop::Error, None)
});
events.push(Event::Finish(Finish {
reason,
raw_reason,
extensions: response::extensions(
self.usage.as_ref().map(|(usage, _)| usage),
&self.namespace,
),
usage: self.usage.take().map(|(usage, raw)| usage.normalized(raw)),
}));
Ok(events)
}
}