| package builtin |
|
|
| import ( |
| "encoding/json" |
| "strings" |
| "testing" |
| ) |
|
|
| |
| |
| func TestOpenAIRequest_ToolResultPrecedesUserMessage(t *testing.T) { |
| conv := conversation{ |
| Turns: []conversationTurn{ |
| {Role: "assistant", Parts: []conversationPart{ |
| {Kind: partKindToolCall, ToolCall: &conversationToolCall{ID: "call_1", Name: "lookup", Arguments: json.RawMessage(`{"q":"x"}`)}}, |
| }}, |
| {Role: "user", Parts: []conversationPart{ |
| {Kind: partKindText, Text: "thanks"}, |
| {Kind: partKindToolResult, ToolResult: &conversationToolResult{CallID: "call_1", Parts: []conversationPart{{Kind: partKindText, Text: "result"}}}}, |
| }}, |
| }, |
| } |
| raw, err := encodeOpenAIRequest("gpt-x", conv, false) |
| if err != nil { |
| t.Fatalf("encodeOpenAIRequest failed: %v", err) |
| } |
| body := string(raw) |
| toolIdx := strings.Index(body, `"tool_call_id":"call_1"`) |
| userIdx := strings.Index(body, `"thanks"`) |
| if toolIdx < 0 || userIdx < 0 { |
| t.Fatalf("expected both tool and user fragments, got: %s", body) |
| } |
| if toolIdx > userIdx { |
| t.Fatalf("tool message must precede user message, got tool=%d user=%d body=%s", toolIdx, userIdx, body) |
| } |
| } |
|
|
| |
| func TestOpenAIRequest_ParallelToolCallsDisabled(t *testing.T) { |
| conv := conversation{ |
| Turns: []conversationTurn{{Role: "user", Parts: []conversationPart{{Kind: partKindText, Text: "hi"}}}}, |
| Tools: []conversationTool{{Type: "function", Name: "lookup"}}, |
| ToolChoice: conversationToolChoice{Mode: "auto", DisableParallel: true}, |
| } |
| raw, err := encodeOpenAIRequest("gpt-x", conv, false) |
| if err != nil { |
| t.Fatalf("encodeOpenAIRequest failed: %v", err) |
| } |
| if !strings.Contains(string(raw), `"parallel_tool_calls":false`) { |
| t.Fatalf("expected parallel_tool_calls=false in body: %s", string(raw)) |
| } |
| } |
|
|
| |
| func TestCodexRequest_ParallelToolCallsDisabled(t *testing.T) { |
| conv := conversation{ |
| Turns: []conversationTurn{{Role: "user", Parts: []conversationPart{{Kind: partKindText, Text: "hi"}}}}, |
| Tools: []conversationTool{{Type: "function", Name: "lookup"}}, |
| ToolChoice: conversationToolChoice{Mode: "auto", DisableParallel: true}, |
| } |
| raw, err := encodeCodexRequest("gpt-x", conv, false) |
| if err != nil { |
| t.Fatalf("encodeCodexRequest failed: %v", err) |
| } |
| if !strings.Contains(string(raw), `"parallel_tool_calls":false`) { |
| t.Fatalf("expected parallel_tool_calls=false in body: %s", string(raw)) |
| } |
| } |
|
|
| |
| |
| func TestCodexRequest_ReasoningGatedByThinking(t *testing.T) { |
| base := conversation{ |
| Turns: []conversationTurn{{Role: "user", Parts: []conversationPart{{Kind: partKindText, Text: "hi"}}}}, |
| } |
|
|
| t.Run("no thinking emits no reasoning config", func(t *testing.T) { |
| raw, err := encodeCodexRequest("gpt-x", base, false) |
| if err != nil { |
| t.Fatalf("encodeCodexRequest failed: %v", err) |
| } |
| body := string(raw) |
| if strings.Contains(body, `"reasoning"`) || strings.Contains(body, `"include"`) { |
| t.Fatalf("expected no reasoning/include without thinking, got: %s", body) |
| } |
| }) |
|
|
| t.Run("thinking enabled writes reasoning effort and include", func(t *testing.T) { |
| conv := base |
| conv.Thinking = &anthropicThinkingConfig{Type: "enabled", BudgetTokens: 8000} |
| raw, err := encodeCodexRequest("gpt-x", conv, false) |
| if err != nil { |
| t.Fatalf("encodeCodexRequest failed: %v", err) |
| } |
| body := string(raw) |
| if !strings.Contains(body, `"reasoning":{`) || !strings.Contains(body, `"effort":"medium"`) || !strings.Contains(body, `"summary":"auto"`) { |
| t.Fatalf("expected reasoning effort=medium summary=auto, got: %s", body) |
| } |
| if !strings.Contains(body, `"include":["reasoning.encrypted_content"]`) { |
| t.Fatalf("expected include=reasoning.encrypted_content, got: %s", body) |
| } |
| }) |
| } |
|
|
| |
| func TestGeminiRequest_SchemaCleaning(t *testing.T) { |
| conv := conversation{ |
| Turns: []conversationTurn{{Role: "user", Parts: []conversationPart{{Kind: partKindText, Text: "hi"}}}}, |
| Tools: []conversationTool{{ |
| Type: "function", |
| Name: "lookup", |
| InputSchema: json.RawMessage(`{ |
| "$schema": "http://json-schema.org/draft-07/schema#", |
| "type": "object", |
| "additionalProperties": false, |
| "x-google-ext": "drop-me", |
| "properties": { |
| "q": {"type": "string", "format": "email", "minLength": 1, "title": "Query"}, |
| "format": {"type": "string"} |
| }, |
| "required": ["q"] |
| }`), |
| }}, |
| } |
| raw, err := encodeGeminiRequest(conv) |
| if err != nil { |
| t.Fatalf("encodeGeminiRequest failed: %v", err) |
| } |
| body := string(raw) |
| for _, banned := range []string{`"$schema"`, `"additionalProperties"`, `"x-google-ext"`, `"format":"email"`, `"minLength"`, `"title":"Query"`} { |
| if strings.Contains(body, banned) { |
| t.Fatalf("expected %s removed, body: %s", banned, body) |
| } |
| } |
| |
| if !strings.Contains(body, `"format":{"type":"string"}`) { |
| t.Fatalf("user-defined property named 'format' must be preserved, body: %s", body) |
| } |
| if !strings.Contains(body, `"required":["q"]`) { |
| t.Fatalf("required must be preserved, body: %s", body) |
| } |
| } |
|
|
| |
| func TestGeminiRequest_FunctionResponseEnvelope(t *testing.T) { |
| conv := conversation{ |
| Turns: []conversationTurn{ |
| {Role: "user", Parts: []conversationPart{{Kind: partKindText, Text: "go"}}}, |
| {Role: "user", Parts: []conversationPart{{Kind: partKindToolResult, ToolResult: &conversationToolResult{ |
| CallID: "call_42", |
| Name: "lookup", |
| IsError: true, |
| Parts: []conversationPart{{Kind: partKindText, Text: "boom"}}, |
| }}}}, |
| }, |
| } |
| raw, err := encodeGeminiRequest(conv) |
| if err != nil { |
| t.Fatalf("encodeGeminiRequest failed: %v", err) |
| } |
| body := string(raw) |
| if !strings.Contains(body, `"functionResponse":{"name":"lookup","response":{"output":"boom"}}`) { |
| t.Fatalf("unexpected functionResponse shape: %s", body) |
| } |
| if strings.Contains(body, `"call_id"`) || strings.Contains(body, `"is_error"`) { |
| t.Fatalf("envelope leaked anthropic-only fields: %s", body) |
| } |
| } |
|
|
| |
| func TestGeminiRequest_ThinkingConfig(t *testing.T) { |
| base := conversation{ |
| Turns: []conversationTurn{{Role: "user", Parts: []conversationPart{{Kind: partKindText, Text: "hi"}}}}, |
| } |
|
|
| t.Run("enabled with budget", func(t *testing.T) { |
| conv := base |
| conv.Thinking = &anthropicThinkingConfig{Type: "enabled", BudgetTokens: 1024} |
| raw, err := encodeGeminiRequest(conv) |
| if err != nil { |
| t.Fatalf("encodeGeminiRequest failed: %v", err) |
| } |
| body := string(raw) |
| if !strings.Contains(body, `"thinkingConfig":{"includeThoughts":true,"thinkingBudget":1024}`) { |
| t.Fatalf("expected enabled thinkingConfig, got: %s", body) |
| } |
| }) |
|
|
| t.Run("disabled forces thinkingBudget=0", func(t *testing.T) { |
| conv := base |
| conv.Thinking = &anthropicThinkingConfig{Type: "disabled"} |
| raw, err := encodeGeminiRequest(conv) |
| if err != nil { |
| t.Fatalf("encodeGeminiRequest failed: %v", err) |
| } |
| if !strings.Contains(string(raw), `"thinkingConfig":{"thinkingBudget":0}`) { |
| t.Fatalf("expected disabled thinkingConfig, got: %s", string(raw)) |
| } |
| }) |
|
|
| t.Run("nil emits no generationConfig", func(t *testing.T) { |
| raw, err := encodeGeminiRequest(base) |
| if err != nil { |
| t.Fatalf("encodeGeminiRequest failed: %v", err) |
| } |
| if strings.Contains(string(raw), `"generationConfig"`) { |
| t.Fatalf("expected no generationConfig without thinking, got: %s", string(raw)) |
| } |
| }) |
| } |
|
|
| |
| func TestNormalizeOpenAI_TopLevelParallelToolCallsDisabled(t *testing.T) { |
| f := false |
| req := openAIChatRequest{ |
| Model: "gpt-x", |
| Messages: []openAIChatMessage{{Role: "user", Content: "hi"}}, |
| ParallelToolCalls: &f, |
| } |
| conv, err := normalizeOpenAIConversation(req) |
| if err != nil { |
| t.Fatalf("normalizeOpenAIConversation failed: %v", err) |
| } |
| if !conv.ToolChoice.DisableParallel { |
| t.Fatalf("expected DisableParallel=true, got %+v", conv.ToolChoice) |
| } |
| } |
|
|
| |
| func TestOpenAIToAnthropic_SamplingPropagation(t *testing.T) { |
| temp := 0.3 |
| topP := 0.9 |
| topK := 40 |
| mct := 2048 |
| req := openAIChatRequest{ |
| Model: "gpt-x", |
| Messages: []openAIChatMessage{{Role: "user", Content: "hi"}}, |
| Temperature: &temp, |
| TopP: &topP, |
| TopK: &topK, |
| MaxCompletionTokens: &mct, |
| Stop: json.RawMessage(`["stop1","stop2"]`), |
| ReasoningEffort: "high", |
| } |
| conv, err := normalizeOpenAIConversation(req) |
| if err != nil { |
| t.Fatalf("normalizeOpenAIConversation failed: %v", err) |
| } |
| raw, err := encodeAnthropicRequest("claude-x", conv, false) |
| if err != nil { |
| t.Fatalf("encodeAnthropicRequest failed: %v", err) |
| } |
| body := string(raw) |
| for _, frag := range []string{ |
| `"max_tokens":2048`, `"temperature":0.3`, `"top_p":0.9`, `"top_k":40`, |
| `"stop_sequences":["stop1","stop2"]`, `"thinking":{"type":"enabled","budget_tokens":16384}`, |
| } { |
| if !strings.Contains(body, frag) { |
| t.Fatalf("expected fragment %s in body: %s", frag, body) |
| } |
| } |
| } |
|
|
| |
| func TestOpenAIToCodex_SamplingPropagation(t *testing.T) { |
| temp := 0.5 |
| topP := 0.95 |
| maxT := 4096 |
| req := openAIChatRequest{ |
| Model: "gpt-x", |
| Messages: []openAIChatMessage{{Role: "user", Content: "hi"}}, |
| Temperature: &temp, |
| TopP: &topP, |
| MaxTokens: &maxT, |
| ReasoningEffort: "low", |
| User: "user-42", |
| } |
| conv, err := normalizeOpenAIConversation(req) |
| if err != nil { |
| t.Fatalf("normalizeOpenAIConversation failed: %v", err) |
| } |
| raw, err := encodeCodexRequest("codex-x", conv, false) |
| if err != nil { |
| t.Fatalf("encodeCodexRequest failed: %v", err) |
| } |
| body := string(raw) |
| for _, frag := range []string{ |
| `"temperature":0.5`, `"top_p":0.95`, `"max_output_tokens":4096`, |
| `"user":"user-42"`, `"effort":"low"`, `"summary":"auto"`, |
| `"include":["reasoning.encrypted_content"]`, |
| } { |
| if !strings.Contains(body, frag) { |
| t.Fatalf("expected fragment %s in body: %s", frag, body) |
| } |
| } |
| } |
|
|
| |
| func TestOpenAIToGemini_SamplingPropagation(t *testing.T) { |
| temp := 0.2 |
| topP := 0.8 |
| topK := 50 |
| maxT := 1024 |
| seed := int64(12345) |
| req := openAIChatRequest{ |
| Model: "gpt-x", |
| Messages: []openAIChatMessage{{Role: "user", Content: "hi"}}, |
| Temperature: &temp, |
| TopP: &topP, |
| TopK: &topK, |
| MaxTokens: &maxT, |
| Stop: json.RawMessage(`"END"`), |
| ReasoningEffort: "medium", |
| Seed: &seed, |
| } |
| conv, err := normalizeOpenAIConversation(req) |
| if err != nil { |
| t.Fatalf("normalizeOpenAIConversation failed: %v", err) |
| } |
| raw, err := encodeGeminiRequest(conv) |
| if err != nil { |
| t.Fatalf("encodeGeminiRequest failed: %v", err) |
| } |
| body := string(raw) |
| for _, frag := range []string{ |
| `"temperature":0.2`, `"topP":0.8`, `"topK":50`, `"maxOutputTokens":1024`, |
| `"stopSequences":["END"]`, `"seed":12345`, |
| `"thinkingConfig":{"includeThoughts":true,"thinkingBudget":4096}`, |
| } { |
| if !strings.Contains(body, frag) { |
| t.Fatalf("expected fragment %s in body: %s", frag, body) |
| } |
| } |
| } |
|
|
| |
| func TestOpenAIToOpenAI_SamplingPropagation(t *testing.T) { |
| temp := 0.4 |
| topP := 0.85 |
| maxT := 512 |
| seed := int64(7) |
| fp := 0.1 |
| pp := -0.1 |
| req := openAIChatRequest{ |
| Model: "gpt-x", |
| Messages: []openAIChatMessage{{Role: "user", Content: "hi"}}, |
| Temperature: &temp, |
| TopP: &topP, |
| MaxTokens: &maxT, |
| Stop: json.RawMessage(`["a","b"]`), |
| ReasoningEffort: "high", |
| Seed: &seed, |
| FrequencyPenalty: &fp, |
| PresencePenalty: &pp, |
| User: "alice", |
| } |
| conv, err := normalizeOpenAIConversation(req) |
| if err != nil { |
| t.Fatalf("normalizeOpenAIConversation failed: %v", err) |
| } |
| raw, err := encodeOpenAIRequest("gpt-x", conv, false) |
| if err != nil { |
| t.Fatalf("encodeOpenAIRequest failed: %v", err) |
| } |
| body := string(raw) |
| for _, frag := range []string{ |
| `"temperature":0.4`, `"top_p":0.85`, `"max_tokens":512`, |
| `"stop":["a","b"]`, `"reasoning_effort":"high"`, `"seed":7`, |
| `"frequency_penalty":0.1`, `"presence_penalty":-0.1`, `"user":"alice"`, |
| } { |
| if !strings.Contains(body, frag) { |
| t.Fatalf("expected fragment %s in body: %s", frag, body) |
| } |
| } |
| } |
|
|
| |
| func TestNormalizeAnthropic_ThinkingAndDisableParallel(t *testing.T) { |
| req := anthropicMessagesRequest{ |
| Model: "claude-x", |
| Messages: []anthropicMessageContent{{Role: "user", Content: "hi"}}, |
| Tools: json.RawMessage(`[{"name":"lookup","description":"d","input_schema":{"type":"object"}}]`), |
| ToolChoice: json.RawMessage(`{"type":"auto","disable_parallel_tool_use":true}`), |
| Thinking: &anthropicThinkingConfig{Type: "enabled", BudgetTokens: 4096}, |
| } |
| conv, err := normalizeAnthropicConversation(req) |
| if err != nil { |
| t.Fatalf("normalizeAnthropicConversation failed: %v", err) |
| } |
| if !conv.ToolChoice.DisableParallel { |
| t.Fatalf("expected ToolChoice.DisableParallel=true, got %+v", conv.ToolChoice) |
| } |
| if conv.Thinking == nil || conv.Thinking.Type != "enabled" || conv.Thinking.BudgetTokens != 4096 { |
| t.Fatalf("expected Thinking{enabled,4096}, got %+v", conv.Thinking) |
| } |
| } |
|
|
| |
| func TestNormalizeCodex_SamplingReasoningAndParallel(t *testing.T) { |
| temp := 0.7 |
| topP := 0.9 |
| maxTok := 2048 |
| f := false |
| req := codexRequest{ |
| Model: "gpt-5", |
| Input: []json.RawMessage{json.RawMessage(`{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}`)}, |
| Reasoning: &codexReasoning{Effort: "high", Summary: "auto"}, |
| ParallelToolCalls: &f, |
| Temperature: &temp, |
| TopP: &topP, |
| MaxOutputTokens: &maxTok, |
| Stop: json.RawMessage(`["END"]`), |
| User: "tester", |
| } |
| conv, err := normalizeCodexConversation(req) |
| if err != nil { |
| t.Fatalf("normalizeCodexConversation failed: %v", err) |
| } |
| if !conv.ToolChoice.DisableParallel { |
| t.Fatalf("expected DisableParallel=true, got %+v", conv.ToolChoice) |
| } |
| if conv.Sampling == nil || conv.Sampling.Temperature == nil || *conv.Sampling.Temperature != 0.7 || |
| conv.Sampling.TopP == nil || *conv.Sampling.TopP != 0.9 || |
| conv.Sampling.MaxTokens == nil || *conv.Sampling.MaxTokens != 2048 || |
| conv.Sampling.ReasoningEffort != "high" || conv.Sampling.User != "tester" || |
| len(conv.Sampling.Stop) != 1 || conv.Sampling.Stop[0] != "END" { |
| t.Fatalf("sampling mismatch: %+v", conv.Sampling) |
| } |
| if conv.Thinking == nil || conv.Thinking.Type != "enabled" || conv.Thinking.BudgetTokens != 16384 { |
| t.Fatalf("expected Thinking{enabled,16384} from reasoning.effort=high, got %+v", conv.Thinking) |
| } |
| } |
|
|
| |
| func TestConvertCodexRequestToOpenAI_FieldsPreserved(t *testing.T) { |
| raw := []byte(`{ |
| "model":"gpt-5", |
| "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}], |
| "reasoning":{"effort":"medium","summary":"auto"}, |
| "parallel_tool_calls":false, |
| "tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}], |
| "temperature":0.5, |
| "top_p":0.8, |
| "max_output_tokens":512, |
| "stop":["DONE"], |
| "user":"u1" |
| }`) |
| out, err := convertCodexRequestToOpenAI("gpt-5", raw, false) |
| if err != nil { |
| t.Fatalf("convertCodexRequestToOpenAI failed: %v", err) |
| } |
| body := string(out) |
| for _, want := range []string{ |
| `"temperature":0.5`, `"top_p":0.8`, `"max_tokens":512`, |
| `"stop":["DONE"]`, `"user":"u1"`, |
| `"reasoning_effort":"medium"`, `"parallel_tool_calls":false`, |
| } { |
| if !strings.Contains(body, want) { |
| t.Fatalf("missing %s in output: %s", want, body) |
| } |
| } |
| } |
|
|
| |
| |
| func TestConvertCodexRequestToAnthropic_FieldsPreserved(t *testing.T) { |
| raw := []byte(`{ |
| "model":"claude-sonnet", |
| "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}], |
| "reasoning":{"effort":"high"}, |
| "parallel_tool_calls":false, |
| "tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}], |
| "temperature":0.3, |
| "top_p":0.95, |
| "max_output_tokens":4096, |
| "stop":["BYE"] |
| }`) |
| out, err := convertCodexRequestToAnthropic("claude-sonnet", raw, false) |
| if err != nil { |
| t.Fatalf("convertCodexRequestToAnthropic failed: %v", err) |
| } |
| body := string(out) |
| for _, want := range []string{ |
| `"temperature":0.3`, `"top_p":0.95`, `"max_tokens":4096`, |
| `"stop_sequences":["BYE"]`, |
| `"thinking":{"type":"enabled","budget_tokens":16384}`, |
| `"disable_parallel_tool_use":true`, |
| } { |
| if !strings.Contains(body, want) { |
| t.Fatalf("missing %s in output: %s", want, body) |
| } |
| } |
| } |
|
|
| |
| func TestConvertCodexRequestToGemini_FieldsPreserved(t *testing.T) { |
| raw := []byte(`{ |
| "model":"gemini-2", |
| "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}], |
| "reasoning":{"effort":"medium"}, |
| "temperature":0.4, |
| "top_p":0.85, |
| "max_output_tokens":1024, |
| "stop":["STOP"] |
| }`) |
| out, err := convertCodexRequestToGemini("gemini-2", raw, false) |
| if err != nil { |
| t.Fatalf("convertCodexRequestToGemini failed: %v", err) |
| } |
| body := string(out) |
| for _, want := range []string{ |
| `"temperature":0.4`, `"topP":0.85`, `"maxOutputTokens":1024`, |
| `"stopSequences":["STOP"]`, |
| `"thinkingBudget":4096`, `"includeThoughts":true`, |
| } { |
| if !strings.Contains(body, want) { |
| t.Fatalf("missing %s in output: %s", want, body) |
| } |
| } |
| } |
|
|
| |
| |
| func TestEncodeAnthropicRequest_DisableParallelInjected(t *testing.T) { |
| conv := conversation{ |
| Turns: []conversationTurn{{Role: "user", Parts: []conversationPart{{Kind: partKindText, Text: "hi"}}}}, |
| Tools: []conversationTool{{Type: "function", Name: "lookup"}}, |
| ToolChoice: conversationToolChoice{DisableParallel: true}, |
| } |
| raw, err := encodeAnthropicRequest("claude-x", conv, false) |
| if err != nil { |
| t.Fatalf("encodeAnthropicRequest failed: %v", err) |
| } |
| body := string(raw) |
| if !strings.Contains(body, `"tool_choice":{`) || |
| !strings.Contains(body, `"type":"auto"`) || |
| !strings.Contains(body, `"disable_parallel_tool_use":true`) { |
| t.Fatalf("expected tool_choice with disable_parallel_tool_use=true, got: %s", body) |
| } |
| } |
|
|