refactor: 大规模模块拆分与代码精简,新增 ai-pipeline/data-contracts 独立 crate

- 新增 aether-ai-pipeline 和 aether-data-contracts crate,将 pipeline 逻辑与数据契约从 gateway 中解耦
- 重构 admin handlers:拆分单体模块为 auth/billing/endpoint/features/model/observability/provider/system 等独立子模块
- 合并 chat/cli 重复代码路径:精简 conversion、finalize、planner 中的 sync/chat/cli 分支
- 重构 scheduler/executor/data 层,引入 facade 模式降低模块间耦合
- 移除冗余的 intent 模块,将 plan_fallback/policy/stream_path/sync_path 迁移至 executor
- 前端适配:调整 admin API 调用和 provider 模型测试对话框
This commit is contained in:
fawney19
2026-04-07 02:50:19 +08:00
parent 763ff03a7b
commit 5d96d6673b
732 changed files with 28593 additions and 20666 deletions

View File

@@ -0,0 +1,13 @@
use serde_json::Value;
pub fn build_generated_tool_call_id(index: usize) -> String {
format!("call_auto_{index}")
}
pub fn canonicalize_tool_arguments(value: Option<Value>) -> String {
match value {
Some(Value::String(text)) => text,
Some(other) => serde_json::to_string(&other).unwrap_or_else(|_| "null".to_string()),
None => "{}".to_string(),
}
}

View File

@@ -0,0 +1,41 @@
use std::fmt;
pub use self::sse::{encode_done_sse, encode_json_sse, map_claude_stop_reason};
pub use self::standard::stream_core::CanonicalStreamEvent;
pub use self::standard::stream_core::CanonicalStreamFrame;
pub use self::stream_rewrite::{resolve_finalize_stream_rewrite_mode, FinalizeStreamRewriteMode};
pub mod common;
pub mod sse;
pub mod standard;
pub mod stream_rewrite;
pub mod sync_products;
#[derive(Debug)]
pub struct PipelineFinalizeError(pub String);
impl PipelineFinalizeError {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
impl fmt::Display for PipelineFinalizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Pipeline finalize error: {}", self.0)
}
}
impl std::error::Error for PipelineFinalizeError {}
impl From<serde_json::Error> for PipelineFinalizeError {
fn from(source: serde_json::Error) -> Self {
Self(source.to_string())
}
}
impl From<base64::DecodeError> for PipelineFinalizeError {
fn from(source: base64::DecodeError) -> Self {
Self(source.to_string())
}
}

View File

@@ -0,0 +1,41 @@
use serde_json::Value;
use crate::finalize::PipelineFinalizeError;
pub fn map_claude_stop_reason(
stop_reason: Option<&str>,
has_tool_calls: bool,
) -> Option<&'static str> {
let mapped = match stop_reason {
Some("end_turn") | Some("stop_sequence") => Some("stop"),
Some("max_tokens") => Some("length"),
Some("tool_use") => Some("tool_calls"),
Some("pause_turn") => Some("stop"),
_ => None,
};
if has_tool_calls && mapped.is_none_or(|value| value == "stop") {
Some("tool_calls")
} else {
mapped
}
}
pub fn encode_done_sse() -> Vec<u8> {
b"data: [DONE]\n\n".to_vec()
}
pub fn encode_json_sse(
event: Option<&str>,
value: &Value,
) -> Result<Vec<u8>, PipelineFinalizeError> {
let mut out = Vec::new();
if let Some(event) = event.filter(|value| !value.trim().is_empty()) {
out.extend_from_slice(b"event: ");
out.extend_from_slice(event.as_bytes());
out.push(b'\n');
}
out.extend_from_slice(b"data: ");
out.extend(serde_json::to_vec(value).map_err(PipelineFinalizeError::from)?);
out.extend_from_slice(b"\n\n");
Ok(out)
}

View File

@@ -0,0 +1 @@
pub mod stream;

View File

@@ -0,0 +1,526 @@
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::finalize::common::{build_generated_tool_call_id, canonicalize_tool_arguments};
use crate::finalize::sse::{encode_json_sse, map_claude_stop_reason};
use crate::finalize::standard::stream_core::common::*;
use crate::finalize::PipelineFinalizeError;
#[derive(Default)]
struct ClaudeProviderToolState {
call_id: String,
name: String,
started_emitted: bool,
}
#[derive(Default)]
pub struct ClaudeProviderState {
message_id: Option<String>,
model: Option<String>,
started: bool,
finished: bool,
tool_calls: BTreeMap<usize, ClaudeProviderToolState>,
}
impl ClaudeProviderState {
fn identity(&self, report_context: &Value) -> (String, String) {
resolve_identity(
self.message_id.as_deref(),
self.model.as_deref(),
report_context,
"msg-local-stream",
)
}
fn ensure_started(&mut self, report_context: &Value, out: &mut Vec<CanonicalStreamFrame>) {
if self.started {
return;
}
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Start,
});
self.started = true;
}
pub fn push_line(
&mut self,
report_context: &Value,
line: Vec<u8>,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
let Some(value) = decode_json_data_line(&line) else {
return Ok(Vec::new());
};
let Some(event_object) = value.as_object() else {
return Ok(Vec::new());
};
let mut out = Vec::new();
match event_object
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"message_start" => {
if let Some(message) = event_object.get("message").and_then(Value::as_object) {
self.message_id = message
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned);
self.model = message
.get("model")
.and_then(Value::as_str)
.map(ToOwned::to_owned);
}
self.ensure_started(report_context, &mut out);
}
"content_block_delta" => {
let index = event_object
.get("index")
.and_then(Value::as_u64)
.map(|value| value as usize)
.unwrap_or(0);
let Some(delta) = event_object.get("delta").and_then(Value::as_object) else {
return Ok(out);
};
match delta
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"text_delta" => {
let Some(piece) = delta.get("text").and_then(Value::as_str) else {
return Ok(out);
};
if piece.is_empty() {
return Ok(out);
}
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::TextDelta(piece.to_string()),
});
}
"input_json_delta" => {
let Some(partial_json) = delta.get("partial_json").and_then(Value::as_str)
else {
return Ok(out);
};
if partial_json.is_empty() {
return Ok(out);
}
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
let tool_state = self.tool_calls.entry(index).or_default();
if !tool_state.started_emitted {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallStart {
index,
call_id: if tool_state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
tool_state.call_id.clone()
},
name: if tool_state.name.is_empty() {
"unknown".to_string()
} else {
tool_state.name.clone()
},
},
});
tool_state.started_emitted = true;
}
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
index,
arguments: partial_json.to_string(),
},
});
}
_ => {}
}
}
"content_block_start" => {
let index = event_object
.get("index")
.and_then(Value::as_u64)
.map(|value| value as usize)
.unwrap_or(0);
let Some(block) = event_object.get("content_block").and_then(Value::as_object)
else {
return Ok(out);
};
let block_type = block
.get("type")
.and_then(Value::as_str)
.unwrap_or_default();
if block_type == "text" {
let Some(text) = block.get("text").and_then(Value::as_str) else {
return Ok(out);
};
if text.is_empty() {
return Ok(out);
}
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::TextDelta(text.to_string()),
});
return Ok(out);
}
if block_type != "tool_use" {
return Ok(out);
}
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
let tool_state = self.tool_calls.entry(index).or_default();
tool_state.call_id = block
.get("id")
.and_then(Value::as_str)
.unwrap_or_else(|| tool_state.call_id.as_str())
.to_string();
tool_state.name = block
.get("name")
.and_then(Value::as_str)
.unwrap_or_else(|| tool_state.name.as_str())
.to_string();
if !tool_state.started_emitted {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallStart {
index,
call_id: if tool_state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
tool_state.call_id.clone()
},
name: if tool_state.name.is_empty() {
"unknown".to_string()
} else {
tool_state.name.clone()
},
},
});
tool_state.started_emitted = true;
}
let arguments = canonicalize_tool_arguments(block.get("input").cloned());
if !arguments.is_empty() && arguments != "{}" {
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments },
});
}
}
"message_delta" => {
self.ensure_started(report_context, &mut out);
let Some(delta) = event_object.get("delta").and_then(Value::as_object) else {
return Ok(out);
};
let finish_reason = map_claude_stop_reason(
delta.get("stop_reason").and_then(Value::as_str),
delta.get("stop_reason").and_then(Value::as_str) == Some("tool_use"),
)
.map(ToOwned::to_owned);
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason,
usage: canonical_usage_from_claude_usage(event_object.get("usage")),
},
});
self.finished = true;
}
_ => {}
}
Ok(out)
}
pub fn finish(
&mut self,
report_context: &Value,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
if !self.started || self.finished {
return Ok(Vec::new());
}
self.finished = true;
let (id, model) = self.identity(report_context);
Ok(vec![CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason: None,
usage: None,
},
}])
}
}
enum ClaudeOpenBlock {
Text {
block_index: usize,
},
Tool {
tool_index: usize,
block_index: usize,
},
}
#[derive(Default)]
pub struct ClaudeClientEmitter {
message_id: Option<String>,
model: Option<String>,
started: bool,
finished: bool,
next_block_index: usize,
open_block: Option<ClaudeOpenBlock>,
tool_block_indices: BTreeMap<usize, usize>,
}
impl ClaudeClientEmitter {
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
self.message_id = Some(frame.id.clone());
self.model = Some(frame.model.clone());
}
fn ensure_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
if self.started {
return Ok(Vec::new());
}
self.started = true;
encode_json_sse(
Some("message_start"),
&json!({
"type": "message_start",
"message": {
"id": self.message_id.as_deref().unwrap_or("msg-local-stream"),
"type": "message",
"role": "assistant",
"model": self.model.as_deref().unwrap_or("unknown"),
"content": [],
"stop_reason": Value::Null,
"stop_sequence": Value::Null,
}
}),
)
}
fn close_open_block(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
let Some(open_block) = self.open_block.take() else {
return Ok(Vec::new());
};
let block_index = match open_block {
ClaudeOpenBlock::Text { block_index } => block_index,
ClaudeOpenBlock::Tool { block_index, .. } => block_index,
};
encode_json_sse(
Some("content_block_stop"),
&json!({
"type": "content_block_stop",
"index": block_index,
}),
)
}
fn ensure_text_block(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
let mut out = Vec::new();
if let Some(ClaudeOpenBlock::Text { .. }) = self.open_block {
return Ok(out);
}
out.extend(self.close_open_block()?);
let block_index = self.next_block_index;
self.next_block_index += 1;
self.open_block = Some(ClaudeOpenBlock::Text { block_index });
out.extend(encode_json_sse(
Some("content_block_start"),
&json!({
"type": "content_block_start",
"index": block_index,
"content_block": {
"type": "text",
"text": "",
}
}),
)?);
Ok(out)
}
fn ensure_tool_block(
&mut self,
tool_index: usize,
call_id: &str,
name: &str,
) -> Result<Vec<u8>, PipelineFinalizeError> {
let mut out = Vec::new();
if let Some(ClaudeOpenBlock::Tool {
tool_index: current_tool_index,
..
}) = self.open_block
{
if current_tool_index == tool_index {
return Ok(out);
}
}
out.extend(self.close_open_block()?);
let block_index = self
.tool_block_indices
.get(&tool_index)
.copied()
.unwrap_or_else(|| {
let block_index = self.next_block_index;
self.next_block_index += 1;
self.tool_block_indices.insert(tool_index, block_index);
block_index
});
self.open_block = Some(ClaudeOpenBlock::Tool {
tool_index,
block_index,
});
out.extend(encode_json_sse(
Some("content_block_start"),
&json!({
"type": "content_block_start",
"index": block_index,
"content_block": {
"type": "tool_use",
"id": call_id,
"name": name,
"input": {},
}
}),
)?);
Ok(out)
}
pub fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, PipelineFinalizeError> {
self.update_identity(&frame);
match frame.event {
CanonicalStreamEvent::Start => self.ensure_started(),
CanonicalStreamEvent::TextDelta(text) => {
let mut out = self.ensure_started()?;
out.extend(self.ensure_text_block()?);
let block_index = match self.open_block {
Some(ClaudeOpenBlock::Text { block_index }) => block_index,
_ => return Ok(out),
};
out.extend(encode_json_sse(
Some("content_block_delta"),
&json!({
"type": "content_block_delta",
"index": block_index,
"delta": {
"type": "text_delta",
"text": text,
}
}),
)?);
Ok(out)
}
CanonicalStreamEvent::ToolCallStart {
index,
call_id,
name,
} => {
let mut out = self.ensure_started()?;
out.extend(self.ensure_tool_block(index, &call_id, &name)?);
Ok(out)
}
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
let mut out = self.ensure_started()?;
let call_id = format!("tool_{index}");
out.extend(self.ensure_tool_block(index, &call_id, "unknown")?);
let block_index = match self.open_block {
Some(ClaudeOpenBlock::Tool { block_index, .. }) => block_index,
_ => return Ok(out),
};
out.extend(encode_json_sse(
Some("content_block_delta"),
&json!({
"type": "content_block_delta",
"index": block_index,
"delta": {
"type": "input_json_delta",
"partial_json": arguments,
}
}),
)?);
Ok(out)
}
CanonicalStreamEvent::Finish {
finish_reason,
usage,
} => {
if self.finished {
return Ok(Vec::new());
}
let mut out = self.ensure_started()?;
out.extend(self.close_open_block()?);
let mut payload = Map::new();
payload.insert(
"type".to_string(),
Value::String("message_delta".to_string()),
);
payload.insert(
"delta".to_string(),
json!({
"stop_reason": map_openai_finish_reason_to_claude(
finish_reason.as_deref()
),
"stop_sequence": Value::Null,
}),
);
if let Some(usage) = usage {
payload.insert(
"usage".to_string(),
json!({
"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens,
}),
);
}
out.extend(encode_json_sse(
Some("message_delta"),
&Value::Object(payload),
)?);
out.extend(encode_json_sse(
Some("message_stop"),
&json!({
"type": "message_stop",
}),
)?);
self.finished = true;
Ok(out)
}
}
}
pub fn finish(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
if !self.started || self.finished {
return Ok(Vec::new());
}
self.emit(CanonicalStreamFrame {
id: self
.message_id
.clone()
.unwrap_or_else(|| "msg-local-stream".to_string()),
model: self.model.clone().unwrap_or_else(|| "unknown".to_string()),
event: CanonicalStreamEvent::Finish {
finish_reason: None,
usage: None,
},
})
}
}

View File

@@ -0,0 +1 @@
pub mod stream;

View File

@@ -0,0 +1,401 @@
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::finalize::common::{build_generated_tool_call_id, canonicalize_tool_arguments};
use crate::finalize::sse::encode_json_sse;
use crate::finalize::standard::stream_core::common::*;
use crate::finalize::PipelineFinalizeError;
#[derive(Default)]
struct GeminiProviderToolState {
call_id: String,
name: String,
arguments: String,
started_emitted: bool,
}
#[derive(Default)]
pub struct GeminiProviderState {
response_id: Option<String>,
model: Option<String>,
started: bool,
finished: bool,
text_parts: BTreeMap<usize, String>,
tool_calls: BTreeMap<usize, GeminiProviderToolState>,
}
impl GeminiProviderState {
fn identity(&self, report_context: &Value) -> (String, String) {
resolve_identity(
self.response_id.as_deref(),
self.model.as_deref(),
report_context,
"resp-local-stream",
)
}
fn ensure_started(&mut self, report_context: &Value, out: &mut Vec<CanonicalStreamFrame>) {
if self.started {
return;
}
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Start,
});
self.started = true;
}
pub fn push_line(
&mut self,
report_context: &Value,
line: Vec<u8>,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
let Some(value) = decode_json_data_line(&line) else {
return Ok(Vec::new());
};
let Some(raw_event_object) = value.as_object() else {
return Ok(Vec::new());
};
if let Some(id) = raw_event_object.get("responseId").and_then(Value::as_str) {
self.response_id = Some(id.to_string());
}
let event_object = raw_event_object
.get("response")
.and_then(Value::as_object)
.filter(|response| response.contains_key("candidates"))
.unwrap_or(raw_event_object);
if let Some(id) = event_object.get("responseId").and_then(Value::as_str) {
self.response_id = Some(id.to_string());
}
if let Some(version) = event_object.get("modelVersion").and_then(Value::as_str) {
self.model = Some(version.to_string());
}
let mut out = Vec::new();
let Some(candidates) = event_object.get("candidates").and_then(Value::as_array) else {
return Ok(out);
};
for candidate in candidates {
let Some(candidate_object) = candidate.as_object() else {
continue;
};
let Some(content) = candidate_object.get("content").and_then(Value::as_object) else {
continue;
};
let Some(parts) = content.get("parts").and_then(Value::as_array) else {
continue;
};
if !parts.is_empty() {
self.ensure_started(report_context, &mut out);
}
let (id, model) = self.identity(report_context);
for (index, part) in parts.iter().enumerate() {
let Some(part_object) = part.as_object() else {
continue;
};
if let Some(text) = part_object.get("text").and_then(Value::as_str) {
let previous = self.text_parts.entry(index).or_default();
let delta = if text.starts_with(previous.as_str()) {
text[previous.len()..].to_string()
} else if previous.as_str() == text {
String::new()
} else {
text.to_string()
};
*previous = text.to_string();
if !delta.is_empty() {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::TextDelta(delta),
});
}
continue;
}
let Some(function_call) =
part_object.get("functionCall").and_then(Value::as_object)
else {
continue;
};
let tool_state = self.tool_calls.entry(index).or_default();
tool_state.call_id = function_call
.get("id")
.and_then(Value::as_str)
.unwrap_or_else(|| tool_state.call_id.as_str())
.to_string();
tool_state.name = function_call
.get("name")
.and_then(Value::as_str)
.unwrap_or_else(|| tool_state.name.as_str())
.to_string();
if !tool_state.started_emitted {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallStart {
index,
call_id: if tool_state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
tool_state.call_id.clone()
},
name: if tool_state.name.is_empty() {
"unknown".to_string()
} else {
tool_state.name.clone()
},
},
});
tool_state.started_emitted = true;
}
let arguments = canonicalize_tool_arguments(function_call.get("args").cloned());
let delta = if arguments.starts_with(&tool_state.arguments) {
arguments[tool_state.arguments.len()..].to_string()
} else if tool_state.arguments == arguments {
String::new()
} else {
arguments.clone()
};
tool_state.arguments = arguments;
if !delta.is_empty() {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
index,
arguments: delta,
},
});
}
}
if let Some(finish_reason) =
candidate_object.get("finishReason").and_then(Value::as_str)
{
let has_tool_calls = !self.tool_calls.is_empty();
let mut finish_reason = normalize_openai_finish_reason(match finish_reason {
"STOP" => Some("stop"),
"MAX_TOKENS" => Some("length"),
"SAFETY" => Some("content_filter"),
other => Some(other),
});
if has_tool_calls && finish_reason.as_deref().is_none_or(|value| value == "stop") {
finish_reason = Some("tool_calls".to_string());
}
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason,
usage: canonical_usage_from_gemini_usage(event_object.get("usageMetadata")),
},
});
self.finished = true;
}
}
Ok(out)
}
pub fn finish(
&mut self,
report_context: &Value,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
if !self.started || self.finished {
return Ok(Vec::new());
}
self.finished = true;
let (id, model) = self.identity(report_context);
Ok(vec![CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason: None,
usage: None,
},
}])
}
}
#[derive(Default)]
struct GeminiClientToolState {
call_id: String,
name: String,
arguments: String,
emitted: bool,
}
#[derive(Default)]
pub struct GeminiClientEmitter {
response_id: Option<String>,
model: Option<String>,
finished: bool,
tool_calls: BTreeMap<usize, GeminiClientToolState>,
}
impl GeminiClientEmitter {
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
self.response_id = Some(frame.id.clone());
self.model = Some(frame.model.clone());
}
fn emit_candidate(
&self,
parts: Vec<Value>,
finish_reason: Option<&str>,
usage: Option<CanonicalUsage>,
) -> Result<Vec<u8>, PipelineFinalizeError> {
let mut candidate = Map::new();
candidate.insert(
"content".to_string(),
json!({
"role": "model",
"parts": parts,
}),
);
candidate.insert("index".to_string(), Value::from(0_u64));
if let Some(finish_reason) = finish_reason {
candidate.insert(
"finishReason".to_string(),
Value::String(map_openai_finish_reason_to_gemini(Some(finish_reason)).to_string()),
);
}
let mut response = Map::new();
response.insert(
"responseId".to_string(),
Value::String(
self.response_id
.clone()
.unwrap_or_else(|| "resp-local-stream".to_string()),
),
);
response.insert(
"modelVersion".to_string(),
Value::String(self.model.clone().unwrap_or_else(|| "unknown".to_string())),
);
response.insert(
"candidates".to_string(),
Value::Array(vec![Value::Object(candidate)]),
);
if let Some(usage) = usage {
response.insert(
"usageMetadata".to_string(),
json!({
"promptTokenCount": usage.input_tokens,
"candidatesTokenCount": usage.output_tokens,
"totalTokenCount": usage.total_tokens,
}),
);
}
encode_json_sse(None, &Value::Object(response))
}
fn flush_pending_tool_calls(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
let mut out = Vec::new();
let mut pending = Vec::new();
for (index, tool_call) in &mut self.tool_calls {
if tool_call.emitted {
continue;
}
let args_value = parse_json_arguments_value(&tool_call.arguments)
.unwrap_or_else(|| Value::Object(Map::new()));
tool_call.emitted = true;
pending.push(json!({
"functionCall": {
"id": if tool_call.call_id.is_empty() {
build_generated_tool_call_id(*index)
} else {
tool_call.call_id.clone()
},
"name": if tool_call.name.is_empty() {
"unknown".to_string()
} else {
tool_call.name.clone()
},
"args": args_value,
}
}));
}
for part in pending {
out.extend(self.emit_candidate(vec![part], None, None)?);
}
Ok(out)
}
pub fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, PipelineFinalizeError> {
self.update_identity(&frame);
match frame.event {
CanonicalStreamEvent::Start => Ok(Vec::new()),
CanonicalStreamEvent::TextDelta(text) => {
self.emit_candidate(vec![json!({ "text": text })], None, None)
}
CanonicalStreamEvent::ToolCallStart {
index,
call_id,
name,
} => {
let state = self.tool_calls.entry(index).or_default();
state.call_id = call_id;
state.name = name;
Ok(Vec::new())
}
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
let emitted_part = {
let state = self.tool_calls.entry(index).or_default();
state.arguments.push_str(&arguments);
if state.emitted {
None
} else {
let args_value = parse_json_arguments_value(&state.arguments);
args_value.map(|args_value| {
state.emitted = true;
json!({
"functionCall": {
"id": if state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
state.call_id.clone()
},
"name": if state.name.is_empty() {
"unknown".to_string()
} else {
state.name.clone()
},
"args": args_value,
}
})
})
}
};
let Some(part) = emitted_part else {
return Ok(Vec::new());
};
self.emit_candidate(vec![part], None, None)
}
CanonicalStreamEvent::Finish {
finish_reason,
usage,
} => {
if self.finished {
return Ok(Vec::new());
}
let mut out = self.flush_pending_tool_calls()?;
out.extend(self.emit_candidate(vec![], finish_reason.as_deref(), usage)?);
self.finished = true;
Ok(out)
}
}
}
pub fn finish(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
if self.finished {
return Ok(Vec::new());
}
let out = self.flush_pending_tool_calls()?;
self.finished = true;
Ok(out)
}
}

View File

@@ -0,0 +1,4 @@
pub mod claude;
pub mod gemini;
pub mod openai;
pub mod stream_core;

View File

@@ -0,0 +1 @@
pub mod stream;

View File

@@ -0,0 +1,959 @@
use std::collections::BTreeMap;
use serde_json::{json, Value};
use crate::conversion::response::build_openai_cli_response;
use crate::finalize::common::build_generated_tool_call_id;
use crate::finalize::sse::{encode_done_sse, encode_json_sse};
use crate::finalize::standard::stream_core::common::*;
use crate::finalize::PipelineFinalizeError;
#[derive(Default)]
struct OpenAIChatProviderToolState {
id: Option<String>,
name: Option<String>,
started_emitted: bool,
}
#[derive(Default)]
pub struct OpenAIChatProviderState {
response_id: Option<String>,
model: Option<String>,
started: bool,
finished: bool,
tool_calls: BTreeMap<usize, OpenAIChatProviderToolState>,
}
#[derive(Default)]
struct OpenAICliProviderToolState {
call_id: String,
name: String,
arguments: String,
started_emitted: bool,
}
#[derive(Default)]
pub struct OpenAICliProviderState {
response_id: Option<String>,
model: Option<String>,
started: bool,
finished: bool,
text: String,
tool_calls: BTreeMap<usize, OpenAICliProviderToolState>,
tool_index_by_key: BTreeMap<String, usize>,
last_tool_index: Option<usize>,
}
impl OpenAIChatProviderState {
fn identity(&self, report_context: &Value) -> (String, String) {
resolve_identity(
self.response_id.as_deref(),
self.model.as_deref(),
report_context,
"chatcmpl-local-stream",
)
}
fn ensure_started(&mut self, report_context: &Value, out: &mut Vec<CanonicalStreamFrame>) {
if self.started {
return;
}
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Start,
});
self.started = true;
}
pub fn push_line(
&mut self,
report_context: &Value,
line: Vec<u8>,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
let Some(value) = decode_json_data_line(&line) else {
return Ok(Vec::new());
};
let Some(chunk_object) = value.as_object() else {
return Ok(Vec::new());
};
self.response_id = chunk_object
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| self.response_id.clone());
self.model = chunk_object
.get("model")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| self.model.clone());
let mut out = Vec::new();
let Some(chunk_choices) = chunk_object.get("choices").and_then(Value::as_array) else {
return Ok(out);
};
for chunk_choice in chunk_choices {
let Some(choice_object) = chunk_choice.as_object() else {
continue;
};
let Some(delta) = choice_object.get("delta").and_then(Value::as_object) else {
if let Some(finish_reason) = normalize_openai_finish_reason(
choice_object.get("finish_reason").and_then(Value::as_str),
) {
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason: Some(finish_reason),
usage: canonical_usage_from_openai_usage(chunk_object.get("usage")),
},
});
self.finished = true;
}
continue;
};
if delta.get("role").and_then(Value::as_str) == Some("assistant") {
self.ensure_started(report_context, &mut out);
}
if let Some(content) = delta.get("content").and_then(Value::as_str) {
if !content.is_empty() {
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::TextDelta(content.to_string()),
});
}
}
if let Some(tool_calls) = delta.get("tool_calls").and_then(Value::as_array) {
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
for tool_call in tool_calls {
let Some(tool_call_object) = tool_call.as_object() else {
continue;
};
let index = tool_call_object
.get("index")
.and_then(Value::as_u64)
.map(|value| value as usize)
.unwrap_or(0);
let state = self.tool_calls.entry(index).or_default();
if let Some(call_id) = tool_call_object.get("id").and_then(Value::as_str) {
state.id = Some(call_id.to_string());
}
if let Some(function) =
tool_call_object.get("function").and_then(Value::as_object)
{
if let Some(name) = function.get("name").and_then(Value::as_str) {
state.name = Some(name.to_string());
}
if !state.started_emitted && (state.id.is_some() || state.name.is_some()) {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallStart {
index,
call_id: state
.id
.clone()
.unwrap_or_else(|| build_generated_tool_call_id(index)),
name: state
.name
.clone()
.unwrap_or_else(|| "unknown".to_string()),
},
});
state.started_emitted = true;
}
if let Some(arguments) = function.get("arguments").and_then(Value::as_str) {
if !arguments.is_empty() {
if !state.started_emitted {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallStart {
index,
call_id: state.id.clone().unwrap_or_else(|| {
build_generated_tool_call_id(index)
}),
name: state
.name
.clone()
.unwrap_or_else(|| "unknown".to_string()),
},
});
state.started_emitted = true;
}
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
index,
arguments: arguments.to_string(),
},
});
}
}
}
}
}
if let Some(finish_reason) = normalize_openai_finish_reason(
choice_object.get("finish_reason").and_then(Value::as_str),
) {
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason: Some(finish_reason),
usage: canonical_usage_from_openai_usage(chunk_object.get("usage")),
},
});
self.finished = true;
}
}
Ok(out)
}
pub fn finish(
&mut self,
report_context: &Value,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
if !self.started || self.finished {
return Ok(Vec::new());
}
self.finished = true;
let (id, model) = self.identity(report_context);
Ok(vec![CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason: None,
usage: None,
},
}])
}
}
impl OpenAICliProviderState {
fn identity(&self, report_context: &Value) -> (String, String) {
resolve_identity(
self.response_id.as_deref(),
self.model.as_deref(),
report_context,
"resp-local-stream",
)
}
fn ensure_started(&mut self, report_context: &Value, out: &mut Vec<CanonicalStreamFrame>) {
if self.started {
return;
}
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Start,
});
self.started = true;
}
fn tool_index_for_key(&mut self, key: Option<String>, output_index: Option<usize>) -> usize {
if let Some(output_index) = output_index {
if let Some(key) = key.as_ref() {
self.tool_index_by_key
.entry(key.clone())
.or_insert(output_index);
}
self.last_tool_index = Some(output_index);
return output_index;
}
if let Some(key) = key.as_ref() {
if let Some(index) = self.tool_index_by_key.get(key).copied() {
self.last_tool_index = Some(index);
return index;
}
}
let index = self.last_tool_index.unwrap_or(self.tool_calls.len());
if let Some(key) = key {
self.tool_index_by_key.insert(key, index);
}
self.last_tool_index = Some(index);
index
}
pub fn push_line(
&mut self,
report_context: &Value,
line: Vec<u8>,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
let Some(value) = decode_json_data_line(&line) else {
return Ok(Vec::new());
};
let mut out = Vec::new();
if let Some(response) = value.get("response").and_then(Value::as_object) {
self.response_id = response
.get("id")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| self.response_id.clone());
self.model = response
.get("model")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| self.model.clone());
}
match value
.get("type")
.and_then(Value::as_str)
.unwrap_or_default()
{
"response.created" => {
self.ensure_started(report_context, &mut out);
}
"response.output_text.delta" => {
let piece = match value.get("delta") {
Some(Value::String(text)) => text.clone(),
Some(Value::Object(delta)) => delta
.get("text")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
_ => String::new(),
};
if !piece.is_empty() {
self.ensure_started(report_context, &mut out);
self.text.push_str(&piece);
let (id, model) = self.identity(report_context);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::TextDelta(piece),
});
}
}
"response.output_item.added" => {
let Some(item) = value.get("item").and_then(Value::as_object) else {
return Ok(out);
};
if item.get("type").and_then(Value::as_str) != Some("function_call") {
return Ok(out);
}
self.ensure_started(report_context, &mut out);
let key = item
.get("call_id")
.or_else(|| item.get("id"))
.and_then(Value::as_str)
.map(ToOwned::to_owned);
let output_index = value
.get("output_index")
.and_then(Value::as_u64)
.map(|value| value as usize);
let index = self.tool_index_for_key(key.clone(), output_index);
let (id, model) = self.identity(report_context);
let state = self.tool_calls.entry(index).or_default();
state.call_id = item
.get("call_id")
.or_else(|| item.get("id"))
.and_then(Value::as_str)
.unwrap_or_else(|| state.call_id.as_str())
.to_string();
state.name = item
.get("name")
.and_then(Value::as_str)
.unwrap_or_else(|| state.name.as_str())
.to_string();
if !state.started_emitted {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallStart {
index,
call_id: if state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
state.call_id.clone()
},
name: if state.name.is_empty() {
"unknown".to_string()
} else {
state.name.clone()
},
},
});
state.started_emitted = true;
}
if let Some(arguments) = item.get("arguments").and_then(Value::as_str) {
if !arguments.is_empty() {
state.arguments.push_str(arguments);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
index,
arguments: arguments.to_string(),
},
});
}
}
}
"response.function_call_arguments.delta" => {
let delta = value
.get("delta")
.and_then(Value::as_str)
.unwrap_or_default();
if delta.is_empty() {
return Ok(out);
}
self.ensure_started(report_context, &mut out);
let key = value
.get("item_id")
.or_else(|| value.get("call_id"))
.or_else(|| value.get("id"))
.and_then(Value::as_str)
.map(ToOwned::to_owned);
let output_index = value
.get("output_index")
.and_then(Value::as_u64)
.map(|value| value as usize);
let index = self.tool_index_for_key(key, output_index);
let (id, model) = self.identity(report_context);
let state = self.tool_calls.entry(index).or_default();
if !state.started_emitted {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallStart {
index,
call_id: if state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
state.call_id.clone()
},
name: if state.name.is_empty() {
"unknown".to_string()
} else {
state.name.clone()
},
},
});
state.started_emitted = true;
}
state.arguments.push_str(delta);
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
index,
arguments: delta.to_string(),
},
});
}
"response.completed" => {
let Some(response) = value.get("response").and_then(Value::as_object) else {
return Ok(out);
};
self.ensure_started(report_context, &mut out);
let (id, model) = self.identity(report_context);
for raw_item in response
.get("output")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let Some(item) = raw_item.as_object() else {
continue;
};
match item.get("type").and_then(Value::as_str).unwrap_or_default() {
"message" => {
let mut completed_text = String::new();
for raw_content in item
.get("content")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let Some(content) = raw_content.as_object() else {
continue;
};
if content.get("type").and_then(Value::as_str)
== Some("output_text")
{
if let Some(text) = content.get("text").and_then(Value::as_str)
{
completed_text.push_str(text);
}
}
}
let missing = if completed_text.starts_with(&self.text) {
completed_text[self.text.len()..].to_string()
} else if self.text == completed_text {
String::new()
} else {
completed_text.clone()
};
if !missing.is_empty() {
self.text.push_str(&missing);
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::TextDelta(missing),
});
}
}
"function_call" => {
let key = item
.get("call_id")
.or_else(|| item.get("id"))
.and_then(Value::as_str)
.map(ToOwned::to_owned);
let index = self.tool_index_for_key(key, None);
let state = self.tool_calls.entry(index).or_default();
state.call_id = item
.get("call_id")
.or_else(|| item.get("id"))
.and_then(Value::as_str)
.unwrap_or_else(|| state.call_id.as_str())
.to_string();
state.name = item
.get("name")
.and_then(Value::as_str)
.unwrap_or_else(|| state.name.as_str())
.to_string();
if !state.started_emitted {
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallStart {
index,
call_id: if state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
state.call_id.clone()
},
name: if state.name.is_empty() {
"unknown".to_string()
} else {
state.name.clone()
},
},
});
state.started_emitted = true;
}
let completed_arguments = item
.get("arguments")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let missing = if completed_arguments.starts_with(&state.arguments) {
completed_arguments[state.arguments.len()..].to_string()
} else if state.arguments == completed_arguments {
String::new()
} else {
completed_arguments.clone()
};
if !missing.is_empty() {
state.arguments.push_str(&missing);
out.push(CanonicalStreamFrame {
id: id.clone(),
model: model.clone(),
event: CanonicalStreamEvent::ToolCallArgumentsDelta {
index,
arguments: missing,
},
});
}
}
_ => {}
}
}
let finish_reason = if self.tool_calls.is_empty() {
Some("stop".to_string())
} else {
Some("tool_calls".to_string())
};
out.push(CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason,
usage: canonical_usage_from_openai_usage(response.get("usage")),
},
});
self.finished = true;
}
_ => {}
}
Ok(out)
}
pub fn finish(
&mut self,
report_context: &Value,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
if !self.started || self.finished {
return Ok(Vec::new());
}
self.finished = true;
let (id, model) = self.identity(report_context);
let finish_reason = if self.tool_calls.is_empty() {
Some("stop".to_string())
} else {
Some("tool_calls".to_string())
};
Ok(vec![CanonicalStreamFrame {
id,
model,
event: CanonicalStreamEvent::Finish {
finish_reason,
usage: None,
},
}])
}
}
#[derive(Default)]
pub struct OpenAIChatClientEmitter {
response_id: Option<String>,
model: Option<String>,
started: bool,
finished: bool,
}
#[derive(Default)]
struct OpenAICliClientToolState {
call_id: String,
name: String,
arguments: String,
}
#[derive(Default)]
pub struct OpenAICliClientEmitter {
response_id: Option<String>,
model: Option<String>,
started: bool,
finished: bool,
text: String,
tool_calls: BTreeMap<usize, OpenAICliClientToolState>,
}
impl OpenAIChatClientEmitter {
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
self.response_id = Some(frame.id.clone());
self.model = Some(frame.model.clone());
}
fn ensure_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
if self.started {
return Ok(Vec::new());
}
self.started = true;
Ok(encode_json_sse(
None,
&build_openai_chat_role_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
),
)?)
}
pub fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, PipelineFinalizeError> {
self.update_identity(&frame);
match frame.event {
CanonicalStreamEvent::Start => self.ensure_started(),
CanonicalStreamEvent::TextDelta(text) => {
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&build_openai_chat_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
text,
None,
None,
),
)?);
Ok(out)
}
CanonicalStreamEvent::ToolCallStart {
index,
call_id,
name,
} => {
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&build_openai_chat_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
String::new(),
Some(vec![json!({
"index": index,
"id": call_id,
"type": "function",
"function": {
"name": name,
"arguments": "",
}
})]),
None,
),
)?);
Ok(out)
}
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": {
"tool_calls": [{
"index": index,
"function": {
"arguments": arguments,
}
}]
},
"finish_reason": Value::Null
}]
}),
)?);
Ok(out)
}
CanonicalStreamEvent::Finish { finish_reason, .. } => {
if self.finished {
return Ok(Vec::new());
}
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&build_openai_chat_finish_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
finish_reason.as_deref(),
),
)?);
out.extend(encode_done_sse());
self.finished = true;
Ok(out)
}
}
}
pub fn finish(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
if !self.started || self.finished {
return Ok(Vec::new());
}
let out = encode_json_sse(
None,
&build_openai_chat_finish_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
None,
),
)?;
self.finished = true;
let mut bytes = out;
bytes.extend(encode_done_sse());
Ok(bytes)
}
}
impl OpenAICliClientEmitter {
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
self.response_id = Some(frame.id.clone().replace("chatcmpl", "resp"));
self.model = Some(frame.model.clone());
}
fn ensure_started(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
if self.started {
return Ok(Vec::new());
}
self.started = true;
encode_json_sse(
Some("response.created"),
&json!({
"type": "response.created",
"response": {
"id": self.response_id.as_deref().unwrap_or("resp-local-stream"),
"object": "response",
"model": self.model.as_deref().unwrap_or("unknown"),
"status": "in_progress",
"output": [],
}
}),
)
}
fn function_output_index(&self, index: usize) -> usize {
if self.text.is_empty() {
index
} else {
index + 1
}
}
pub fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, PipelineFinalizeError> {
self.update_identity(&frame);
match frame.event {
CanonicalStreamEvent::Start => self.ensure_started(),
CanonicalStreamEvent::TextDelta(text) => {
let mut out = self.ensure_started()?;
self.text.push_str(&text);
out.extend(encode_json_sse(
Some("response.output_text.delta"),
&json!({
"type": "response.output_text.delta",
"output_index": 0,
"content_index": 0,
"delta": text,
}),
)?);
Ok(out)
}
CanonicalStreamEvent::ToolCallStart {
index,
call_id,
name,
} => {
let mut out = self.ensure_started()?;
let output_index = self.function_output_index(index);
let state = self.tool_calls.entry(index).or_default();
state.call_id = call_id.clone();
state.name = name.clone();
out.extend(encode_json_sse(
Some("response.output_item.added"),
&json!({
"type": "response.output_item.added",
"output_index": output_index,
"item": {
"type": "function_call",
"id": call_id,
"call_id": state.call_id,
"name": state.name,
"arguments": "",
}
}),
)?);
Ok(out)
}
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
let mut out = self.ensure_started()?;
let output_index = self.function_output_index(index);
let state = self.tool_calls.entry(index).or_default();
state.arguments.push_str(&arguments);
out.extend(encode_json_sse(
Some("response.function_call_arguments.delta"),
&json!({
"type": "response.function_call_arguments.delta",
"output_index": output_index,
"item_id": if state.call_id.is_empty() {
build_generated_tool_call_id(index)
} else {
state.call_id.clone()
},
"delta": arguments,
}),
)?);
Ok(out)
}
CanonicalStreamEvent::Finish { usage, .. } => {
if self.finished {
return Ok(Vec::new());
}
let mut out = self.ensure_started()?;
let usage = usage.unwrap_or_default();
let function_calls = self
.tool_calls
.iter()
.map(|(index, state)| {
json!({
"type": "function_call",
"id": if state.call_id.is_empty() {
build_generated_tool_call_id(*index)
} else {
state.call_id.clone()
},
"call_id": if state.call_id.is_empty() {
build_generated_tool_call_id(*index)
} else {
state.call_id.clone()
},
"name": if state.name.is_empty() {
"unknown".to_string()
} else {
state.name.clone()
},
"arguments": state.arguments.clone(),
})
})
.collect::<Vec<_>>();
out.extend(encode_json_sse(
Some("response.completed"),
&json!({
"type": "response.completed",
"response": build_openai_cli_response(
self.response_id.as_deref().unwrap_or("resp-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
&self.text,
function_calls,
usage.input_tokens,
usage.output_tokens,
usage.total_tokens,
),
}),
)?);
self.finished = true;
Ok(out)
}
}
}
pub fn finish(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
if !self.started || self.finished {
return Ok(Vec::new());
}
self.emit(CanonicalStreamFrame {
id: self
.response_id
.clone()
.unwrap_or_else(|| "resp-local-stream".to_string()),
model: self.model.clone().unwrap_or_else(|| "unknown".to_string()),
event: CanonicalStreamEvent::Finish {
finish_reason: None,
usage: None,
},
})
}
}

View File

@@ -0,0 +1,218 @@
use serde_json::{json, Map, Value};
#[derive(Clone, Debug, Default)]
pub struct CanonicalUsage {
pub input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
}
#[derive(Clone, Debug)]
pub enum CanonicalStreamEvent {
Start,
TextDelta(String),
ToolCallStart {
index: usize,
call_id: String,
name: String,
},
ToolCallArgumentsDelta {
index: usize,
arguments: String,
},
Finish {
finish_reason: Option<String>,
usage: Option<CanonicalUsage>,
},
}
#[derive(Clone, Debug)]
pub struct CanonicalStreamFrame {
pub id: String,
pub model: String,
pub event: CanonicalStreamEvent,
}
pub fn decode_json_data_line(line: &[u8]) -> Option<Value> {
let text = std::str::from_utf8(line).ok()?;
let trimmed = text.trim_matches('\r').trim();
if trimmed.is_empty() || trimmed.starts_with(':') || trimmed.starts_with("event:") {
return None;
}
let data_line = trimmed.strip_prefix("data:")?.trim();
if data_line.is_empty() || data_line == "[DONE]" {
return None;
}
serde_json::from_str(data_line).ok()
}
pub fn resolve_identity(
response_id: Option<&str>,
model: Option<&str>,
report_context: &Value,
default_id: &str,
) -> (String, String) {
let id = response_id
.filter(|value| !value.is_empty())
.unwrap_or(default_id)
.to_string();
let model = model
.filter(|value| !value.is_empty())
.or_else(|| report_context.get("mapped_model").and_then(Value::as_str))
.or_else(|| report_context.get("model").and_then(Value::as_str))
.unwrap_or("unknown")
.to_string();
(id, model)
}
pub fn canonical_usage_from_openai_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage = value?.as_object()?;
let input_tokens = usage
.get("input_tokens")
.or_else(|| usage.get("prompt_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("output_tokens")
.or_else(|| usage.get("completion_tokens"))
.and_then(Value::as_u64)
.unwrap_or(0);
let total_tokens = usage
.get("total_tokens")
.and_then(Value::as_u64)
.unwrap_or(input_tokens + output_tokens);
Some(CanonicalUsage {
input_tokens,
output_tokens,
total_tokens,
})
}
pub fn canonical_usage_from_claude_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage = value?.as_object()?;
let input_tokens = usage
.get("input_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("output_tokens")
.and_then(Value::as_u64)
.unwrap_or(0);
Some(CanonicalUsage {
input_tokens,
output_tokens,
total_tokens: input_tokens + output_tokens,
})
}
pub fn canonical_usage_from_gemini_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage = value?.as_object()?;
let input_tokens = usage
.get("promptTokenCount")
.and_then(Value::as_u64)
.unwrap_or(0);
let output_tokens = usage
.get("candidatesTokenCount")
.and_then(Value::as_u64)
.unwrap_or(0);
let total_tokens = usage
.get("totalTokenCount")
.and_then(Value::as_u64)
.unwrap_or(input_tokens + output_tokens);
Some(CanonicalUsage {
input_tokens,
output_tokens,
total_tokens,
})
}
pub fn normalize_openai_finish_reason(value: Option<&str>) -> Option<String> {
match value {
Some("function_call") => Some("tool_calls".to_string()),
Some(other) if !other.trim().is_empty() => Some(other.to_string()),
_ => None,
}
}
pub fn map_openai_finish_reason_to_claude(value: Option<&str>) -> &'static str {
match value {
Some("length") => "max_tokens",
Some("tool_calls") | Some("function_call") => "tool_use",
Some("content_filter") => "content_filtered",
_ => "end_turn",
}
}
pub fn map_openai_finish_reason_to_gemini(value: Option<&str>) -> &'static str {
match value {
Some("length") => "MAX_TOKENS",
Some("content_filter") => "SAFETY",
_ => "STOP",
}
}
pub fn parse_json_arguments_value(arguments: &str) -> Option<Value> {
let trimmed = arguments.trim();
if trimmed.is_empty() {
return Some(Value::Object(Map::new()));
}
serde_json::from_str(trimmed).ok()
}
pub fn build_openai_chat_chunk(
id: &str,
model: &str,
text: String,
tool_calls: Option<Vec<Value>>,
finish_reason: Option<&str>,
) -> Value {
let mut delta = Map::new();
delta.insert("role".to_string(), Value::String("assistant".to_string()));
if !text.is_empty() {
delta.insert("content".to_string(), Value::String(text));
} else if tool_calls.is_none() {
delta.insert("content".to_string(), Value::String(String::new()));
}
if let Some(tool_calls) = tool_calls {
delta.insert("tool_calls".to_string(), Value::Array(tool_calls));
}
json!({
"id": id,
"object": "chat.completion.chunk",
"model": model,
"choices": [{
"index": 0,
"delta": Value::Object(delta),
"finish_reason": finish_reason,
}]
})
}
pub fn build_openai_chat_role_chunk(id: &str, model: &str) -> Value {
json!({
"id": id,
"object": "chat.completion.chunk",
"model": model,
"choices": [{
"index": 0,
"delta": {
"role": "assistant"
},
"finish_reason": Value::Null
}]
})
}
pub fn build_openai_chat_finish_chunk(id: &str, model: &str, finish_reason: Option<&str>) -> Value {
json!({
"id": id,
"object": "chat.completion.chunk",
"model": model,
"choices": [{
"index": 0,
"delta": {},
"finish_reason": finish_reason,
}]
})
}

View File

@@ -0,0 +1,161 @@
use serde_json::Value;
use crate::finalize::standard::claude::stream::{ClaudeClientEmitter, ClaudeProviderState};
use crate::finalize::standard::gemini::stream::{GeminiClientEmitter, GeminiProviderState};
use crate::finalize::standard::openai::stream::{
OpenAIChatClientEmitter, OpenAIChatProviderState, OpenAICliClientEmitter,
OpenAICliProviderState,
};
use crate::finalize::standard::stream_core::common::CanonicalStreamFrame;
use crate::finalize::PipelineFinalizeError;
#[derive(Default)]
pub struct StreamingStandardFormatMatrix {
provider: Option<ProviderStreamParser>,
client: Option<ClientStreamEmitter>,
}
impl StreamingStandardFormatMatrix {
pub fn transform_line(
&mut self,
report_context: &Value,
line: Vec<u8>,
) -> Result<Vec<u8>, PipelineFinalizeError> {
self.ensure_initialized(report_context);
let Some(provider) = self.provider.as_mut() else {
return Ok(Vec::new());
};
let frames = provider.push_line(report_context, line)?;
self.emit_frames(frames)
}
pub fn finish(&mut self, report_context: &Value) -> Result<Vec<u8>, PipelineFinalizeError> {
self.ensure_initialized(report_context);
let Some(provider) = self.provider.as_mut() else {
return Ok(Vec::new());
};
let frames = provider.finish(report_context)?;
let mut out = self.emit_frames(frames)?;
if let Some(client) = self.client.as_mut() {
out.extend(client.finish()?);
}
Ok(out)
}
fn ensure_initialized(&mut self, report_context: &Value) {
if self.provider.is_some() && self.client.is_some() {
return;
}
let provider_api_format = report_context
.get("provider_api_format")
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
let client_api_format = report_context
.get("client_api_format")
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
self.provider = ProviderStreamParser::for_api_format(provider_api_format.as_str());
self.client = ClientStreamEmitter::for_api_format(client_api_format.as_str());
}
fn emit_frames(
&mut self,
frames: Vec<CanonicalStreamFrame>,
) -> Result<Vec<u8>, PipelineFinalizeError> {
let Some(client) = self.client.as_mut() else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for frame in frames {
out.extend(client.emit(frame)?);
}
Ok(out)
}
}
enum ProviderStreamParser {
OpenAIChat(OpenAIChatProviderState),
OpenAICli(OpenAICliProviderState),
Claude(ClaudeProviderState),
Gemini(GeminiProviderState),
}
impl ProviderStreamParser {
fn for_api_format(provider_api_format: &str) -> Option<Self> {
Some(match provider_api_format {
"openai:chat" => Self::OpenAIChat(OpenAIChatProviderState::default()),
"openai:cli" | "openai:compact" => Self::OpenAICli(OpenAICliProviderState::default()),
"claude:chat" | "claude:cli" => Self::Claude(ClaudeProviderState::default()),
"gemini:chat" | "gemini:cli" => Self::Gemini(GeminiProviderState::default()),
_ => return None,
})
}
fn push_line(
&mut self,
report_context: &Value,
line: Vec<u8>,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
match self {
ProviderStreamParser::OpenAIChat(state) => state.push_line(report_context, line),
ProviderStreamParser::OpenAICli(state) => state.push_line(report_context, line),
ProviderStreamParser::Claude(state) => state.push_line(report_context, line),
ProviderStreamParser::Gemini(state) => state.push_line(report_context, line),
}
}
fn finish(
&mut self,
report_context: &Value,
) -> Result<Vec<CanonicalStreamFrame>, PipelineFinalizeError> {
match self {
ProviderStreamParser::OpenAIChat(state) => state.finish(report_context),
ProviderStreamParser::OpenAICli(state) => state.finish(report_context),
ProviderStreamParser::Claude(state) => state.finish(report_context),
ProviderStreamParser::Gemini(state) => state.finish(report_context),
}
}
}
enum ClientStreamEmitter {
OpenAIChat(OpenAIChatClientEmitter),
OpenAICli(OpenAICliClientEmitter),
Claude(ClaudeClientEmitter),
Gemini(GeminiClientEmitter),
}
impl ClientStreamEmitter {
fn for_api_format(client_api_format: &str) -> Option<Self> {
Some(match client_api_format {
"openai:chat" => Self::OpenAIChat(OpenAIChatClientEmitter::default()),
"openai:cli" | "openai:compact" => Self::OpenAICli(OpenAICliClientEmitter::default()),
"claude:chat" | "claude:cli" => Self::Claude(ClaudeClientEmitter::default()),
"gemini:chat" | "gemini:cli" => Self::Gemini(GeminiClientEmitter::default()),
_ => return None,
})
}
fn emit(&mut self, frame: CanonicalStreamFrame) -> Result<Vec<u8>, PipelineFinalizeError> {
match self {
ClientStreamEmitter::OpenAIChat(state) => state.emit(frame),
ClientStreamEmitter::OpenAICli(state) => state.emit(frame),
ClientStreamEmitter::Claude(state) => state.emit(frame),
ClientStreamEmitter::Gemini(state) => state.emit(frame),
}
}
fn finish(&mut self) -> Result<Vec<u8>, PipelineFinalizeError> {
match self {
ClientStreamEmitter::OpenAIChat(state) => state.finish(),
ClientStreamEmitter::OpenAICli(state) => state.finish(),
ClientStreamEmitter::Claude(state) => state.finish(),
ClientStreamEmitter::Gemini(state) => state.finish(),
}
}
}

View File

@@ -0,0 +1,5 @@
pub mod common;
pub mod format_matrix;
pub use common::{CanonicalStreamEvent, CanonicalStreamFrame};
pub use format_matrix::StreamingStandardFormatMatrix;

View File

@@ -0,0 +1,147 @@
use serde_json::Value;
use crate::adaptation::surfaces::{
provider_adaptation_should_unwrap_stream_envelope, KIRO_ENVELOPE_NAME,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FinalizeStreamRewriteMode {
EnvelopeUnwrap,
Standard,
KiroToClaudeCli,
}
pub fn resolve_finalize_stream_rewrite_mode(
report_context: &Value,
) -> Option<FinalizeStreamRewriteMode> {
let needs_conversion = report_context
.get("needs_conversion")
.and_then(Value::as_bool)
.unwrap_or(false);
let envelope_name = report_context
.get("envelope_name")
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
let provider_api_format = report_context
.get("provider_api_format")
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
let client_api_format = report_context
.get("client_api_format")
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
if needs_conversion {
return supports_standard_stream_rewrite(
provider_api_format.as_str(),
client_api_format.as_str(),
)
.then_some(FinalizeStreamRewriteMode::Standard);
}
if envelope_name.eq_ignore_ascii_case(KIRO_ENVELOPE_NAME) {
return (provider_api_format == "claude:cli" && client_api_format == "claude:cli")
.then_some(FinalizeStreamRewriteMode::KiroToClaudeCli);
}
(provider_api_format == client_api_format
&& provider_adaptation_should_unwrap_stream_envelope(
envelope_name.as_str(),
provider_api_format.as_str(),
))
.then_some(FinalizeStreamRewriteMode::EnvelopeUnwrap)
}
fn supports_standard_stream_rewrite(provider_api_format: &str, client_api_format: &str) -> bool {
is_standard_provider_api_format(provider_api_format)
&& (is_standard_chat_client_api_format(client_api_format)
|| is_standard_cli_client_api_format(client_api_format))
}
fn is_standard_provider_api_format(api_format: &str) -> bool {
matches!(
api_format,
"openai:chat"
| "openai:cli"
| "openai:compact"
| "claude:chat"
| "claude:cli"
| "gemini:chat"
| "gemini:cli"
)
}
fn is_standard_chat_client_api_format(api_format: &str) -> bool {
matches!(api_format, "openai:chat" | "claude:chat" | "gemini:chat")
}
fn is_standard_cli_client_api_format(api_format: &str) -> bool {
matches!(
api_format,
"openai:cli" | "openai:compact" | "claude:cli" | "gemini:cli"
)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{resolve_finalize_stream_rewrite_mode, FinalizeStreamRewriteMode};
#[test]
fn resolves_standard_mode_for_cross_format_standard_streams() {
let report_context = json!({
"provider_api_format": "claude:chat",
"client_api_format": "openai:chat",
"needs_conversion": true,
});
assert_eq!(
resolve_finalize_stream_rewrite_mode(&report_context),
Some(FinalizeStreamRewriteMode::Standard)
);
}
#[test]
fn resolves_envelope_unwrap_for_same_format_private_envelopes() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"client_api_format": "gemini:cli",
"envelope_name": "antigravity:v1internal",
"needs_conversion": false,
});
assert_eq!(
resolve_finalize_stream_rewrite_mode(&report_context),
Some(FinalizeStreamRewriteMode::EnvelopeUnwrap)
);
}
#[test]
fn resolves_kiro_same_format_streams_to_kiro_mode() {
let report_context = json!({
"provider_api_format": "claude:cli",
"client_api_format": "claude:cli",
"envelope_name": "kiro:generateAssistantResponse",
"needs_conversion": false,
});
assert_eq!(
resolve_finalize_stream_rewrite_mode(&report_context),
Some(FinalizeStreamRewriteMode::KiroToClaudeCli)
);
}
#[test]
fn rejects_unsupported_non_conversion_streams() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "openai:chat",
"needs_conversion": false,
});
assert_eq!(resolve_finalize_stream_rewrite_mode(&report_context), None);
}
}

File diff suppressed because it is too large Load Diff