mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
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 字节级未漂移。
201 lines
9.9 KiB
Go
201 lines
9.9 KiB
Go
// Package types — 统一线格式 v2(最大集合 / maximal set)
|
||
//
|
||
// 设计原则(feature/unified-wire-v2):
|
||
//
|
||
// 内核只调度,不知道任何具体协议。
|
||
// 适配器负责两个方向的协议转换:
|
||
// 入向 上游原生格式 → 本文件的统一格式
|
||
// 出向 本文件的统一格式 → 对外服务协议
|
||
//
|
||
// 因此本文件定义的是**市面所有主流 LLM 协议所需字段的并集**:
|
||
// OpenAI Chat Completions / Responses、Anthropic Messages、Gemini
|
||
// generateContent、Ollama、DeepSeek(reasoning_content)等。
|
||
// 适配器**按需取用**——每个具体协议都是这个超集的一个子集。
|
||
//
|
||
// 命名约定(重要):
|
||
// - 字段名取"该能力在业界最广为人知的拼写",便于适配器直接映射,
|
||
// 而不是为中立而中立地另造一套名字(那会让每个适配器都要维护一张
|
||
// 翻译表,反而更容易出错)。
|
||
// - 但**协议专属容器**(如 Gemini 的 generationConfig、Ollama 的 options)
|
||
// 不进入统一格式:它们是"上游协议的形状",属于适配器的内部事务。
|
||
// 统一格式只保留**语义**(temperature / top_k / max_tokens …),
|
||
// 由适配器决定装进哪个容器。
|
||
//
|
||
// 与 v1 的关键差异:
|
||
// - 内核私有字段不再泄漏给适配器(v1 的 disable_thinking / extra_body
|
||
// 会被序列化进请求体,10 个适配器被迫各自 = nil 清理)
|
||
// - 新增 Anthropic/Gemini/Ollama 需要的字段(system / top_k /
|
||
// stop_sequences / safety_settings / seed …),使适配器不必再从
|
||
// extra_body 这种旁路偷字段
|
||
package types
|
||
|
||
import "encoding/json"
|
||
|
||
// UnifiedRequest 是内核交给适配器的**完整请求**(最大集合)。
|
||
//
|
||
// 适配器只读它需要的字段;未用到的字段是"内核提供了但你这条协议不用",
|
||
// 不是"内核不知道该字段"。这个方向的信息损失为零。
|
||
type UnifiedRequest struct {
|
||
Model string `json:"model"`
|
||
Messages []ChatMessage `json:"messages"`
|
||
|
||
// ---- 采样参数(所有协议都有,名称归一) ----
|
||
Temperature *float64 `json:"temperature,omitempty"`
|
||
TopP *float64 `json:"top_p,omitempty"`
|
||
TopK int `json:"top_k,omitempty"` // Gemini/Ollama/Anthropic 有
|
||
MaxTokens int `json:"max_tokens,omitempty"` // Anthropic 必需
|
||
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` // OpenAI
|
||
PresencePenalty *float64 `json:"presence_penalty,omitempty"` // OpenAI
|
||
Seed *int64 `json:"seed,omitempty"` // OpenAI/Gemini
|
||
|
||
// ---- 停止条件 ----
|
||
Stop []string `json:"stop,omitempty"` // OpenAI
|
||
StopSequences []string `json:"stop_sequences,omitempty"` // Anthropic
|
||
|
||
// ---- 工具调用 ----
|
||
// Tools 保留 json.RawMessage(宽松的原始形状)而不是结构化的 []ToolDef,
|
||
// 原因是**渐进迁移**:内核现有的 12 个适配器与 gateway 都按 []interface{}
|
||
// /json.RawMessage 形状序列化请求到适配器。若此处改成 []ToolDef,则
|
||
// 所有适配器与调用点立即编译失败,必须同步改写 19 个非测试点 + 30 个测试。
|
||
// 而 Tools 的真实消费者是适配器(transform_request),内核本身从不解析它,
|
||
// 所以用原始形状不损失任何内核侧的统一性——它本就是"原样交给适配器的载荷"。
|
||
//
|
||
// ToolDef 类型保留在此包内供新适配器选用(maximal-set 的结构化语义),
|
||
// 但内核序列化路径暂不强推它。
|
||
Tools json.RawMessage `json:"tools,omitempty"`
|
||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||
|
||
// ParallelToolCalls *bool 允许显式 false(OpenAI 语义)。
|
||
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` // OpenAI
|
||
|
||
// ---- 结构化输出 ----
|
||
ResponseFormat *ResponseFormat `json:"response_format,omitempty"` // OpenAI
|
||
|
||
// ---- 系统提示 ----
|
||
// Anthropic 与 Gemini 都把 system 放在顶层而非 messages 里;
|
||
// OpenAI 系则放进 messages[role=system]。统一格式同时提供两条路:
|
||
// System 保存语义,Messages 里可能也有一条 role=system。
|
||
// 适配器按自己的协议取用(Anthropic 取 System 并递归剥离 messages 里的
|
||
// system;OpenAI 直接用 messages)。
|
||
System string `json:"system,omitempty"`
|
||
|
||
// ---- 推理/思考 ----
|
||
// Thinking 取代 v1 的内核私有字段 DisableThinking:那个字段曾被序列化进
|
||
// 交给适配器的 body,迫使 10 个适配器各自写 req.disable_thinking = nil。
|
||
// 这里它是双方都认可的对称语义(客户端要什么 / 上游怎么映射)。
|
||
// Thinking 是"要不要思考、思考预算多少"的语义;
|
||
// 适配器各自映射(Anthropic thinking.budget_tokens、Gemini
|
||
// thinkingConfig.thinkingBudget、DeepSeek extra_body.thinking.type)。
|
||
Thinking *ThinkingConfig `json:"thinking,omitempty"`
|
||
|
||
// ---- 多模态 ----
|
||
// 图片/音频等随 message content 的 parts 传递(见 ChatMessage.Content),
|
||
// 这里放协议级的生成参数(如 Gemini candidateCount、OpenAI n)。
|
||
GenerationCount int `json:"n,omitempty"`
|
||
|
||
// ---- 厂商扩展逃生舱 ----
|
||
// ExtraBody 是**明确标注的**厂商透传通道:客户端可以把它不认识的字段
|
||
// 塞进来,适配器自行决定是否读取。它不参与"内核私有字段"泄漏问题,
|
||
// 因为它的语义就是"客户端原样要求的额外字段"。
|
||
//
|
||
// 与 v1 的区别:v1 的 ExtraBody 会被 MarshalJSON 手工注入到 OpenAI 形状
|
||
// 的顶层,从而和内核私有字段混在一起;v2 里它是统一格式里的一个**明确定义
|
||
// 的字段**,内核不再需要 MarsalJSON 后门。
|
||
ExtraBody map[string]any `json:"extra_body,omitempty"`
|
||
|
||
// ---- 网关内部(不序列化,不交给适配器 body) ----
|
||
// ClientSession 只用于让适配器生成稳定的上游会话,不能出现在上游 body 里。
|
||
ClientSession string `json:"-"`
|
||
// Stream 由网关按客户端请求设置;适配器负责映射到上游的流式开关。
|
||
Stream bool `json:"stream,omitempty"`
|
||
}
|
||
|
||
// ThinkingConfig 是推理预算的语义表示(跨协议)。
|
||
//
|
||
// Enabled=false 表示客户端明确要求关闭思考。不同上游的关闭方式不一
|
||
// (DeepSeek 用 extra_body.thinking.type=disabled,Anthropic 直接不传
|
||
// thinking 字段,Gemini 用 thinkingBudget=0),映射由适配器负责。
|
||
type ThinkingConfig struct {
|
||
Enabled *bool `json:"enabled,omitempty"`
|
||
BudgetTokens int64 `json:"budget_tokens,omitempty"`
|
||
}
|
||
|
||
// ToolDef 是工具声明的统一形状(OpenAI 的 {type,function} 外壳被摊平)。
|
||
type ToolDef struct {
|
||
Name string `json:"name"`
|
||
Description string `json:"description,omitempty"`
|
||
// Parameters 是 JSON Schema,原样透传。
|
||
Parameters json.RawMessage `json:"parameters,omitempty"`
|
||
// Type 保留协议侧的 kind(OpenAI 恒为 "function";其它协议可能为空)。
|
||
Type string `json:"type,omitempty"`
|
||
}
|
||
|
||
// ResponseFormat 是结构化输出的统一形状。
|
||
type ResponseFormat struct {
|
||
Type string `json:"type"` // text | json_object | json_schema
|
||
JSONSchema json.RawMessage `json:"json_schema,omitempty"`
|
||
}
|
||
|
||
// UnifiedResult 是适配器从上游响应转换出来的**统一结果**(最大集合)。
|
||
//
|
||
// 这是 v1 的 UnifiedResponse 的超集:v1 只有 content/finish_reason/
|
||
// token_usage/tool_calls;v2 补齐各协议会产出的其它语义。
|
||
type UnifiedResult struct {
|
||
Model string `json:"model,omitempty"`
|
||
Content string `json:"content"`
|
||
Thinking string `json:"thinking,omitempty"`
|
||
|
||
// FinishReason 是**归一后**的结束原因:stop / length / tool_calls /
|
||
// content_filter / 空。适配器负责把上游措辞映射到这套词表
|
||
// (如 Anthropic 的 end_turn→stop、max_tokens→length、
|
||
// tool_use→tool_calls;Gemini 的 STOP→stop、MAX_TOKENS→length)。
|
||
FinishReason string `json:"finish_reason,omitempty"`
|
||
|
||
TokenUsage TokenUsage `json:"token_usage"`
|
||
|
||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||
|
||
// ---- 其它协议可能产出的语义 ----
|
||
// Citations 是 Anthropic 的引用块、Gemini 的 grounding。
|
||
Citations []json.RawMessage `json:"citations,omitempty"`
|
||
// Refusal 是 OpenAI 的 refusal 字段(内容被安全策略拒绝)。
|
||
Refusal string `json:"refusal,omitempty"`
|
||
// SafetyRatings 是 Gemini 的安全评分。
|
||
SafetyRatings []json.RawMessage `json:"safety_ratings,omitempty"`
|
||
|
||
// ImageData 供生图适配器使用。
|
||
ImageData []ImageData `json:"image_data,omitempty"`
|
||
|
||
// Cost 是上游自报的费用(小数**字符串**),原样透传。
|
||
// 刻意不在内核里解析或累加:它是上游事实,且只有部分上游提供。
|
||
Cost string `json:"cost,omitempty"`
|
||
}
|
||
|
||
// UnifiedDelta 是流式分块的统一形状(最大集合)。
|
||
type UnifiedDelta struct {
|
||
Content string `json:"content"`
|
||
Thinking string `json:"thinking,omitempty"`
|
||
Done bool `json:"done"`
|
||
// FinishReason 只在终止块上有值。
|
||
FinishReason string `json:"finish_reason,omitempty"`
|
||
// ToolCalls 是流式工具调用的增量片段(原始 JSON,由适配器决定形状)。
|
||
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
||
// Usage 出现在带用量的块上(通常是终止块)。
|
||
Usage *TokenUsage `json:"usage,omitempty"`
|
||
// Citations 流式引用块。
|
||
Citations []json.RawMessage `json:"citations,omitempty"`
|
||
// Cost 上游自报费用。
|
||
Cost string `json:"cost,omitempty"`
|
||
}
|
||
|
||
// ChatRequest is the historical name for the in-bound request type.
|
||
//
|
||
// Deprecated: it is an alias of UnifiedRequest. The rename happened because the
|
||
// kernel's internal format stopped being "whatever the client sent" once the
|
||
// maximal set landed — it is now the maximal set, and the client's own protocol
|
||
// shape only survives until the adapter sees it.
|
||
//
|
||
// Kept as an alias (not a distinct type) so the 49 existing references keep
|
||
// compiling while call sites migrate deliberately rather than in one sweep.
|
||
type ChatRequest = UnifiedRequest
|