JJianFeeeeefeat(types): 统一格式落地为内核实际使用的类型 —— 私有字段泄漏归零
| Files | Last commit | Last update |
|---|---|---|
feat(types): 统一格式落地为内核实际使用的类型 —— 私有字段泄漏归零 Phase 2 收尾(入向)。ChatRequest 从"客户端发什么就是什么"改为 UnifiedRequest(最大集合)的别名,内核真正用上了那个超集类型。 改掉的具体缺陷: 1. 私有字段泄漏归零(v1 的核心问题) v1 的 disable_thinking 是内核私有开关,却被序列化进交给适配器的 body, 迫使 10 个适配器各自写 req.disable_thinking = nil 来清理。 现在它是中立的 thinking 字段({enabled, budget_tokens}),客户端与 适配器都认这个词: - gateway/chat.go:thinkingFromClient() 把 disable_thinking 翻译成 thinking.enabled=false;冲突时中立字段优先(它能带预算,布尔不能) - deepseek.lua:thinking.enabled=false → extra_body.thinking.type=disabled - anthropic.lua:thinking.enabled=true + budget → 原生 {type,budget_tokens} (enabled=false 时不传该字段——Anthropic 没有 type=disabled 这个写法) - 其余 7 个适配器:把"清理内核私有字段"改成"清理中立字段" 2. Tools/ToolChoice 改 json.RawMessage 原为 []interface{}(OpenAI 原始形状 = 内核在假设某个协议)。 内核从不解释工具 schema——它原样交给适配器,所以原始形状不损失任何 内核侧统一性。ToolDef 结构化类型保留在包内供新适配器选用。 3. 删除 MarshalJSON 后门 v1 手工 splice ExtraBody 进 OpenAI 形状的 body;现在 ExtraBody 是 UnifiedRequest 上一个正正经经声明的字段。 渐进迁移:ChatRequest 保留为别名(不是新类型),49 处引用零改动编译, 语义变更不被机械改名淹没。 测试:新增 thinking_test.go(翻译链 4 例 + 泄漏回归 + tools 透传); vm_test.go 两处迁移到中立语义。golden 字节级未漂移。 | 28 days ago | |
feat(types): 统一格式落地为内核实际使用的类型 —— 私有字段泄漏归零 Phase 2 收尾(入向)。ChatRequest 从"客户端发什么就是什么"改为 UnifiedRequest(最大集合)的别名,内核真正用上了那个超集类型。 改掉的具体缺陷: 1. 私有字段泄漏归零(v1 的核心问题) v1 的 disable_thinking 是内核私有开关,却被序列化进交给适配器的 body, 迫使 10 个适配器各自写 req.disable_thinking = nil 来清理。 现在它是中立的 thinking 字段({enabled, budget_tokens}),客户端与 适配器都认这个词: - gateway/chat.go:thinkingFromClient() 把 disable_thinking 翻译成 thinking.enabled=false;冲突时中立字段优先(它能带预算,布尔不能) - deepseek.lua:thinking.enabled=false → extra_body.thinking.type=disabled - anthropic.lua:thinking.enabled=true + budget → 原生 {type,budget_tokens} (enabled=false 时不传该字段——Anthropic 没有 type=disabled 这个写法) - 其余 7 个适配器:把"清理内核私有字段"改成"清理中立字段" 2. Tools/ToolChoice 改 json.RawMessage 原为 []interface{}(OpenAI 原始形状 = 内核在假设某个协议)。 内核从不解释工具 schema——它原样交给适配器,所以原始形状不损失任何 内核侧统一性。ToolDef 结构化类型保留在包内供新适配器选用。 3. 删除 MarshalJSON 后门 v1 手工 splice ExtraBody 进 OpenAI 形状的 body;现在 ExtraBody 是 UnifiedRequest 上一个正正经经声明的字段。 渐进迁移:ChatRequest 保留为别名(不是新类型),49 处引用零改动编译, 语义变更不被机械改名淹没。 测试:新增 thinking_test.go(翻译链 4 例 + 泄漏回归 + tools 透传); vm_test.go 两处迁移到中立语义。golden 字节级未漂移。 | 28 days ago | |
feat(types): 统一格式落地为内核实际使用的类型 —— 私有字段泄漏归零 Phase 2 收尾(入向)。ChatRequest 从"客户端发什么就是什么"改为 UnifiedRequest(最大集合)的别名,内核真正用上了那个超集类型。 改掉的具体缺陷: 1. 私有字段泄漏归零(v1 的核心问题) v1 的 disable_thinking 是内核私有开关,却被序列化进交给适配器的 body, 迫使 10 个适配器各自写 req.disable_thinking = nil 来清理。 现在它是中立的 thinking 字段({enabled, budget_tokens}),客户端与 适配器都认这个词: - gateway/chat.go:thinkingFromClient() 把 disable_thinking 翻译成 thinking.enabled=false;冲突时中立字段优先(它能带预算,布尔不能) - deepseek.lua:thinking.enabled=false → extra_body.thinking.type=disabled - anthropic.lua:thinking.enabled=true + budget → 原生 {type,budget_tokens} (enabled=false 时不传该字段——Anthropic 没有 type=disabled 这个写法) - 其余 7 个适配器:把"清理内核私有字段"改成"清理中立字段" 2. Tools/ToolChoice 改 json.RawMessage 原为 []interface{}(OpenAI 原始形状 = 内核在假设某个协议)。 内核从不解释工具 schema——它原样交给适配器,所以原始形状不损失任何 内核侧统一性。ToolDef 结构化类型保留在包内供新适配器选用。 3. 删除 MarshalJSON 后门 v1 手工 splice ExtraBody 进 OpenAI 形状的 body;现在 ExtraBody 是 UnifiedRequest 上一个正正经经声明的字段。 渐进迁移:ChatRequest 保留为别名(不是新类型),49 处引用零改动编译, 语义变更不被机械改名淹没。 测试:新增 thinking_test.go(翻译链 4 例 + 泄漏回归 + tools 透传); vm_test.go 两处迁移到中立语义。golden 字节级未漂移。 | 28 days ago | |
feat(opencode): 透传 completion_tokens_details.reasoning_tokens 与上游 cost 回答「opencodego 的用量与费用透传呢」时逐字段核对上游产出,发现 usage 漏了 一项、费用整项丢失。 ## 上游实际发什么(实测 opencode.ai/zen/go/v1) { "choices": [...], "usage": { "prompt_tokens": 37, "completion_tokens": 40, "total_tokens": 77, "prompt_cache_hit_tokens": 0, "prompt_cache_miss_tokens": 37, "prompt_tokens_details": {"cached_tokens": 0}, "completion_tokens_details": {"reasoning_tokens": 40} }, "cost": "0" } cost 在**顶层**且是**字符串**。流式时还会单独发一帧: {"choices":[],"cost":"0"} ## 此前丢了两样 1. completion_tokens_details.reasoning_tokens —— 输出里有多少是思考 token。 没有它,客户端无法判断 completion_tokens 里多少是可见回答、多少是思考, 而两者都按输出计费。 2. cost —— 唯一的费用信号,网关整个丢弃。Go 订阅是包月制恒为 "0", 但 Zen 按量付费模型(以及未来的其它源)有信息量。 顺带修掉一处流式/非流式不一致:命中缓存时上游同时给 prompt_tokens_details.cached_tokens 和独立的 hit/miss,流式路径写成了 elseif, 只留 details,与非流式产出不同(只认独立字段的老客户端会看不到缓存)。 ## 实现 - types.TokenUsage += CompletionTokensDetails;UnifiedResponse / UnifiedChunk += Cost - opencodego/opencodezen 适配器映射两个字段;空 choices 帧改成 usage 与 cost 都可带(早退只带 usage 会把同帧的 cost 丢干净 —— 新测试先抓到的就是这个) - Gateway ChatCompletion / ChatChunk += cost,随终帧发(对齐上游的 {"choices":[],"cost":"0"} 形态) - Go 兜底 standardSSEChunk 同步支持(openai 系适配器不再漏 reasoning_tokens; 纯 cost 帧不再被整体丢弃),新增 rawCostString 兼容字符串/数字两种形态 费用只做**搬运**:不解析、不换算、不汇总 —— 它是上游事实,且只有部分上游提供。 ## 验证 经网关实测 gozen:deepseek-v4.1-flash,流式与非流式产出逐字段一致: prompt_tokens_details.cached_tokens=6784 prompt_cache_hit_tokens=6784 / miss=148 completion_tokens_details.reasoning_tokens=16 cost="0" 测试:TestOpenCodeCostAndReasoningPassthrough(含「无数据不得凭空造字段」反例)、 TestOpenCodeStreamCacheFieldsMatchNonStream、TestTokenUsageMarshalsCompletionTokensDetails。 (cherry picked from commit c744ee151e6bd7e43f279120d32813d317348cc2) | 1 month ago |