修复: 命名空间上限与网络域解析兜底
- 命名空间超容单条拒绝,上限不变式恢复 - usage 部分字段缺失致整帧内容蒸发,补缺省兜底 - tool_call 缺 id 致整批丢弃,对齐流式 fallback 口径 - 视觉模型判定改词素尾,deepseek-v3 不再误判 - 并行分支 panic 任务对账补齐为失败结果 - plan 执行器死代码清理,实况注释校正
This commit is contained in:
@@ -102,10 +102,14 @@ pub(crate) struct OpenAiMessageResp {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct OpenAiToolCallResp {
|
||||
/// 缺省容错:个别兼容端点同步响应省略 id → 缺省空串,由 parse_tool_calls 的
|
||||
/// tool_call_id_or_fallback 生成唯一 fallback。此前必填,单条 tool_call 缺 id 令
|
||||
/// 整个响应 serde 失败 → complete 报"响应解析失败"整批丢弃(流式路径早已容错)。
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
// 反序列化 #[serde(rename="type")] 字段,保留以对齐 OpenAI 响应结构,标注意图消除 dead_code warning
|
||||
#[allow(dead_code)]
|
||||
#[serde(rename = "type")]
|
||||
#[serde(rename = "type", default)]
|
||||
pub call_type: String,
|
||||
pub function: OpenAiFunctionResp,
|
||||
}
|
||||
@@ -113,6 +117,9 @@ pub(crate) struct OpenAiToolCallResp {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct OpenAiFunctionResp {
|
||||
pub name: String,
|
||||
/// 缺省容错:arguments 缺失 → 空串(对齐 anthropic 同步 tool_use input 缺失 args="" 口径),
|
||||
/// 防单条缺字段令整个响应解析失败;name 保持必填(无名 tool_call 无法派发,无安全缺省)。
|
||||
#[serde(default)]
|
||||
pub arguments: String,
|
||||
}
|
||||
|
||||
@@ -130,8 +137,13 @@ pub(crate) struct PromptTokensDetails {
|
||||
/// 嵌套 cached_tokens 回填,消费方一律读归一后的扁平字段。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct OpenAiUsageRaw {
|
||||
/// 三个计数缺省 0:部分兼容网关 usage 只报部分字段(如仅 total_tokens),必填会让
|
||||
/// 整帧 serde 失败走空 chunk 分支 — 同帧 content 一并蒸发、usage 归零无痕。
|
||||
#[serde(default)]
|
||||
pub prompt_tokens: u32,
|
||||
#[serde(default)]
|
||||
pub completion_tokens: u32,
|
||||
#[serde(default)]
|
||||
pub total_tokens: u32,
|
||||
/// DeepSeek 扩展:缓存命中 token(低价,deepseek-chat/reasoner)。无此字段 → default 0。
|
||||
#[serde(default)]
|
||||
@@ -288,6 +300,14 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
||||
}
|
||||
// 提取 usage(带 include_usage 时末段 chunk 携带,覆盖累积)
|
||||
if let Some(u) = chunk.usage {
|
||||
// 缺省计数的留痕:两个分项均 0 基本可断定 provider 上报不完整(真实完成
|
||||
// 必有 completion 计数),非流路径下游拿到零 usage 无从察觉。
|
||||
if u.prompt_tokens == 0 && u.completion_tokens == 0 {
|
||||
tracing::warn!(
|
||||
total = u.total_tokens,
|
||||
"[OpenAI] usage 缺 prompt/completion 分项(按 0 兜底),provider 上报不完整"
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
prompt = u.prompt_tokens,
|
||||
completion = u.completion_tokens,
|
||||
@@ -316,6 +336,21 @@ pub(crate) fn apply_openai_sse(data: &str, usage_accum: &mut Option<TokenUsage>)
|
||||
);
|
||||
|
||||
let tool_calls = choice.delta.tool_calls.map(|tcs| {
|
||||
// 缺 index 的增量缺省 0(单工具调用语义等价);但同帧多 call 且 index
|
||||
// 冲突时缺省会互相踩(下游按 index 累积 → 参数拼错位),warn 留痕定位
|
||||
// provider 兼容缺陷,不做静默改写。
|
||||
if tcs.len() > 1 {
|
||||
let mut idxs: Vec<u32> = tcs.iter().map(|tc| tc.index).collect();
|
||||
idxs.sort_unstable();
|
||||
idxs.dedup();
|
||||
if idxs.len() < tcs.len() {
|
||||
tracing::warn!(
|
||||
calls = tcs.len(),
|
||||
unique = idxs.len(),
|
||||
"[OpenAI] 同帧多 tool_call 增量 index 冲突(缺省 0 互踩),按 index 累积将错位"
|
||||
);
|
||||
}
|
||||
}
|
||||
tcs.into_iter()
|
||||
.map(|tc| {
|
||||
// CR-空 id:流式 chunk 的 id 可能为 Some("")(SenseNova 兼容缺陷)。
|
||||
@@ -423,6 +458,35 @@ mod tests {
|
||||
assert_eq!(usage.prompt_cache_miss_tokens, 36);
|
||||
}
|
||||
|
||||
/// 部分兼容网关 usage 只报部分字段:缺省 0 入累加器,同帧 content 不再因整帧
|
||||
/// serde 失败被吞(旧行为:必填字段缺失 → 空 chunk → 文本蒸发 + usage 归零无痕)
|
||||
#[test]
|
||||
fn sse_partial_usage_frame_parses_with_defaults() {
|
||||
let mut accum = None;
|
||||
let data = r#"{"choices":[{"delta":{"content":"hi"}}],"usage":{"total_tokens":10}}"#;
|
||||
let c = apply_openai_sse(data, &mut accum);
|
||||
assert_eq!(c.delta, "hi", "usage 缺分项不应令整帧解析失败丢 content");
|
||||
let u = accum.expect("部分 usage 应写入累加器");
|
||||
assert_eq!(u.total_tokens, 10);
|
||||
assert_eq!(u.prompt_tokens, 0, "缺省分项按 0 兜底");
|
||||
assert_eq!(u.completion_tokens, 0);
|
||||
}
|
||||
|
||||
/// 同帧多 tool_call 增量缺 index:缺省 0 触发 index 冲突路径(仅 warn 留痕,不改写
|
||||
/// 缺省策略);解析本身不失败,增量仍逐条带出。
|
||||
#[test]
|
||||
fn sse_tool_calls_missing_index_in_multi_call_frame_still_parse() {
|
||||
let mut accum = None;
|
||||
let data = r#"{"choices":[{"delta":{"tool_calls":[
|
||||
{"id":"call_a","function":{"name":"ta","arguments":"{"}},
|
||||
{"id":"call_b","function":{"name":"tb","arguments":"}"}}
|
||||
]}}]}"#;
|
||||
let c = apply_openai_sse(data, &mut accum);
|
||||
let tcs = c.tool_calls.expect("缺 index 的多 call 帧应可解析");
|
||||
assert_eq!(tcs.len(), 2);
|
||||
assert!(tcs.iter().all(|tc| tc.index == 0), "缺 index 缺省 0");
|
||||
}
|
||||
|
||||
/// content_filter 属终态:敏感内容拦截的流部分 provider 不发 [DONE] 帧,
|
||||
/// 必须靠 finish_reason 收敛,否则悬挂;未列入终态的 finish_reason 保持非终态
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user