package wire import ( "encoding/json" "testing" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/flyto" ) // openaiReqProbe 是 OpenAICompatClient.buildRequest 输出的 JSON 解析探针. // 仅声明本测试关心的 sampling 字段; omitempty 缺失时反序列化为 nil. // // openaiReqProbe parses the JSON emitted by OpenAICompatClient.buildRequest // for inspection. Only the sampling fields under test are declared; missing // omitempty fields unmarshal to nil. type openaiReqProbe struct { Temperature *float64 `json:"temperature"` TopP *float64 `json:"top_p"` TopK *int `json:"top_k"` MinP *float64 `json:"min_p"` ReasoningSplit *bool `json:"reasoning_split"` } func parseOpenAIReq(t *testing.T, data []byte) openaiReqProbe { t.Helper() var p openaiReqProbe if err := json.Unmarshal(data, &p); err != nil { t.Fatalf("unmarshal openai request: %v", err) } return p } // TestOpenAIBuildRequest_TemperatureTopP_Forwarded 锁 OpenAI 兼容路径 // (4 provider: openai/ollama/lmstudio/openrouter) 把 Request.Temperature/ // TopP 透传到 wire JSON 的 top-level temperature / top_p 字段. // // Locks the OpenAI-compat path forwarding Request.Temperature/TopP to the // top-level wire JSON fields. func TestOpenAIBuildRequest_TemperatureTopP_Forwarded(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") req := &StreamRequest{ Model: "gpt-4o", MaxTokens: 100, Temperature: flyto.Float(0.7), TopP: flyto.Float(0.9), } got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } p := parseOpenAIReq(t, got) if p.Temperature == nil || *p.Temperature != 0.7 { t.Errorf("temperature = %v, want 0.7", p.Temperature) } if p.TopP == nil || *p.TopP != 0.9 { t.Errorf("top_p = %v, want 0.9", p.TopP) } } // TestOpenAIBuildRequest_NilSampling_OmittedOnWire 锁 nil 时 wire 不传字段 // (passthrough policy 的默认行为: 上游用 provider 默认值). // // Locks the nil-omits-on-wire passthrough policy: upstream falls back to // its provider/model default when the caller doesn't set a sampling knob. func TestOpenAIBuildRequest_NilSampling_OmittedOnWire(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") req := &StreamRequest{Model: "gpt-4o", MaxTokens: 100} got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } // 解析 raw map 直接断言键缺失 (omitempty 验证). var raw map[string]json.RawMessage if err := json.Unmarshal(got, &raw); err != nil { t.Fatalf("unmarshal raw: %v", err) } if _, ok := raw["temperature"]; ok { t.Errorf("temperature key present on wire when Request.Temperature == nil") } if _, ok := raw["top_p"]; ok { t.Errorf("top_p key present on wire when Request.TopP == nil") } } // TestOpenAIBuildRequest_ZeroTemperature_TransmittedExplicitly 锁 // "0 是合法的 deterministic 值" 语义: 调用方主动传 *0 时 wire 上必须有 // "temperature":0, 不能被 omitempty 吞掉. Go 的 *float64 + omitempty 在 // json 编码时只有 nil 才省略, 这条 test 防止未来误改成 float64+sentinel. // // Locks the "0 is a valid deterministic value" semantic. When the caller // explicitly sets *0, the wire must carry "temperature":0; omitempty only // triggers on nil. Guards against a future regression to float64+sentinel. func TestOpenAIBuildRequest_ZeroTemperature_TransmittedExplicitly(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") req := &StreamRequest{ Model: "gpt-4o", MaxTokens: 100, Temperature: flyto.Float(0), } got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } p := parseOpenAIReq(t, got) if p.Temperature == nil { t.Fatal("temperature missing on wire when *Temperature = 0 (omitempty must not eat explicit zero)") } if *p.Temperature != 0 { t.Errorf("temperature = %v, want 0", *p.Temperature) } } // TestOpenAIBuildRequest_TopKMinP_Forwarded locks the openai-compat path // forwarding Request.TopK/MinP to the top-level wire JSON top_k / min_p // fields (the non-standard sampling extras consumed by ds4 / local vLLM). // // 锁 openai-compat 路径把 Request.TopK/MinP 透传到 wire JSON 顶层 top_k / // min_p 字段 (ds4 / 本地 vLLM 消费的非标准采样扩展). func TestOpenAIBuildRequest_TopKMinP_Forwarded(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") k := 40 req := &StreamRequest{ Model: "deepseek-v4-flash", MaxTokens: 100, TopK: &k, MinP: flyto.Float(0.05), } got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } p := parseOpenAIReq(t, got) if p.TopK == nil || *p.TopK != 40 { t.Errorf("top_k = %v, want 40", p.TopK) } if p.MinP == nil || *p.MinP != 0.05 { t.Errorf("min_p = %v, want 0.05", p.MinP) } } // TestOpenAIBuildRequest_NilTopKMinP_OmittedOnWire locks the nil-omits // policy for the extras: official OpenAI (which has no top_k / min_p) must // never receive them, which holds because callers leave them nil there. // // 锁 extras 的 nil 省略策略: 官方 OpenAI (没有 top_k / min_p) 永不收到 -- // 因为调用方对它留 nil. func TestOpenAIBuildRequest_NilTopKMinP_OmittedOnWire(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") req := &StreamRequest{Model: "gpt-4o", MaxTokens: 100} got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } var raw map[string]json.RawMessage if err := json.Unmarshal(got, &raw); err != nil { t.Fatalf("unmarshal raw: %v", err) } if _, ok := raw["top_k"]; ok { t.Errorf("top_k key present on wire when Request.TopK == nil") } if _, ok := raw["min_p"]; ok { t.Errorf("min_p key present on wire when Request.MinP == nil") } } // TestOpenAIBuildRequest_ZeroMinP_TransmittedExplicitly locks "0 is a // meaningful value" for min_p (explicit "no floor"): an explicit *0 must // reach the wire, not be eaten by omitempty (same guard as temperature). // // 锁 min_p 的 "0 是有意义值" (显式 "无地板"): 显式 *0 必须到 wire, 不能被 // omitempty 吞 (同 temperature 的守卫). func TestOpenAIBuildRequest_ZeroMinP_TransmittedExplicitly(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") req := &StreamRequest{Model: "deepseek-v4-flash", MaxTokens: 100, MinP: flyto.Float(0)} got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } p := parseOpenAIReq(t, got) if p.MinP == nil { t.Fatal("min_p missing on wire when *MinP = 0 (omitempty must not eat explicit zero)") } if *p.MinP != 0 { t.Errorf("min_p = %v, want 0", *p.MinP) } } // TestOpenAIBuildRequest_ReasoningSplit_Forwarded locks the openai-compat // path forwarding Request.ReasoningSplit to the top-level wire JSON // reasoning_split field. The minimax provider sets it true for ModeOpenAI so // MiniMax M3 / M2.7 emit reasoning on reasoning_content / reasoning_details // (routed to ThinkingDeltaEvent) instead of inlining into content. // // 锁 openai-compat 路径把 Request.ReasoningSplit 透传到 wire JSON 顶层 // reasoning_split 字段. minimax provider 对 ModeOpenAI 设 true, 让 MiniMax // M3 / M2.7 把 reasoning 走 reasoning_content / reasoning_details (路由成 // ThinkingDeltaEvent) 而非内联 进 content. func TestOpenAIBuildRequest_ReasoningSplit_Forwarded(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") split := true req := &StreamRequest{ Model: "MiniMax-M3", MaxTokens: 100, ReasoningSplit: &split, } got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } p := parseOpenAIReq(t, got) if p.ReasoningSplit == nil || !*p.ReasoningSplit { t.Errorf("reasoning_split = %v, want true", p.ReasoningSplit) } } // TestOpenAIBuildRequest_NilReasoningSplit_OmittedOnWire locks the nil-omits // policy: a non-MiniMax caller (which leaves ReasoningSplit nil) must never // send the field, so the standard openai-compat request shape is unchanged. // // 锁 nil 省略策略: 非 MiniMax 调用方 (留 ReasoningSplit nil) 永不发该字段, // 标准 openai-compat 请求形态不变. func TestOpenAIBuildRequest_NilReasoningSplit_OmittedOnWire(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") req := &StreamRequest{Model: "gpt-4o", MaxTokens: 100} got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } var raw map[string]json.RawMessage if err := json.Unmarshal(got, &raw); err != nil { t.Fatalf("unmarshal raw: %v", err) } if _, ok := raw["reasoning_split"]; ok { t.Errorf("reasoning_split key present on wire when Request.ReasoningSplit == nil") } } // TestGeminiBuildRequest_TopKMinP_NotMapped locks the documented tracked // gap: Gemini's buildRequest does NOT map TopK/MinP, so even when set on // the StreamRequest they never appear in generationConfig. This pins the // "silent-drop is documented, not accidental" contract -- if a future // commit wires Gemini, update flyto.Request godoc + this test together. // // 锁 documented tracked gap: Gemini 的 buildRequest **不**映射 TopK/MinP, // 故即便 StreamRequest 设了它们也绝不出现在 generationConfig. 钉死 "静默丢是 // documented 而非意外" 契约 -- 将来 commit 若接 Gemini, 同步改 flyto.Request // godoc + 本测试. func TestGeminiBuildRequest_TopKMinP_NotMapped(t *testing.T) { c := NewGeminiClient("fake-key", "https://fake.api") k := 40 req := &StreamRequest{Model: "gemini-2.0-flash", MaxTokens: 1024, TopK: &k, MinP: flyto.Float(0.05)} got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } var raw struct { GenerationConfig map[string]json.RawMessage `json:"generationConfig"` } if err := json.Unmarshal(got, &raw); err != nil { t.Fatalf("unmarshal raw: %v", err) } if _, ok := raw.GenerationConfig["top_k"]; ok { t.Errorf("top_k unexpectedly mapped into Gemini generationConfig (tracked gap says not-yet-mapped)") } if _, ok := raw.GenerationConfig["topK"]; ok { t.Errorf("topK unexpectedly mapped into Gemini generationConfig (tracked gap says not-yet-mapped)") } if _, ok := raw.GenerationConfig["min_p"]; ok { t.Errorf("min_p unexpectedly mapped into Gemini generationConfig (Gemini has no min_p)") } } // geminiSamplingProbe 解析 Gemini buildRequest 输出的 generationConfig // 只关注 sampling 字段. // // geminiSamplingProbe parses the generationConfig from Gemini's // buildRequest output, restricted to sampling fields under test. type geminiSamplingProbe struct { GenerationConfig struct { Temperature *float64 `json:"temperature"` TopP *float64 `json:"topP"` } `json:"generationConfig"` } // TestGeminiBuildRequest_TemperatureTopP_Forwarded 锁 Gemini 路径把 // Request.Temperature/TopP 嵌入 generationConfig.temperature/topP // (而非 top-level, Gemini 特有 wire format). // // Locks the Gemini path nesting Request.Temperature/TopP under // generationConfig.temperature/topP, matching Gemini's wire format // (not top-level like OpenAI-compat). func TestGeminiBuildRequest_TemperatureTopP_Forwarded(t *testing.T) { c := NewGeminiClient("fake-key", "https://fake.api") req := &StreamRequest{ Model: "gemini-2.0-flash", MaxTokens: 1024, Temperature: flyto.Float(0.5), TopP: flyto.Float(0.85), } got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } var p geminiSamplingProbe if err := json.Unmarshal(got, &p); err != nil { t.Fatalf("unmarshal gemini request: %v", err) } if p.GenerationConfig.Temperature == nil || *p.GenerationConfig.Temperature != 0.5 { t.Errorf("generationConfig.temperature = %v, want 0.5", p.GenerationConfig.Temperature) } if p.GenerationConfig.TopP == nil || *p.GenerationConfig.TopP != 0.85 { t.Errorf("generationConfig.topP = %v, want 0.85", p.GenerationConfig.TopP) } } // TestGeminiBuildRequest_NilSampling_OmittedOnWire 锁 Gemini 路径 nil // 时 generationConfig 内不出现 temperature/topP 键. // // Locks the Gemini path omitting temperature/topP keys when callers // don't set them. func TestGeminiBuildRequest_NilSampling_OmittedOnWire(t *testing.T) { c := NewGeminiClient("fake-key", "https://fake.api") req := &StreamRequest{Model: "gemini-2.0-flash", MaxTokens: 1024} got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } var raw struct { GenerationConfig map[string]json.RawMessage `json:"generationConfig"` } if err := json.Unmarshal(got, &raw); err != nil { t.Fatalf("unmarshal raw: %v", err) } if _, ok := raw.GenerationConfig["temperature"]; ok { t.Errorf("temperature present in generationConfig when Request.Temperature == nil") } if _, ok := raw.GenerationConfig["topP"]; ok { t.Errorf("topP present in generationConfig when Request.TopP == nil") } } // thinkingProbe parses the DeepSeek V4 thinking-mode fields from the // OpenAI-compat buildRequest output (top-level thinking object + // reasoning_effort string). // // thinkingProbe 解析 OpenAI-compat buildRequest 输出的 DeepSeek V4 思考模式 // 字段 (顶级 thinking 对象 + reasoning_effort 字符串). type thinkingProbe struct { Thinking *struct { Type string `json:"type"` } `json:"thinking"` ReasoningEffort *string `json:"reasoning_effort"` } // TestOpenAIBuildRequest_ThinkingDisabled_EffortHigh_Forwarded is the // advisor-requested marshal guard: it pins the EXACT DeepSeek V4 thinking-mode // JSON shape (per api-docs.deepseek.com/guides/thinking_mode) so a future // hand-slip on a field name or nesting is caught by `go test`, not by the // operator's first staging run 400-ing. ThinkingType "disabled" must serialize // to top-level `{"thinking":{"type":"disabled"}}` and ReasoningEffort "high" to // top-level `"reasoning_effort":"high"`. // // advisor 要求的 marshal 守卫: 钉死 DeepSeek V4 思考模式精确 JSON 形态 (官方 // /guides/thinking_mode), 让未来手滑改字段名/嵌套被 go test 抓住而非操作员第一次 // staging run 400. ThinkingType "disabled" 须序列化成顶级 // `{"thinking":{"type":"disabled"}}`, ReasoningEffort "high" 成顶级 // `"reasoning_effort":"high"`. func TestOpenAIBuildRequest_ThinkingDisabled_EffortHigh_Forwarded(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") disabled, high := "disabled", "high" req := &StreamRequest{ Model: "deepseek-v4-flash", MaxTokens: 100, ThinkingType: &disabled, ReasoningEffort: &high, } got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } var p thinkingProbe if err := json.Unmarshal(got, &p); err != nil { t.Fatalf("unmarshal: %v", err) } if p.Thinking == nil { t.Fatal("thinking object missing on wire when ThinkingType set") } if p.Thinking.Type != "disabled" { t.Errorf("thinking.type = %q, want %q", p.Thinking.Type, "disabled") } if p.ReasoningEffort == nil || *p.ReasoningEffort != "high" { t.Errorf("reasoning_effort = %v, want high", p.ReasoningEffort) } } // TestOpenAIBuildRequest_ThinkingEnabled_Forwarded pins the "enabled" arm of // the switch (the main / capability-probe path). // // 钉死开关的 "enabled" 分支 (main / capability-probe 路径). func TestOpenAIBuildRequest_ThinkingEnabled_Forwarded(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") enabled := "enabled" req := &StreamRequest{Model: "deepseek-v4-pro", MaxTokens: 100, ThinkingType: &enabled} got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } var p thinkingProbe if err := json.Unmarshal(got, &p); err != nil { t.Fatalf("unmarshal: %v", err) } if p.Thinking == nil || p.Thinking.Type != "enabled" { t.Errorf("thinking = %+v, want {type:enabled}", p.Thinking) } } // TestOpenAIBuildRequest_NilThinking_OmittedOnWire locks the nil-omits policy: // every non-DeepSeek openai-compat caller leaves ThinkingType / ReasoningEffort // nil, so the body must be byte-identical to before (no thinking / // reasoning_effort keys) -- proving the change is additive, not a regression // for MiniMax / OpenRouter / Ollama / local vLLM. // // 锁 nil 省略策略: 每个非 DeepSeek 的 openai-compat 调用方留 ThinkingType / // ReasoningEffort nil, body 须与改动前逐字节一致 (无 thinking / reasoning_effort // 键) -- 证明改动是叠加非回归 (MiniMax / OpenRouter / Ollama / 本地 vLLM). func TestOpenAIBuildRequest_NilThinking_OmittedOnWire(t *testing.T) { c := NewOpenAICompatClient("fake-key", "https://fake.api") req := &StreamRequest{Model: "gpt-4o", MaxTokens: 100} got, err := c.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } var raw map[string]json.RawMessage if err := json.Unmarshal(got, &raw); err != nil { t.Fatalf("unmarshal raw: %v", err) } if _, ok := raw["thinking"]; ok { t.Errorf("thinking key present on wire when ThinkingType == nil") } if _, ok := raw["reasoning_effort"]; ok { t.Errorf("reasoning_effort key present on wire when ReasoningEffort == nil") } }