mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
Phase 1:内核定义市面所有主流协议字段的**并集**作为唯一内部格式, 适配器按需取用;内核不再携带任何协议形状。 - internal/types/maxwire.go:UnifiedRequest(21 字段,v1 的 10)、 UnifiedResult(11,v1 的 8)、UnifiedDelta。覆盖 openai/anthropic/ gemini/ollama/deepseek 各自必需字段(system 顶层、top_k、stop_sequences、 thinking 预算、parallel_tool_calls、response_format、safety/citations…)。 - 只带**语义**不带**容器**:generationConfig/systemInstruction/options/ num_predict/maxOutputTokens 等协议专属形状一律不进统一格式,由适配器 自行装填。测试 TestNoProtocolContainerLeak 钉死这条。 - 修掉 v1 的私有字段泄漏:disable_thinking/extra_body 曾是内核私有字段, 却被序列化进交给适配器的 body,导致 10 个适配器各自写 `req.disable_thinking = nil` 清理。v2 用 thinking 语义字段替代, TestNoKernelPrivateFieldLeak 为回归。 Phase 0:建立对外输出的**字节级** golden 基线(.snapshots/), 因为现有 e2e 只有 strings.Contains 宽松断言,无法证明 Phase 2 把 OpenAI wire 移交给适配器 serializer 后输出字节未变。 - e2e/wire_golden_test.go:非流式/流式/生图三个 golden, 比对前 scrub 掉 id 与 created(每请求变化),其余逐字节比对。 - 反向验证:注入 `chat.completion.chunk.DRIFTED` 漂移后 golden 确实失败 (证明它不是永远通过的空壳),随后完整恢复。 - 稳定性:连跑 3 次全绿。 验证:go test -tags luajit ./internal/types/ ./e2e/ -run 'TestMax|TestNo|TestWireGolden' 全绿。
176 lines
8.3 KiB
Go
176 lines
8.3 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 []ToolDef `json:"tools,omitempty"`
|
||
ToolChoice any `json:"tool_choice,omitempty"`
|
||
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 是"要不要思考、思考预算多少"的语义;
|
||
// 适配器各自映射(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"`
|
||
}
|