focus/crates/focus-providers/src/openai.rs

820 lines
33 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! OpenAI providerResponses API 与 Chat Completions 双协议SSE 流式)。
//! OpenAI provider: both the Responses API and Chat Completions (SSE streaming).
//!
//! 两套协议共享同一套核心类型翻译messages / tools / usage / stop_reason
//! 仅请求格式与流式事件不同;通过 [`OpenAiProtocol`] 切换。
//! Both protocols share the same core-type translation (messages / tools /
//! usage / stop_reason); only the request format and stream events differ,
//! selected via [`OpenAiProtocol`].
use crate::common;
use crate::config::{split_base_url, ProviderConfig};
use focus_core::model::*;
use focus_core::provider::{
ProviderEventReducer, ProviderRequest, StreamEvent, StreamProvider, StreamResult,
};
use focus_core::CoreError;
use focus_json::JsonValue;
use focus_transport::{HttpRequest, SseEvent, SseParser, Transport};
use std::collections::HashSet;
use std::fmt;
use std::sync::mpsc::SyncSender;
use std::sync::Arc;
/// OpenAI 默认 API 基址。
/// The default OpenAI API base.
const DEFAULT_BASE_URL: &str = "https://api.openai.com";
/// 两套 OpenAI 协议。
/// The two OpenAI protocols.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OpenAiProtocol {
/// 最新的 Responses API`/v1/responses`)。
/// The latest Responses API (`/v1/responses`).
#[default]
Responses,
/// 传统 Chat Completions`/v1/chat/completions`),兼容绝大多数第三方端点。
/// Legacy Chat Completions (`/v1/chat/completions`), compatible with most
/// third-party endpoints.
ChatCompletions,
}
impl OpenAiProtocol {
fn endpoint_path(&self, prefix: &str) -> String {
match self {
OpenAiProtocol::Responses => format!("{}/v1/responses", prefix),
OpenAiProtocol::ChatCompletions => format!("{}/v1/chat/completions", prefix),
}
}
}
/// 实现 OpenAI API 的 provider。
/// A provider implementing the OpenAI API.
#[derive(Clone)]
pub struct OpenAiProvider {
config: ProviderConfig,
protocol: OpenAiProtocol,
transport: Arc<dyn Transport>,
}
impl fmt::Debug for OpenAiProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OpenAiProvider")
.field("config", &self.config)
.field("protocol", &self.protocol)
.field("transport", &"<Arc<dyn Transport>>")
.finish()
}
}
impl OpenAiProvider {
/// 使用默认 HTTP 传输构造 provider。
/// Build a provider with the default HTTP transport.
pub fn new(config: ProviderConfig, protocol: OpenAiProtocol) -> Self {
Self::with_transport(
config,
protocol,
Arc::new(focus_transport::HttpTransport::new()),
)
}
/// 注入自定义传输(测试用 mock构造 provider。
/// Build a provider with an injected transport (e.g. a mock in tests).
pub fn with_transport(
config: ProviderConfig,
protocol: OpenAiProtocol,
transport: Arc<dyn Transport>,
) -> Self {
Self {
config,
protocol,
transport,
}
}
/// 依据配置解析目标 (host, port, path)。
/// Resolve the target (host, port, path) from the config.
fn endpoint(&self) -> Result<(String, u16, String), String> {
let base = self
.config
.base_url
.clone()
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
let (host, port, prefix) = split_base_url(&base)?;
Ok((host, port, self.protocol.endpoint_path(&prefix)))
}
}
impl StreamProvider for OpenAiProvider {
fn stream(&self, request: &ProviderRequest) -> StreamResult {
let (host, port, path) = self.endpoint().map_err(CoreError::Provider)?;
let body = match self.protocol {
OpenAiProtocol::Responses => build_responses_body(request),
OpenAiProtocol::ChatCompletions => build_chat_body(request),
};
let body = focus_json::to_string(&body);
let model = request.model.clone();
let req = HttpRequest::post(&host, &path)
.port(port)
.header("Authorization", &format!("Bearer {}", self.config.api_key))
.header("accept", "text/event-stream")
.json_body(&body);
let transport = self.transport.clone();
let timeout = self.config.timeout;
// 先取出协议Copy 类型),避免闭包借用 self。
// Copy the protocol out first so the closure doesn't borrow self.
let protocol = self.protocol;
common::run_stream(move |tx| {
Box::pin(async move {
let model = model.clone();
let result: Result<(), String> = async {
let mut chunks = transport
.stream_request(&req, timeout)
.await
.map_err(|e| e.to_string())?;
let mut sse = SseParser::new();
let mut turn = match protocol {
OpenAiProtocol::Responses => OpenAiTurn::responses(&model),
OpenAiProtocol::ChatCompletions => OpenAiTurn::chat(&model),
};
loop {
let chunk = chunks.next_chunk().await.map_err(|e| e.to_string())?;
match chunk {
Some(bytes) => {
common::debug_chunk(&bytes);
sse.feed(&bytes);
while let Some(ev) = sse.next_event() {
match turn.handle_sse(&ev, &tx) {
Ok(true) => {}
Ok(false) => return Ok(()), // 消费者已离开 / consumer gone
Err(msg) => {
let _ = tx.send(StreamEvent::Error {
error: common::error_message(&model, &msg),
});
return Ok(());
}
}
if turn.done {
return Ok(());
}
}
}
None => break,
}
}
if !turn.done {
return Err("stream ended without a terminal event".to_string());
}
Ok(())
}
.await;
if let Err(msg) = result {
let _ = tx.send(StreamEvent::Error {
error: common::error_message(&model, &msg),
});
}
})
})
}
}
// ---- 请求体构造 ----
// ---- request-body construction -------------------------------------------
/// Chat Completions 请求体。
/// Chat Completions request body.
fn build_chat_body(request: &ProviderRequest) -> JsonValue {
let mut body = JsonValue::obj();
body.insert("model", request.model.clone().into()).ok();
let mut messages = JsonValue::arr();
// 系统提示作为第一条 system 消息。
// The system prompt becomes the first system message.
if !request.system_prompt.is_empty() {
let mut sys = JsonValue::obj();
sys.insert("role", "system".into()).ok();
sys.insert("content", request.system_prompt.clone().into())
.ok();
messages.push(sys).ok();
}
for m in &request.messages {
messages.push(chat_message(m)).ok();
}
body.insert("messages", messages).ok();
if has_tools(&request.tools) {
body.insert("tools", tools_to_chat(&request.tools)).ok();
body.insert("tool_choice", "auto".into()).ok();
}
if let Some(mt) = request.max_tokens {
// 新模型使用 max_completion_tokens旧字段 max_tokens 已被弃用)。
// Newer models use max_completion_tokens (max_tokens is deprecated).
body.insert("max_completion_tokens", (mt as f64).into())
.ok();
}
if let Some(t) = request.temperature {
body.insert("temperature", t.into()).ok();
}
body.insert("stream", true.into()).ok();
// 请求结束 chunk 携带用量。
// Ask for usage in the final chunk.
let mut so = JsonValue::obj();
so.insert("include_usage", true.into()).ok();
body.insert("stream_options", so).ok();
body
}
/// Responses API 请求体。
/// Responses API request body.
fn build_responses_body(request: &ProviderRequest) -> JsonValue {
let mut body = JsonValue::obj();
body.insert("model", request.model.clone().into()).ok();
if !request.system_prompt.is_empty() {
body.insert("instructions", request.system_prompt.clone().into())
.ok();
}
body.insert("input", messages_to_responses(&request.messages))
.ok();
if has_tools(&request.tools) {
body.insert("tools", tools_to_responses(&request.tools))
.ok();
}
if let Some(mt) = request.max_tokens {
body.insert("max_output_tokens", (mt as f64).into()).ok();
}
if let Some(t) = request.temperature {
body.insert("temperature", t.into()).ok();
}
body.insert("stream", true.into()).ok();
body
}
/// 一条核心消息 → Chat Completions 消息。
/// One core message → a Chat Completions message.
fn chat_message(m: &Message) -> JsonValue {
match m {
Message::User(u) => {
let mut o = JsonValue::obj();
o.insert("role", "user".into()).ok();
let text = join_text(&u.content);
let images: Vec<&ImageContent> = u
.content
.iter()
.filter_map(|c| match c {
ContentBlock::Image(i) => Some(i),
_ => None,
})
.collect();
if images.is_empty() {
o.insert("content", text.into()).ok();
} else {
let mut parts = JsonValue::arr();
if !text.is_empty() {
let mut p = JsonValue::obj();
p.insert("type", "text".into()).ok();
p.insert("text", text.clone().into()).ok();
parts.push(p).ok();
}
for img in images {
let mut p = JsonValue::obj();
p.insert("type", "image_url".into()).ok();
let mut url = JsonValue::obj();
url.insert(
"url",
format!("data:{};base64,{}", img.mime_type, img.data).into(),
)
.ok();
p.insert("image_url", url).ok();
parts.push(p).ok();
}
o.insert("content", parts).ok();
}
o
}
Message::Assistant(a) => {
let mut o = JsonValue::obj();
o.insert("role", "assistant".into()).ok();
o.insert("content", join_text(&a.content).into()).ok();
// 推理 API如 DeepSeek thinking 模式)要求把思考内容作为
// reasoning_content 原样回传OpenAI 官方不会产生思考块,因此
// 仅在存在思考块时发送该字段,安全兼容两者。
// Reasoning-capable APIs (e.g. DeepSeek thinking mode) require the
// thinking text to be echoed back as reasoning_content; OpenAI
// official never produces thinking blocks, so sending the field
// only when thinking exists stays safe for both.
let reasoning: String = a
.content
.iter()
.filter_map(|c| match c {
ContentBlock::Thinking(t) => Some(t.thinking.clone()),
_ => None,
})
.collect();
if !reasoning.is_empty() {
// 同时写 reasoning_contentDeepSeek 官方)与 reasoning_text
//(部分兼容层);推理端点普遍接受这两个字段名。
// Write both reasoning_content (DeepSeek official) and
// reasoning_text (some compatible layers); reasoning-capable
// endpoints generally accept either name.
o.insert("reasoning_content", reasoning.clone().into()).ok();
o.insert("reasoning_text", reasoning.into()).ok();
}
let calls: Vec<&ToolCall> = a.content.iter().filter_map(|c| c.as_tool_call()).collect();
if !calls.is_empty() {
let mut arr = JsonValue::arr();
for tc in calls {
let mut c = JsonValue::obj();
c.insert("id", tc.id.clone().into()).ok();
c.insert("type", "function".into()).ok();
let mut f = JsonValue::obj();
f.insert("name", tc.name.clone().into()).ok();
f.insert("arguments", focus_json::to_string(&tc.arguments).into())
.ok();
c.insert("function", f).ok();
arr.push(c).ok();
}
o.insert("tool_calls", arr).ok();
}
o
}
Message::ToolResult(t) => {
let mut o = JsonValue::obj();
o.insert("role", "tool".into()).ok();
o.insert("tool_call_id", t.tool_call_id.clone().into()).ok();
o.insert("content", join_text(&t.content).into()).ok();
o
}
}
}
/// 核心消息列表 → Responses API input items。
/// Core messages → Responses API input items.
fn messages_to_responses(messages: &[Message]) -> JsonValue {
let mut items = JsonValue::arr();
for m in messages {
match m {
Message::User(u) => {
let mut item = JsonValue::obj();
item.insert("role", "user".into()).ok();
let mut content = JsonValue::arr();
let text = join_text(&u.content);
if !text.is_empty() {
let mut part = JsonValue::obj();
part.insert("type", "input_text".into()).ok();
part.insert("text", text.into()).ok();
content.push(part).ok();
}
item.insert("content", content).ok();
items.push(item).ok();
}
Message::Assistant(a) => {
// 推理内容必须回传为 reasoning itemthinking 模式 API 校验)。
// Thinking must be replayed as a reasoning item (thinking-mode
// APIs validate this).
let reasoning: String = a
.content
.iter()
.filter_map(|c| match c {
ContentBlock::Thinking(t) => Some(t.thinking.clone()),
_ => None,
})
.collect();
if !reasoning.is_empty() {
let mut ri = JsonValue::obj();
ri.insert("type", "reasoning".into()).ok();
let mut part = JsonValue::obj();
part.insert("type", "reasoning_text".into()).ok();
part.insert("text", reasoning.into()).ok();
let mut content = JsonValue::arr();
content.push(part).ok();
ri.insert("content", content).ok();
ri.insert("summary", JsonValue::arr()).ok();
items.push(ri).ok();
}
let mut item = JsonValue::obj();
item.insert("role", "assistant".into()).ok();
let mut content = JsonValue::arr();
let text = join_text(&a.content);
if !text.is_empty() {
let mut part = JsonValue::obj();
part.insert("type", "output_text".into()).ok();
part.insert("text", text.into()).ok();
content.push(part).ok();
}
item.insert("content", content).ok();
items.push(item).ok();
// 工具调用作为独立的 function_call itemsfunction_call_output
// 的 call_id 必须对应它们)。
// Tool calls become standalone function_call items (the
// function_call_output call_ids must match them).
for tc in a.content.iter().filter_map(|c| c.as_tool_call()) {
let mut fc = JsonValue::obj();
fc.insert("type", "function_call".into()).ok();
fc.insert("call_id", tc.id.clone().into()).ok();
fc.insert("name", tc.name.clone().into()).ok();
fc.insert("arguments", focus_json::to_string(&tc.arguments).into())
.ok();
items.push(fc).ok();
}
}
Message::ToolResult(t) => {
let mut item = JsonValue::obj();
item.insert("type", "function_call_output".into()).ok();
item.insert("call_id", t.tool_call_id.clone().into()).ok();
item.insert("output", join_text(&t.content).into()).ok();
items.push(item).ok();
}
}
}
items
}
/// 核心工具定义 → Chat Completions toolsfunction 格式)。
/// Core tool definitions → Chat Completions tools (function format).
fn tools_to_chat(tools: &JsonValue) -> JsonValue {
match tools {
JsonValue::Arr(items) => {
let mut arr = JsonValue::arr();
for t in items {
let mut o = JsonValue::obj();
o.insert("type", "function".into()).ok();
let mut f = JsonValue::obj();
if let Some(n) = t.get_str("name") {
f.insert("name", n.into()).ok();
}
if let Some(d) = t.get_str("description") {
f.insert("description", d.into()).ok();
}
if let Some(p) = t.get("parameters") {
f.insert("parameters", p.clone()).ok();
}
o.insert("function", f).ok();
arr.push(o).ok();
}
arr
}
_ => JsonValue::arr(),
}
}
/// 核心工具定义 → Responses API tools。
/// Core tool definitions → Responses API tools.
fn tools_to_responses(tools: &JsonValue) -> JsonValue {
match tools {
JsonValue::Arr(items) => {
let mut arr = JsonValue::arr();
for t in items {
let mut o = JsonValue::obj();
o.insert("type", "function".into()).ok();
if let Some(n) = t.get_str("name") {
o.insert("name", n.into()).ok();
}
if let Some(d) = t.get_str("description") {
o.insert("description", d.into()).ok();
}
if let Some(p) = t.get("parameters") {
o.insert("parameters", p.clone()).ok();
}
arr.push(o).ok();
}
arr
}
_ => JsonValue::arr(),
}
}
fn has_tools(tools: &JsonValue) -> bool {
matches!(tools, JsonValue::Arr(items) if !items.is_empty())
}
/// 拼接文本块思考块不计入OpenAI 无法消费 reasoning 内容)。
/// Join text blocks; thinking is excluded (OpenAI cannot consume reasoning).
fn join_text(blocks: &[ContentBlock]) -> String {
blocks
.iter()
.filter_map(|c| match c {
ContentBlock::Text(t) => Some(t.text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
/// 把 Chat Completions 的 finish_reason 映射到核心类型。
/// Map a Chat Completions finish_reason onto the core type.
fn map_chat_finish(s: &str) -> StopReason {
match s {
"length" => StopReason::Length,
"tool_calls" | "function_call" => StopReason::ToolUse,
_ => StopReason::Stop, // stop / content_filter
}
}
/// 两种协议共享的回合累积状态(基于 [`ProviderEventReducer`])。
/// Per-turn accumulator shared by both protocols (built on
/// [`ProviderEventReducer`]).
struct OpenAiTurn {
reducer: ProviderEventReducer,
model: String,
protocol: OpenAiProtocol,
done: bool,
/// 已打开的工具调用provider 局部索引),用于去重。
/// Tool calls already opened (provider-local index), for dedup.
seen_calls: HashSet<usize>,
/// 输入 / 输出 token最后一块 usage 提供)。
/// Input / output tokens (from the final usage chunk).
input_tokens: u64,
output_tokens: u64,
cached_tokens: u64,
}
impl OpenAiTurn {
fn responses(model: &str) -> Self {
Self::new(model, OpenAiProtocol::Responses)
}
fn chat(model: &str) -> Self {
Self::new(model, OpenAiProtocol::ChatCompletions)
}
fn new(model: &str, protocol: OpenAiProtocol) -> Self {
Self {
reducer: ProviderEventReducer::new(model),
model: model.to_string(),
protocol,
done: false,
seen_calls: HashSet::new(),
input_tokens: 0,
output_tokens: 0,
cached_tokens: 0,
}
}
/// 收尾:关闭所有未闭合块并发送 Done。
/// Finalize: close all open blocks and send Done.
fn finalize(&mut self, tx: &SyncSender<StreamEvent>) -> Result<bool, String> {
for ev in self.reducer.finalize_events() {
if !common::send(tx, ev) {
return Ok(false);
}
}
let mut message = self
.reducer
.finish()
.ok_or_else(|| "reducer produced no final message".to_string())?;
message.usage = Usage {
input_tokens: self.input_tokens,
output_tokens: self.output_tokens,
cache_read_tokens: self.cached_tokens,
cache_write_tokens: 0,
};
self.done = true;
Ok(common::send(tx, StreamEvent::Done { message }))
}
/// 处理一个 SSE 事件。
/// Handle one SSE event.
fn handle_sse(&mut self, ev: &SseEvent, tx: &SyncSender<StreamEvent>) -> Result<bool, String> {
let data = ev.data.trim();
// Chat Completions 的流结束标记不是合法 JSON必须特判。
// Chat Completions' end-of-stream marker is not valid JSON; special-case it.
if data == "[DONE]" {
return self.finalize(tx);
}
let json: JsonValue = if data.is_empty() {
JsonValue::obj()
} else {
focus_json::parse(data).map_err(|e| format!("invalid SSE JSON: {}", e))?
};
match self.protocol {
OpenAiProtocol::ChatCompletions => self.handle_chat_event(&json, tx),
OpenAiProtocol::Responses => self.handle_responses_event(ev, &json, tx),
}
}
fn handle_chat_event(
&mut self,
json: &JsonValue,
tx: &SyncSender<StreamEvent>,
) -> Result<bool, String> {
// API 错误以 `{"error": {...}}` chunk 出现。
// API errors arrive as a `{"error": {...}}` chunk.
if let Some(err) = json.get("error") {
let ty = err.get_str("type").unwrap_or("api_error");
let message = err.get_str("message").unwrap_or("unknown error");
self.done = true;
return Ok(common::send(
tx,
StreamEvent::Error {
error: common::error_message(&self.model, &format!("{}: {}", ty, message)),
},
));
}
if let Some(choices) = json.get_arr("choices") {
for choice in choices {
if let Some(delta) = choice.get("delta") {
if let Some(content) = delta.get_str("content") {
if !content.is_empty() {
for ev in self.reducer.text_delta(content) {
if !common::send(tx, ev) {
return Ok(false);
}
}
}
}
// 推理内容 → 思考块。不同端点字段名不同DeepSeek 官方用
// reasoning_content部分兼容层用 reasoning_text / reasoning /
// thinking逐个尝试取第一个非空。
// Reasoning → thinking block. Field names differ across
// endpoints (DeepSeek official uses reasoning_content; some
// compatible layers use reasoning_text / reasoning /
// thinking); try each in order, keep the first non-empty.
const REASONING_FIELDS: [&str; 4] = [
"reasoning_content",
"reasoning_text",
"reasoning",
"thinking",
];
if let Some(reasoning) = REASONING_FIELDS
.iter()
.find_map(|k| delta.get_str(k).filter(|v| !v.is_empty()))
{
for ev in self.reducer.thinking_delta(reasoning) {
if !common::send(tx, ev) {
return Ok(false);
}
}
}
if let Some(tcs) = delta.get_arr("tool_calls") {
for tc in tcs {
let idx = tc.get_num("index").unwrap_or(0.0) as usize;
if let Some(f) = tc.get("function") {
if let Some(name) = f.get_str("name") {
if !name.is_empty() && self.seen_calls.insert(idx) {
for ev in self.reducer.tool_call_start(idx, name) {
if !common::send(tx, ev) {
return Ok(false);
}
}
}
}
if let Some(args) = f.get_str("arguments") {
if !args.is_empty() {
for ev in self.reducer.tool_call_delta(idx, args) {
if !common::send(tx, ev) {
return Ok(false);
}
}
}
}
}
}
}
}
if let Some(fr) = choice.get_str("finish_reason") {
if !fr.is_empty() {
self.reducer.set_stop(map_chat_finish(fr));
}
}
}
}
if let Some(usage) = json.get("usage") {
self.input_tokens = usage.get_num("prompt_tokens").unwrap_or(0.0) as u64;
self.output_tokens = usage.get_num("completion_tokens").unwrap_or(0.0) as u64;
if let Some(details) = usage.get("prompt_tokens_details") {
self.cached_tokens = details.get_num("cached_tokens").unwrap_or(0.0) as u64;
}
}
Ok(true)
}
fn handle_responses_event(
&mut self,
ev: &SseEvent,
json: &JsonValue,
tx: &SyncSender<StreamEvent>,
) -> Result<bool, String> {
match ev.event.as_str() {
"response.output_item.added" => {
if let Some(item) = json.get("item") {
match item.get_str("type").unwrap_or("") {
"message" => {
// 打开一个空的文本块,随后的 output_text.delta 填充。
// Open an empty text block; output_text.delta fills it.
for e in self.reducer.text_delta("") {
if !common::send(tx, e) {
return Ok(false);
}
}
}
"function_call" => {
let idx = json.get_num("output_index").unwrap_or(0.0) as usize;
let name = item.get_str("name").unwrap_or("").to_string();
if !name.is_empty() {
for e in self.reducer.tool_call_start(idx, &name) {
if !common::send(tx, e) {
return Ok(false);
}
}
}
}
"reasoning" => {
// 推理条目:打开思考块,随后的 reasoning_text.delta 填充。
// A reasoning item: open a thinking block; the
// following reasoning_text.delta events fill it.
for e in self.reducer.thinking_delta("") {
if !common::send(tx, e) {
return Ok(false);
}
}
}
_ => {}
}
}
Ok(true)
}
"response.output_text.delta" => {
if let Some(delta) = json.get_str("delta") {
if !delta.is_empty() {
for e in self.reducer.text_delta(delta) {
if !common::send(tx, e) {
return Ok(false);
}
}
}
}
Ok(true)
}
"response.function_call_arguments.delta" => {
if let Some(delta) = json.get_str("delta") {
if !delta.is_empty() {
let idx = json.get_num("output_index").unwrap_or(0.0) as usize;
for e in self.reducer.tool_call_delta(idx, delta) {
if !common::send(tx, e) {
return Ok(false);
}
}
}
}
Ok(true)
}
"response.reasoning_text.delta" => {
// 推理增量DeepSeek 等 thinking 模式)→ 思考块。
// Reasoning deltas (thinking mode, e.g. DeepSeek) → thinking.
if let Some(delta) = json.get_str("delta") {
if !delta.is_empty() {
for e in self.reducer.thinking_delta(delta) {
if !common::send(tx, e) {
return Ok(false);
}
}
}
}
Ok(true)
}
"response.completed" => {
if let Some(resp) = json.get("response") {
match resp.get_str("status").unwrap_or("completed") {
"completed" => self.reducer.set_stop(StopReason::Stop),
"incomplete" => {
let reason = resp
.get("incomplete_details")
.and_then(|d| d.get_str("reason"))
.unwrap_or("unknown");
self.reducer.set_stop(if reason == "max_output_tokens" {
StopReason::Length
} else {
StopReason::Stop
});
}
_ => {}
}
if let Some(usage) = resp.get("usage") {
self.input_tokens = usage.get_num("input_tokens").unwrap_or(0.0) as u64;
self.output_tokens = usage.get_num("output_tokens").unwrap_or(0.0) as u64;
if let Some(details) = usage.get("input_tokens_details") {
self.cached_tokens =
details.get_num("cached_tokens").unwrap_or(0.0) as u64;
}
}
}
self.finalize(tx)
}
"response.failed" | "error" => {
let code = json.get_str("code").unwrap_or("response_failed");
let message = json.get_str("message").unwrap_or("unknown error");
self.done = true;
Ok(common::send(
tx,
StreamEvent::Error {
error: common::error_message(
&self.model,
&format!("{}: {}", code, message),
),
},
))
}
_ => Ok(true), // created / in_progress / output_item.done 等忽略
}
}
}