This commit is contained in:
jif-oai
2025-11-11 15:36:22 +00:00
parent b7dcc8ef5c
commit 769e9cc92c
16 changed files with 306 additions and 856 deletions

View File

@@ -0,0 +1,113 @@
use async_trait::async_trait;
use codex_otel::otel_event_manager::OtelEventManager;
use serde_json::Value;
use tokio::sync::mpsc;
use tracing::debug;
use crate::client::WireResponseDecoder;
use crate::error::Result;
use crate::stream::WireEvent;
#[derive(Default)]
struct FunctionCallState {
active: bool,
call_id: Option<String>,
name: Option<String>,
arguments: String,
}
pub struct WireChatSseDecoder {
fn_call_state: FunctionCallState,
}
impl WireChatSseDecoder {
pub fn new() -> Self {
Self {
fn_call_state: FunctionCallState::default(),
}
}
}
#[async_trait]
impl WireResponseDecoder for WireChatSseDecoder {
async fn on_frame(
&mut self,
json: &str,
tx: &mpsc::Sender<crate::error::Result<WireEvent>>,
_otel: &OtelEventManager,
) -> Result<()> {
// Chat sends a terminal "[DONE]" frame; ignore it.
let Ok(parsed_chunk) = serde_json::from_str::<Value>(json) else {
debug!("failed to parse Chat SSE JSON: {}", json);
return Ok(());
};
let choices = parsed_chunk
.get("choices")
.and_then(|choices| choices.as_array())
.cloned()
.unwrap_or_default();
for choice in choices {
if let Some(delta) = choice.get("delta") {
if let Some(content) = delta.get("content").and_then(|c| c.as_array()) {
for piece in content {
if let Some(text) = piece.get("text").and_then(|t| t.as_str()) {
let _ = tx
.send(Ok(WireEvent::OutputTextDelta(text.to_string())))
.await;
}
}
}
if let Some(tool_calls) = delta.get("tool_calls").and_then(|c| c.as_array()) {
for call in tool_calls {
if let Some(id_val) = call.get("id").and_then(|id| id.as_str()) {
self.fn_call_state.call_id = Some(id_val.to_string());
}
if let Some(function) = call.get("function") {
if let Some(name) = function.get("name").and_then(|n| n.as_str()) {
self.fn_call_state.name = Some(name.to_string());
self.fn_call_state.active = true;
}
if let Some(args) = function.get("arguments").and_then(|a| a.as_str()) {
self.fn_call_state.arguments.push_str(args);
}
}
}
}
if let Some(reasoning) = delta.get("reasoning_content").and_then(|c| c.as_array()) {
for entry in reasoning {
if let Some(text) = entry.get("text").and_then(|t| t.as_str()) {
let _ = tx
.send(Ok(WireEvent::ReasoningContentDelta(text.to_string())))
.await;
}
}
}
}
if let Some(finish_reason) = choice.get("finish_reason").and_then(|f| f.as_str())
&& finish_reason == "tool_calls"
&& self.fn_call_state.active
{
let function_name = self.fn_call_state.name.take().unwrap_or_default();
let call_id = self.fn_call_state.call_id.take().unwrap_or_default();
let arguments = self.fn_call_state.arguments.clone();
self.fn_call_state = FunctionCallState::default();
let item = serde_json::json!({
"type": "function_call",
"id": call_id,
"call_id": call_id,
"name": function_name,
"arguments": arguments,
});
let _ = tx.send(Ok(WireEvent::OutputItemDone(item))).await;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,2 @@
pub mod chat;
pub mod responses;

View File

@@ -0,0 +1,166 @@
use async_trait::async_trait;
use codex_otel::otel_event_manager::OtelEventManager;
use serde::Deserialize;
use serde_json::Value;
use tokio::sync::mpsc;
use tracing::debug;
use crate::client::WireResponseDecoder;
use crate::error::Error;
use crate::error::Result;
use crate::stream::WireEvent;
use crate::stream::WireTokenUsage;
#[derive(Debug, Deserialize)]
struct StreamEvent {
#[serde(rename = "type")]
event_type: String,
#[serde(default)]
response: Option<Value>,
#[serde(default)]
item: Option<Value>,
#[serde(default)]
error: Option<Value>,
#[serde(default)]
delta: Option<String>,
}
pub struct WireResponsesSseDecoder;
#[async_trait]
impl WireResponseDecoder for WireResponsesSseDecoder {
async fn on_frame(
&mut self,
json: &str,
tx: &mpsc::Sender<Result<WireEvent>>,
otel: &OtelEventManager,
) -> Result<()> {
let Ok(event) = serde_json::from_str::<StreamEvent>(json) else {
debug!("failed to parse Responses SSE JSON: {}", json);
return Ok(());
};
match event.event_type.as_str() {
"response.created" => {
let _ = tx.send(Ok(WireEvent::Created)).await;
}
"response.output_text.delta" => {
if let Some(delta) = event.delta.or_else(|| {
event.item.and_then(|v| {
v.get("delta")
.and_then(|d| d.as_str().map(|s| s.to_string()))
})
}) {
let _ = tx.send(Ok(WireEvent::OutputTextDelta(delta))).await;
}
}
"response.reasoning_text.delta" => {
if let Some(delta) = event.delta {
let _ = tx.send(Ok(WireEvent::ReasoningContentDelta(delta))).await;
}
}
"response.reasoning_summary_text.delta" => {
if let Some(delta) = event.delta {
let _ = tx.send(Ok(WireEvent::ReasoningSummaryDelta(delta))).await;
}
}
"response.output_item.done" => {
if let Some(item_val) = event.item {
let _ = tx.send(Ok(WireEvent::OutputItemDone(item_val))).await;
}
}
"response.output_item.added" => {
if let Some(item_val) = event.item {
let _ = tx.send(Ok(WireEvent::OutputItemAdded(item_val))).await;
}
}
"response.reasoning_summary_part.added" => {
let _ = tx.send(Ok(WireEvent::ReasoningSummaryPartAdded)).await;
}
"response.completed" => {
if let Some(resp) = event.response {
let response_id = resp
.get("id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let usage = parse_wire_usage(&resp);
if let Some(u) = &usage {
otel.sse_event_completed(
u.input_tokens,
u.output_tokens,
Some(u.cached_input_tokens),
Some(u.reasoning_output_tokens),
u.total_tokens,
);
} else {
otel.see_event_completed_failed(&"missing token usage".to_string());
}
let _ = tx
.send(Ok(WireEvent::Completed {
response_id,
token_usage: usage,
}))
.await;
}
}
"response.error" | "response.failed" => {
let message = event
.error
.as_ref()
.and_then(|v| v.get("message"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown error".to_string());
let _ = tx.send(Err(Error::Stream(message, None))).await;
}
_ => {}
}
Ok(())
}
}
fn parse_wire_usage(resp: &Value) -> Option<WireTokenUsage> {
let usage = resp.get("usage").cloned()?;
let input_tokens = usage
.get("input_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let cached_input_tokens = usage
.get("cached_input_tokens")
.and_then(|v| v.as_i64())
.or_else(|| {
usage
.get("input_tokens_details")
.and_then(|d| d.get("cached_tokens"))
.and_then(|v| v.as_i64())
})
.unwrap_or(0);
let output_tokens = usage
.get("output_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let reasoning_output_tokens = usage
.get("reasoning_output_tokens")
.and_then(|v| v.as_i64())
.or_else(|| {
usage
.get("output_tokens_details")
.and_then(|d| d.get("reasoning_tokens"))
.and_then(|v| v.as_i64())
})
.unwrap_or(0);
let total_tokens = usage
.get("total_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
Some(WireTokenUsage {
input_tokens,
cached_input_tokens,
output_tokens,
reasoning_output_tokens,
total_tokens,
})
}