Files
ModelRouter/internal/types/maxwire.go
JianFeeeee 3ad46ea23e feat(types): 统一线格式 v2 最大集合 + 对外输出 golden 基线
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' 全绿。
2026-10-11 08:24:58 +08:00

176 lines
8.3 KiB
Go
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.

// 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"`
}