| package protocol_test |
|
|
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "strings" |
| "testing" |
|
|
| "ccLoad/internal/protocol" |
| "ccLoad/internal/protocol/builtin" |
| ) |
|
|
| func TestRegistry_TranslateRequest_OpenAIToGemini(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"hello"}],"stream":false}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if string(got) != `{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}` { |
| t.Fatalf("unexpected translated request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_GeminiToOpenAI(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"hello"}],"stream":false}`) |
| translatedReq := []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`) |
| rawResp := []byte(`{"candidates":[{"content":{"parts":[{"text":"world"}]}}],"usageMetadata":{"promptTokenCount":3,"candidatesTokenCount":5,"totalTokenCount":8},"modelVersion":"gemini-2.5-pro"}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Gemini, protocol.OpenAI, "gemini-2.5-pro", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
|
|
| want := `{"id":"chatcmpl-proxy","object":"chat.completion","created":0,"model":"gemini-2.5-pro","choices":[{"index":0,"message":{"role":"assistant","content":"world"},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":5,"total_tokens":8}}` |
| if string(got) != want { |
| t.Fatalf("unexpected translated response:\nwant: %s\ngot: %s", want, got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_AnthropicToGemini(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) |
| got, err := reg.TranslateRequest(protocol.Anthropic, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if string(got) != `{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}` { |
| t.Fatalf("unexpected translated request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_GeminiToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) |
| translatedReq := []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`) |
| rawResp := []byte(`{"candidates":[{"content":{"parts":[{"text":"world"}]}}],"usageMetadata":{"promptTokenCount":3,"candidatesTokenCount":5,"totalTokenCount":8},"modelVersion":"gemini-2.5-pro"}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Gemini, protocol.Anthropic, "gemini-2.5-pro", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"type":"message"`) || !strings.Contains(string(got), `"role":"assistant"`) || !strings.Contains(string(got), `"type":"text"`) || !strings.Contains(string(got), `"text":"world"`) || !strings.Contains(string(got), `"model":"gemini-2.5-pro"`) || !strings.Contains(string(got), `"stop_reason":"end_turn"`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_CodexToGemini(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| got, err := reg.TranslateRequest(protocol.Codex, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if string(got) != `{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}` { |
| t.Fatalf("unexpected translated request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_GeminiToCodex(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gemini-2.5-pro","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| translatedReq := []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`) |
| rawResp := []byte(`{"candidates":[{"content":{"parts":[{"text":"world"}]}}],"usageMetadata":{"promptTokenCount":3,"candidatesTokenCount":5,"totalTokenCount":8},"modelVersion":"gemini-2.5-pro"}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Gemini, protocol.Codex, "gemini-2.5-pro", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"object":"response"`) || !strings.Contains(string(got), `"status":"completed"`) || !strings.Contains(string(got), `"model":"gemini-2.5-pro"`) || !strings.Contains(string(got), `"type":"message"`) || !strings.Contains(string(got), `"role":"assistant"`) || !strings.Contains(string(got), `"type":"output_text"`) || !strings.Contains(string(got), `"text":"world"`) || !strings.Contains(string(got), `"input_tokens":3`) || !strings.Contains(string(got), `"output_tokens":5`) || !strings.Contains(string(got), `"total_tokens":8`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_GeminiToOpenAI(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"hello"}],"stream":true}`) |
| translatedReq := []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`) |
|
|
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.OpenAI, "gemini-2.5-pro", rawReq, translatedReq, []byte("data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"hello\"}]}}]}\n\n"), nil) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| if len(chunks) != 1 { |
| t.Fatalf("unexpected translated stream chunk: %#v", chunks) |
| } |
| gotChunk := string(chunks[0]) |
| if !strings.HasPrefix(gotChunk, "data: {") || !strings.Contains(gotChunk, `"object":"chat.completion.chunk"`) || !strings.Contains(gotChunk, `"content":"hello"`) { |
| t.Fatalf("unexpected translated stream chunk: %#v", chunks) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.OpenAI, "gemini-2.5-pro", rawReq, translatedReq, []byte("data: [DONE]\n\n"), nil) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| if len(done) != 1 || string(done[0]) != "data: [DONE]\n\n" { |
| t.Fatalf("unexpected done chunk: %#v", done) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_NilStatePointerSupported(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| testCases := []struct { |
| name string |
| from protocol.Protocol |
| to protocol.Protocol |
| model string |
| payload []byte |
| want string |
| }{ |
| { |
| name: "gemini to anthropic", |
| from: protocol.Gemini, |
| to: protocol.Anthropic, |
| model: "gemini-2.5-pro", |
| payload: []byte("data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"hello\"}]}}]}\n\n"), |
| want: "event: message_start", |
| }, |
| { |
| name: "gemini to codex", |
| from: protocol.Gemini, |
| to: protocol.Codex, |
| model: "gemini-2.5-pro", |
| payload: []byte("data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"hello\"}]}}]}\n\n"), |
| want: "event: response.output_text.delta", |
| }, |
| { |
| name: "openai to codex", |
| from: protocol.OpenAI, |
| to: protocol.Codex, |
| model: "gpt-5-codex", |
| payload: []byte("data: {\"id\":\"chatcmpl_1\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5-codex\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hello\"},\"finish_reason\":null}]}\n\n"), |
| want: "event: response.output_text.delta", |
| }, |
| { |
| name: "codex to openai", |
| from: protocol.Codex, |
| to: protocol.OpenAI, |
| model: "gpt-4o", |
| payload: []byte("event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"hello\"}\n\n"), |
| want: "\"chat.completion.chunk\"", |
| }, |
| } |
|
|
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| chunks, err := reg.TranslateResponseStream(context.Background(), tc.from, tc.to, tc.model, nil, nil, tc.payload, nil) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| if len(chunks) == 0 { |
| t.Fatalf("expected translated stream chunks, got %#v", chunks) |
| } |
| if joined := string(bytes.Join(chunks, nil)); !strings.Contains(joined, tc.want) { |
| t.Fatalf("unexpected translated stream chunks: %s", joined) |
| } |
| }) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_GeminiToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}],"stream":true}`) |
| translatedReq := []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`) |
|
|
| var state any |
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.Anthropic, "gemini-2.5-pro", rawReq, translatedReq, []byte("data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"hello\"}]}}]}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| if len(chunks) < 3 { |
| t.Fatalf("expected anthropic stream bootstrap chunks, got %#v", chunks) |
| } |
| joined := string(bytes.Join(chunks, nil)) |
| if !strings.Contains(joined, "event: message_start") || !strings.Contains(joined, "event: content_block_delta") || !strings.Contains(joined, "\"text\":\"hello\"") { |
| t.Fatalf("unexpected anthropic stream chunks: %#v", chunks) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_GeminiToAnthropic_DoneAfterFinishedChunkEmitsNothing(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| var state any |
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte("data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"hello\"}]},\"finishReason\":\"STOP\"}],\"modelVersion\":\"gemini-2.5-pro\",\"usageMetadata\":{\"promptTokenCount\":3,\"candidatesTokenCount\":5,\"totalTokenCount\":8}}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream finished chunk failed: %v", err) |
| } |
| joined := string(bytes.Join(chunks, nil)) |
| if !strings.Contains(joined, "event: message_stop") { |
| t.Fatalf("expected finished chunk to emit message_stop, got %#v", chunks) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte("data: [DONE]\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| if done != nil { |
| t.Fatalf("expected DONE sentinel to emit nothing after finished chunk, got %#v", done) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_GeminiToCodex(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gemini-2.5-pro","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}],"stream":true}`) |
| translatedReq := []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`) |
|
|
| var state any |
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.Codex, "gemini-2.5-pro", rawReq, translatedReq, []byte("data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"hello\"}]}}],\"usageMetadata\":{\"promptTokenCount\":3,\"candidatesTokenCount\":5,\"totalTokenCount\":8},\"modelVersion\":\"gemini-2.5-pro\"}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| if len(chunks) != 1 { |
| t.Fatalf("unexpected codex stream chunks: %#v", chunks) |
| } |
| if !strings.Contains(string(chunks[0]), "event: response.output_text.delta") || !strings.Contains(string(chunks[0]), `"delta":"hello"`) { |
| t.Fatalf("unexpected codex stream chunk: %#v", chunks) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.Codex, "gemini-2.5-pro", rawReq, translatedReq, []byte("data: [DONE]\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| if len(done) != 1 { |
| t.Fatalf("unexpected codex done chunks: %#v", done) |
| } |
| gotDone := string(done[0]) |
| if !strings.Contains(gotDone, "event: response.completed") { |
| t.Fatalf("unexpected codex done chunk: %#v", done) |
| } |
| payload, ok := strings.CutPrefix(gotDone, "event: response.completed\ndata: ") |
| if !ok { |
| t.Fatalf("missing codex stream payload: %#v", done) |
| } |
| payload = strings.TrimSpace(payload) |
| var envelope struct { |
| Type string `json:"type"` |
| Response struct { |
| Status string `json:"status"` |
| Model string `json:"model"` |
| Usage struct { |
| InputTokens int64 `json:"input_tokens"` |
| OutputTokens int64 `json:"output_tokens"` |
| TotalTokens int64 `json:"total_tokens"` |
| } `json:"usage"` |
| } `json:"response"` |
| } |
| if err := json.Unmarshal([]byte(payload), &envelope); err != nil { |
| t.Fatalf("unmarshal codex stream payload: %v", err) |
| } |
| if envelope.Type != "response.completed" || |
| envelope.Response.Status != "completed" || |
| envelope.Response.Model != "gemini-2.5-pro" || |
| envelope.Response.Usage.InputTokens != 3 || |
| envelope.Response.Usage.OutputTokens != 5 || |
| envelope.Response.Usage.TotalTokens != 8 { |
| t.Fatalf("unexpected codex done payload: %+v", envelope) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_GeminiToCodex_PreservesResponseID(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| var state any |
| if _, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.Codex, "gemini-2.5-pro", nil, nil, []byte("data: {\"responseId\":\"resp_1\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"hello\"}]}}],\"usageMetadata\":{\"promptTokenCount\":3,\"candidatesTokenCount\":5,\"totalTokenCount\":8},\"modelVersion\":\"gemini-2.5-pro\"}\n\n"), &state); err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.Gemini, protocol.Codex, "gemini-2.5-pro", nil, nil, []byte("data: [DONE]\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| if len(done) != 1 { |
| t.Fatalf("unexpected codex done chunks: %#v", done) |
| } |
|
|
| payload, ok := strings.CutPrefix(string(done[0]), "event: response.completed\ndata: ") |
| if !ok { |
| t.Fatalf("missing codex stream payload: %#v", done) |
| } |
| payload = strings.TrimSpace(payload) |
|
|
| var envelope struct { |
| Response struct { |
| ID string `json:"id"` |
| } `json:"response"` |
| } |
| if err := json.Unmarshal([]byte(payload), &envelope); err != nil { |
| t.Fatalf("unmarshal codex stream payload: %v", err) |
| } |
| if envelope.Response.ID != "resp_1" { |
| t.Fatalf("expected response id resp_1, got %+v", envelope) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_CodexToOpenAI(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}],"stream":true}`) |
| translatedReq := []byte(`{"model":"gpt-4o","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}],"stream":true}`) |
|
|
| var state any |
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", rawReq, translatedReq, []byte("event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"hello\"}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| if len(chunks) != 1 || !strings.Contains(string(chunks[0]), `"chat.completion.chunk"`) || !strings.Contains(string(chunks[0]), `"content":"hello"`) { |
| t.Fatalf("unexpected openai stream chunk: %#v", chunks) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", rawReq, translatedReq, []byte("event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"model\":\"gpt-4o\",\"usage\":{\"input_tokens\":3,\"output_tokens\":5,\"total_tokens\":8}}}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| if len(done) != 2 || !strings.Contains(string(done[0]), `"finish_reason":"stop"`) || !strings.Contains(string(done[0]), `"prompt_tokens":3`) || string(done[1]) != "data: [DONE]\n\n" { |
| t.Fatalf("unexpected done chunks: %#v", done) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_OpenAIToCodex(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}],"stream":true}`) |
| translatedReq := []byte(`{"model":"gpt-5-codex","messages":[{"role":"user","content":"hello"}],"stream":true}`) |
|
|
| var state any |
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Codex, "gpt-5-codex", rawReq, translatedReq, []byte("data: {\"id\":\"chatcmpl_1\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5-codex\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hello\"},\"finish_reason\":null}]}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| if len(chunks) != 1 || !strings.Contains(string(chunks[0]), "event: response.output_text.delta") || !strings.Contains(string(chunks[0]), `"delta":"hello"`) { |
| t.Fatalf("unexpected codex stream chunk: %#v", chunks) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Codex, "gpt-5-codex", rawReq, translatedReq, []byte("data: [DONE]\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| if len(done) != 1 || !strings.Contains(string(done[0]), "event: response.completed") { |
| t.Fatalf("unexpected codex done chunk: %#v", done) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"claude-3-5-sonnet","messages":[{"role":"system","content":"be careful"},{"role":"user","content":"hello"}]}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Anthropic, "claude-3-5-sonnet", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"system":[{`) || !strings.Contains(string(got), `"text":"be careful"`) { |
| t.Fatalf("expected anthropic system field, got %s", got) |
| } |
| if !strings.Contains(string(got), `"role":"user"`) || !strings.Contains(string(got), `"text":"hello"`) { |
| t.Fatalf("unexpected translated request: %s", got) |
| } |
| if !strings.Contains(string(got), `"stream":false`) { |
| t.Fatalf("expected anthropic stream=false to preserve non-stream request, got %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToAnthropic_TextOnlyAssistantUsesStringContent(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"mimo-v2.5","messages":[{"role":"user","content":"first"},{"role":"assistant","content":"previous answer"},{"role":"user","content":"next"}]}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Anthropic, "mimo-v2.5", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
|
|
| var req struct { |
| Messages []struct { |
| Role string `json:"role"` |
| Content any `json:"content"` |
| } `json:"messages"` |
| } |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal translated request: %v", err) |
| } |
| if len(req.Messages) != 3 { |
| t.Fatalf("messages length = %d, want 3; body=%s", len(req.Messages), got) |
| } |
| if req.Messages[1].Role != "assistant" { |
| t.Fatalf("message[1].role = %q, want assistant", req.Messages[1].Role) |
| } |
| if content, ok := req.Messages[1].Content.(string); !ok || content != "previous answer" { |
| t.Fatalf("assistant content = %#v, want string previous answer; body=%s", req.Messages[1].Content, got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToAnthropic_SystemOnly(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"claude-3-5-sonnet","messages":[{"role":"system","content":"optimize this code"}]}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Anthropic, "claude-3-5-sonnet", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
|
|
| var req struct { |
| System []map[string]any `json:"system"` |
| Messages []struct { |
| Role string `json:"role"` |
| Content any `json:"content"` |
| } `json:"messages"` |
| } |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal translated request: %v", err) |
| } |
| if len(req.System) != 0 { |
| t.Fatalf("expected no anthropic system field for system-only prompt, got %+v", req.System) |
| } |
| if len(req.Messages) != 1 || req.Messages[0].Role != "user" { |
| t.Fatalf("unexpected anthropic messages: %+v", req.Messages) |
| } |
| content, ok := req.Messages[0].Content.([]any) |
| if !ok || len(content) != 1 { |
| t.Fatalf("expected one anthropic content block, got %+v", req.Messages[0].Content) |
| } |
| block, ok := content[0].(map[string]any) |
| if !ok || block["type"] != "text" || block["text"] != "optimize this code" { |
| t.Fatalf("unexpected anthropic content block: %+v", content[0]) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToGemini_SystemOnly(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"system","content":"optimize this code"}]}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
|
|
| var req struct { |
| SystemInstruction struct { |
| Parts []struct { |
| Text string `json:"text"` |
| } `json:"parts"` |
| } `json:"systemInstruction"` |
| Contents []struct { |
| Role string `json:"role"` |
| Parts []struct { |
| Text string `json:"text"` |
| } `json:"parts"` |
| } `json:"contents"` |
| } |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal translated request: %v", err) |
| } |
| if len(req.SystemInstruction.Parts) != 0 { |
| t.Fatalf("expected no gemini system instruction for system-only prompt, got %+v", req.SystemInstruction) |
| } |
| if len(req.Contents) != 1 || req.Contents[0].Role != "user" || len(req.Contents[0].Parts) != 1 || req.Contents[0].Parts[0].Text != "optimize this code" { |
| t.Fatalf("expected user prompt content, got %+v", req.Contents) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToCodex_SystemOnly(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gpt-5-codex","messages":[{"role":"system","content":"optimize this code"}]}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Codex, "gpt-5-codex", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
|
|
| var req map[string]any |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal translated request: %v", err) |
| } |
| if req["instructions"] != "optimize this code" { |
| t.Fatalf("unexpected codex instructions: %+v", req) |
| } |
| if _, ok := req["input"]; ok { |
| t.Fatalf("expected codex request without input items, got %+v", req) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_SystemOnlySemantics_OtherSources(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| const prompt = "optimize this code" |
|
|
| assertAnthropicUserPrompt := func(t *testing.T, body []byte) { |
| t.Helper() |
| var req struct { |
| System []map[string]any `json:"system"` |
| Messages []struct { |
| Role string `json:"role"` |
| Content []struct { |
| Type string `json:"type"` |
| Text string `json:"text"` |
| } `json:"content"` |
| } `json:"messages"` |
| } |
| if err := json.Unmarshal(body, &req); err != nil { |
| t.Fatalf("unmarshal anthropic request: %v", err) |
| } |
| if len(req.System) != 0 { |
| t.Fatalf("expected no anthropic system field, got %+v", req.System) |
| } |
| if len(req.Messages) != 1 || req.Messages[0].Role != "user" || len(req.Messages[0].Content) != 1 || req.Messages[0].Content[0].Type != "text" || req.Messages[0].Content[0].Text != prompt { |
| t.Fatalf("unexpected anthropic request: %+v", req) |
| } |
| } |
|
|
| assertGeminiUserPrompt := func(t *testing.T, body []byte) { |
| t.Helper() |
| var req struct { |
| SystemInstruction *struct { |
| Parts []struct { |
| Text string `json:"text"` |
| } `json:"parts"` |
| } `json:"systemInstruction"` |
| Contents []struct { |
| Role string `json:"role"` |
| Parts []struct { |
| Text string `json:"text"` |
| } `json:"parts"` |
| } `json:"contents"` |
| } |
| if err := json.Unmarshal(body, &req); err != nil { |
| t.Fatalf("unmarshal gemini request: %v", err) |
| } |
| if req.SystemInstruction != nil && len(req.SystemInstruction.Parts) > 0 { |
| t.Fatalf("expected no gemini system instruction, got %+v", req.SystemInstruction) |
| } |
| if len(req.Contents) != 1 || req.Contents[0].Role != "user" || len(req.Contents[0].Parts) != 1 || req.Contents[0].Parts[0].Text != prompt { |
| t.Fatalf("unexpected gemini request: %+v", req) |
| } |
| } |
|
|
| assertOpenAISystemPrompt := func(t *testing.T, body []byte) { |
| t.Helper() |
| var req struct { |
| Messages []struct { |
| Role string `json:"role"` |
| Content any `json:"content"` |
| } `json:"messages"` |
| } |
| if err := json.Unmarshal(body, &req); err != nil { |
| t.Fatalf("unmarshal openai request: %v", err) |
| } |
| if len(req.Messages) != 1 || req.Messages[0].Role != "system" || req.Messages[0].Content != prompt { |
| t.Fatalf("unexpected openai request: %+v", req) |
| } |
| } |
|
|
| assertCodexInstructionsOnly := func(t *testing.T, body []byte) { |
| t.Helper() |
| var req map[string]any |
| if err := json.Unmarshal(body, &req); err != nil { |
| t.Fatalf("unmarshal codex request: %v", err) |
| } |
| if req["instructions"] != prompt { |
| t.Fatalf("unexpected codex instructions: %+v", req) |
| } |
| if _, ok := req["input"]; ok { |
| t.Fatalf("expected codex request without input, got %+v", req) |
| } |
| } |
|
|
| tests := []struct { |
| name string |
| from protocol.Protocol |
| to protocol.Protocol |
| model string |
| raw []byte |
| assert func(*testing.T, []byte) |
| }{ |
| { |
| name: "anthropic_to_openai", |
| from: protocol.Anthropic, |
| to: protocol.OpenAI, |
| model: "gpt-4o", |
| raw: []byte(`{"model":"gpt-4o","system":[{"type":"text","text":"optimize this code"}],"messages":[]}`), |
| assert: assertOpenAISystemPrompt, |
| }, |
| { |
| name: "anthropic_to_gemini", |
| from: protocol.Anthropic, |
| to: protocol.Gemini, |
| model: "gemini-2.5-pro", |
| raw: []byte(`{"model":"gemini-2.5-pro","system":[{"type":"text","text":"optimize this code"}],"messages":[]}`), |
| assert: assertGeminiUserPrompt, |
| }, |
| { |
| name: "anthropic_to_codex", |
| from: protocol.Anthropic, |
| to: protocol.Codex, |
| model: "gpt-5-codex", |
| raw: []byte(`{"model":"gpt-5-codex","system":[{"type":"text","text":"optimize this code"}],"messages":[]}`), |
| assert: assertCodexInstructionsOnly, |
| }, |
| { |
| name: "codex_to_openai", |
| from: protocol.Codex, |
| to: protocol.OpenAI, |
| model: "gpt-4o", |
| raw: []byte(`{"model":"gpt-4o","instructions":"optimize this code"}`), |
| assert: assertOpenAISystemPrompt, |
| }, |
| { |
| name: "codex_to_gemini", |
| from: protocol.Codex, |
| to: protocol.Gemini, |
| model: "gemini-2.5-pro", |
| raw: []byte(`{"model":"gemini-2.5-pro","instructions":"optimize this code"}`), |
| assert: assertGeminiUserPrompt, |
| }, |
| { |
| name: "codex_to_anthropic", |
| from: protocol.Codex, |
| to: protocol.Anthropic, |
| model: "claude-3-5-sonnet", |
| raw: []byte(`{"model":"claude-3-5-sonnet","instructions":"optimize this code"}`), |
| assert: assertAnthropicUserPrompt, |
| }, |
| { |
| name: "gemini_to_openai", |
| from: protocol.Gemini, |
| to: protocol.OpenAI, |
| model: "gpt-4o", |
| raw: []byte(`{"systemInstruction":{"parts":[{"text":"optimize this code"}]}}`), |
| assert: assertOpenAISystemPrompt, |
| }, |
| { |
| name: "gemini_to_anthropic", |
| from: protocol.Gemini, |
| to: protocol.Anthropic, |
| model: "claude-3-5-sonnet", |
| raw: []byte(`{"systemInstruction":{"parts":[{"text":"optimize this code"}]}}`), |
| assert: assertAnthropicUserPrompt, |
| }, |
| { |
| name: "gemini_to_codex", |
| from: protocol.Gemini, |
| to: protocol.Codex, |
| model: "gpt-5-codex", |
| raw: []byte(`{"systemInstruction":{"parts":[{"text":"optimize this code"}]}}`), |
| assert: assertCodexInstructionsOnly, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| got, err := reg.TranslateRequest(tt.from, tt.to, tt.model, tt.raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| tt.assert(t, got) |
| }) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToAnthropic_StringStreamAccepted(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"claude-3-5-sonnet","messages":[{"role":"user","content":"hello"}],"stream":"true","tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}],"tool_choice":"auto"}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Anthropic, "claude-3-5-sonnet", raw, true) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"stream":true`) { |
| t.Fatalf("expected anthropic stream=true, got %s", got) |
| } |
| if !strings.Contains(string(got), `"name":"get_current_weather"`) || !strings.Contains(string(got), `"type":"auto"`) { |
| t.Fatalf("unexpected translated tool payload: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_AnthropicToOpenAI(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"claude-3-5-sonnet","messages":[{"role":"user","content":"hello"}]}`) |
| translatedReq := []byte(`{"model":"claude-3-5-sonnet","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) |
| rawResp := []byte(`{"id":"msg_1","type":"message","role":"assistant","content":[{"type":"text","text":"world"}],"model":"claude-3-5-sonnet","stop_reason":"end_turn","usage":{"input_tokens":3,"output_tokens":5}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Anthropic, protocol.OpenAI, "claude-3-5-sonnet", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"object":"chat.completion"`) || !strings.Contains(string(got), `"content":"world"`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_AnthropicSSEToOpenAI(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"mimo-v2.5","messages":[{"role":"user","content":"hello"}]}`) |
| translatedReq := []byte(`{"model":"mimo-v2.5","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}],"stream":false}`) |
| rawResp := []byte( |
| "event: message_start\n" + |
| "data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"mimo-v2.5\",\"usage\":{\"input_tokens\":3,\"cache_read_input_tokens\":2,\"cache_creation_input_tokens\":1,\"output_tokens\":0}}}\n\n" + |
| "event: content_block_start\n" + |
| "data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n\n" + |
| "event: content_block_delta\n" + |
| "data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"think\"}}\n\n" + |
| "event: content_block_stop\n" + |
| "data: {\"type\":\"content_block_stop\",\"index\":0}\n\n" + |
| "event: content_block_start\n" + |
| "data: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n" + |
| "event: content_block_delta\n" + |
| "data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"hello\"}}\n\n" + |
| "event: message_delta\n" + |
| "data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}\n\n" + |
| "event: message_stop\n" + |
| "data: {\"type\":\"message_stop\"}\n\n", |
| ) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Anthropic, protocol.OpenAI, "mimo-v2.5", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| body := string(got) |
| if strings.Contains(body, "event:") { |
| t.Fatalf("expected OpenAI JSON, got raw SSE: %s", body) |
| } |
| if !strings.Contains(body, `"object":"chat.completion"`) || !strings.Contains(body, `"content":"hello"`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| if !strings.Contains(body, `"reasoning_content":"think"`) { |
| t.Fatalf("expected reasoning_content from thinking SSE, got %s", got) |
| } |
| if !strings.Contains(body, `"prompt_tokens":6`) || !strings.Contains(body, `"completion_tokens":5`) { |
| t.Fatalf("unexpected usage payload: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_AnthropicToOpenAI(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gpt-4o","system":[{"type":"text","text":"be careful"}],"tools":[{"name":"search","description":"lookup","input_schema":{"type":"object"}}],"tool_choice":{"type":"tool","name":"search"},"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"search","input":{"query":"go"}}]},{"role":"user","content":[{"type":"text","text":"hello"},{"type":"image","source":{"type":"url","url":"https://example.com/a.png","media_type":"image/png"}},{"type":"document","source":{"type":"base64","media_type":"application/pdf","data":"cGRm"},"title":"doc.pdf"},{"type":"tool_result","tool_use_id":"toolu_1","content":"done"}]}],"stream":true}`) |
| got, err := reg.TranslateRequest(protocol.Anthropic, protocol.OpenAI, "gpt-4o", raw, true) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"role":"system"`) || !strings.Contains(string(got), `"be careful"`) { |
| t.Fatalf("expected openai system message, got %s", got) |
| } |
| if !strings.Contains(string(got), `"tool_calls":[{"id":"toolu_1","type":"function","function":{"name":"search","arguments":"{\"query\":\"go\"}"}}]`) { |
| t.Fatalf("expected assistant tool_calls, got %s", got) |
| } |
| if !strings.Contains(string(got), `"type":"image_url"`) || !strings.Contains(string(got), `"type":"file"`) || !strings.Contains(string(got), `"role":"tool"`) { |
| t.Fatalf("unexpected translated openai request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_OpenAIToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) |
| translatedReq := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}`) |
| rawResp := []byte(`{"id":"chatcmpl_1","object":"chat.completion","created":0,"model":"gpt-4o","choices":[{"index":0,"message":{"role":"assistant","content":[{"type":"text","text":"world"}],"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{\"query\":\"go\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":3,"completion_tokens":5,"total_tokens":8}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "gpt-4o", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"type":"message"`) || !strings.Contains(string(got), `"type":"tool_use"`) || !strings.Contains(string(got), `"stop_reason":"tool_use"`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_OpenAIToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}],"stream":true}`) |
| translatedReq := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}],"stream":true}`) |
|
|
| var state any |
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "gpt-4o", rawReq, translatedReq, []byte("data: {\"id\":\"chatcmpl_1\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hello\"},\"finish_reason\":null}]}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| joined := string(bytes.Join(chunks, nil)) |
| if !strings.Contains(joined, "event: message_start") || !strings.Contains(joined, "event: content_block_delta") || !strings.Contains(joined, `"text":"hello"`) { |
| t.Fatalf("unexpected translated stream chunks: %#v", chunks) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "gpt-4o", rawReq, translatedReq, []byte("data: [DONE]\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| doneJoined := string(bytes.Join(done, nil)) |
| if !strings.Contains(doneJoined, "event: message_delta") || !strings.Contains(doneJoined, "event: message_stop") { |
| t.Fatalf("unexpected anthropic done chunks: %#v", done) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_OpenAIToAnthropic_DoneAfterFinishedChunkEmitsNothing(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| var state any |
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte("data: {\"id\":\"chatcmpl_1\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hello\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":3,\"completion_tokens\":5,\"total_tokens\":8}}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream finished chunk failed: %v", err) |
| } |
| joined := string(bytes.Join(chunks, nil)) |
| if !strings.Contains(joined, "event: message_stop") { |
| t.Fatalf("expected finished chunk to emit message_stop, got %#v", chunks) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte("data: [DONE]\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| if done != nil { |
| t.Fatalf("expected DONE sentinel to emit nothing after finished chunk, got %#v", done) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_CodexToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"claude-3-5-sonnet","instructions":"be careful","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| got, err := reg.TranslateRequest(protocol.Codex, protocol.Anthropic, "claude-3-5-sonnet", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| var req struct { |
| System []map[string]any `json:"system"` |
| Messages []struct { |
| Role string `json:"role"` |
| Content []map[string]any `json:"content"` |
| } `json:"messages"` |
| } |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal translated request: %v", err) |
| } |
| if len(req.System) != 1 || req.System[0]["text"] != "be careful" { |
| t.Fatalf("expected anthropic system field, got %+v", req.System) |
| } |
| if len(req.Messages) != 1 || req.Messages[0].Role != "user" || len(req.Messages[0].Content) != 1 || req.Messages[0].Content[0]["type"] != "text" || req.Messages[0].Content[0]["text"] != "hello" { |
| t.Fatalf("unexpected translated request: %+v", req) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_CodexBareMessageToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"claude-3-5-sonnet","input":[{"role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| got, err := reg.TranslateRequest(protocol.Codex, protocol.Anthropic, "claude-3-5-sonnet", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| var req struct { |
| Messages []struct { |
| Role string `json:"role"` |
| Content []map[string]any `json:"content"` |
| } `json:"messages"` |
| } |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal translated request: %v", err) |
| } |
| if len(req.Messages) != 1 || req.Messages[0].Role != "user" || len(req.Messages[0].Content) != 1 || req.Messages[0].Content[0]["type"] != "text" || req.Messages[0].Content[0]["text"] != "hello" { |
| t.Fatalf("unexpected translated request: %+v", req) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_AnthropicToCodex(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"claude-3-5-sonnet","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| translatedReq := []byte(`{"model":"claude-3-5-sonnet","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) |
| rawResp := []byte(`{"id":"msg_1","type":"message","role":"assistant","content":[{"type":"text","text":"world"}],"model":"claude-3-5-sonnet","stop_reason":"end_turn","usage":{"input_tokens":3,"output_tokens":5}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Anthropic, protocol.Codex, "claude-3-5-sonnet", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"object":"response"`) || !strings.Contains(string(got), `"text":"world"`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_AnthropicToCodex(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gpt-5-codex","system":[{"type":"text","text":"be careful"}],"tools":[{"name":"search","description":"lookup","input_schema":{"type":"object"}}],"tool_choice":{"type":"tool","name":"search"},"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"search","input":{"query":"go"}}]},{"role":"user","content":[{"type":"text","text":"hello"},{"type":"image","source":{"type":"url","url":"https://example.com/a.png","media_type":"image/png"}},{"type":"document","source":{"type":"base64","media_type":"application/pdf","data":"cGRm"},"title":"doc.pdf"},{"type":"tool_result","tool_use_id":"toolu_1","content":"done"}]}],"stream":true}`) |
| got, err := reg.TranslateRequest(protocol.Anthropic, protocol.Codex, "gpt-5-codex", raw, true) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"instructions":"be careful"`) { |
| t.Fatalf("expected codex instructions, got %s", got) |
| } |
| if !strings.Contains(string(got), `"type":"function_call"`) || !strings.Contains(string(got), `"type":"function_call_output"`) { |
| t.Fatalf("expected codex tool items, got %s", got) |
| } |
| if !strings.Contains(string(got), `"type":"input_image"`) || !strings.Contains(string(got), `"type":"input_file"`) { |
| t.Fatalf("unexpected translated codex request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_AnthropicToCodex_StringToolArguments(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gpt-5-codex","messages":[{"role":"assistant","content":[{"type":"tool_use","id":"call_skill_1","name":"Skill","input":{"skill":"superpowers:using-superpowers","args":""}}]}]}`) |
| got, err := reg.TranslateRequest(protocol.Anthropic, protocol.Codex, "gpt-5-codex", raw, true) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
|
|
| var req struct { |
| Input []map[string]any `json:"input"` |
| } |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal codex request: %v", err) |
| } |
| if len(req.Input) != 1 { |
| t.Fatalf("unexpected codex input: %+v", req.Input) |
| } |
| if req.Input[0]["type"] != "function_call" || req.Input[0]["call_id"] != "call_skill_1" || req.Input[0]["name"] != "Skill" { |
| t.Fatalf("unexpected codex function_call: %+v", req.Input[0]) |
| } |
| if req.Input[0]["arguments"] != `{"skill":"superpowers:using-superpowers","args":""}` && req.Input[0]["arguments"] != `{"args":"","skill":"superpowers:using-superpowers"}` { |
| t.Fatalf("expected codex string arguments, got %+v", req.Input[0]["arguments"]) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_CodexToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-5-codex","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) |
| translatedReq := []byte(`{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| rawResp := []byte(`{"id":"resp_1","object":"response","status":"completed","model":"gpt-5-codex","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"world"}]},{"type":"function_call","call_id":"call_1","name":"search","arguments":{"query":"go"}}],"usage":{"input_tokens":3,"output_tokens":5,"total_tokens":8}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Codex, protocol.Anthropic, "gpt-5-codex", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"type":"message"`) || !strings.Contains(string(got), `"type":"tool_use"`) || !strings.Contains(string(got), `"stop_reason":"tool_use"`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_CodexToAnthropic_StringArguments(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-5-codex","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}]}`) |
| translatedReq := []byte(`{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| rawResp := []byte(`{"id":"resp_1","object":"response","status":"completed","model":"gpt-5-codex","output":[{"type":"function_call","call_id":"call_skill_1","name":"Skill","arguments":"{\"args\":\"skill: \\\"superpowers:using-superpowers\\\"\",\"skill\":\"superpowers:using-superpowers\"}"}],"usage":{"input_tokens":3,"output_tokens":5,"total_tokens":8}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Codex, protocol.Anthropic, "gpt-5-codex", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
|
|
| var payload map[string]any |
| if err := json.Unmarshal(got, &payload); err != nil { |
| t.Fatalf("unmarshal anthropic payload: %v", err) |
| } |
| content := payload["content"].([]any) |
| toolUse := content[0].(map[string]any) |
| input := toolUse["input"].(map[string]any) |
| if toolUse["type"] != "tool_use" || input["args"] != `skill: "superpowers:using-superpowers"` || input["skill"] != "superpowers:using-superpowers" { |
| t.Fatalf("expected anthropic tool_use input object, got %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_CodexToOpenAI_StringArguments(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawResp := []byte(`{"id":"resp_1","object":"response","status":"completed","model":"gpt-5-codex","output":[{"type":"function_call","call_id":"call_skill_1","name":"Skill","arguments":"{\"args\":\"skill: \\\"superpowers:using-superpowers\\\"\",\"skill\":\"superpowers:using-superpowers\"}"}],"usage":{"input_tokens":3,"output_tokens":5,"total_tokens":8}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", nil, nil, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
|
|
| payload := mustJSONMap(t, got) |
| choices := mustSlice(t, payload["choices"]) |
| choice := mustMap(t, choices[0]) |
| message := mustMap(t, choice["message"]) |
| toolCalls := mustSlice(t, message["tool_calls"]) |
| toolCall := mustMap(t, toolCalls[0]) |
| function := mustMap(t, toolCall["function"]) |
|
|
| if mustString(t, toolCall["id"]) != "call_skill_1" || |
| mustString(t, toolCall["type"]) != "function" || |
| mustString(t, function["name"]) != "Skill" || |
| mustString(t, function["arguments"]) != `{"args":"skill: \"superpowers:using-superpowers\"","skill":"superpowers:using-superpowers"}` || |
| mustString(t, choice["finish_reason"]) != "tool_calls" { |
| t.Fatalf("expected openai tool arguments raw json string, got %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_CodexToGemini_StringArguments(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawResp := []byte(`{"id":"resp_1","object":"response","status":"completed","model":"gpt-5-codex","output":[{"type":"function_call","call_id":"call_skill_1","name":"Skill","arguments":"{\"args\":\"skill: \\\"superpowers:using-superpowers\\\"\",\"skill\":\"superpowers:using-superpowers\"}"}],"usage":{"input_tokens":3,"output_tokens":5,"total_tokens":8}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Codex, protocol.Gemini, "gemini-2.5-pro", nil, nil, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
|
|
| var payload map[string]any |
| if err := json.Unmarshal(got, &payload); err != nil { |
| t.Fatalf("unmarshal gemini payload: %v", err) |
| } |
| candidates := payload["candidates"].([]any) |
| content := candidates[0].(map[string]any)["content"].(map[string]any) |
| parts := content["parts"].([]any) |
| functionCall := parts[0].(map[string]any)["functionCall"].(map[string]any) |
| args := functionCall["args"].(map[string]any) |
| if functionCall["name"] != "Skill" || args["args"] != `skill: "superpowers:using-superpowers"` || args["skill"] != "superpowers:using-superpowers" { |
| t.Fatalf("expected gemini functionCall args object, got %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseStream_CodexToAnthropic(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-5-codex","messages":[{"role":"user","content":[{"type":"text","text":"hello"}]}],"stream":true}`) |
| translatedReq := []byte(`{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}],"stream":true}`) |
|
|
| var state any |
| chunks, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Anthropic, "gpt-5-codex", rawReq, translatedReq, []byte("event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"hello\"}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| joined := string(bytes.Join(chunks, nil)) |
| if !strings.Contains(joined, "event: message_start") || !strings.Contains(joined, "event: content_block_delta") || !strings.Contains(joined, `"text":"hello"`) { |
| t.Fatalf("unexpected translated stream chunks: %#v", chunks) |
| } |
|
|
| done, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Anthropic, "gpt-5-codex", rawReq, translatedReq, []byte("event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"model\":\"gpt-5-codex\",\"status\":\"completed\",\"usage\":{\"input_tokens\":3,\"output_tokens\":5,\"total_tokens\":8}}}\n\n"), &state) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream done failed: %v", err) |
| } |
| doneJoined := string(bytes.Join(done, nil)) |
| if !strings.Contains(doneJoined, "event: message_delta") || !strings.Contains(doneJoined, "event: message_stop") { |
| t.Fatalf("unexpected anthropic done chunks: %#v", done) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToCodex(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gpt-5-codex","messages":[{"role":"system","content":"be careful"},{"role":"user","content":[{"type":"text","text":"hello"},{"type":"image_url","image_url":{"url":"https://example.com/a.png","detail":"high"}}]}]}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Codex, "gpt-5-codex", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"instructions":"be careful"`) { |
| t.Fatalf("expected codex instructions, got %s", got) |
| } |
| if !strings.Contains(string(got), `"type":"input_image"`) || !strings.Contains(string(got), `"image_url":"https://example.com/a.png"`) { |
| t.Fatalf("unexpected translated codex request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToCodex_BuiltinWebSearch(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gpt-5-codex","tools":[{"type":"web_search","search_context_size":"high"}],"tool_choice":{"type":"web_search"},"messages":[{"role":"user","content":"hello"}]}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Codex, "gpt-5-codex", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| var req struct { |
| Tools []map[string]any `json:"tools"` |
| ToolChoice map[string]any `json:"tool_choice"` |
| } |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal translated request: %v", err) |
| } |
| if len(req.Tools) != 1 || req.Tools[0]["type"] != "web_search" || req.Tools[0]["search_context_size"] != "high" { |
| t.Fatalf("unexpected builtin tools: %+v", req.Tools) |
| } |
| if req.ToolChoice["type"] != "web_search" { |
| t.Fatalf("unexpected builtin tool choice: %+v", req.ToolChoice) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_CodexToOpenAI(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}`) |
| translatedReq := []byte(`{"model":"gpt-4o","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| rawResp := []byte(`{"id":"resp_1","object":"response","status":"completed","model":"gpt-4o","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"world"}]}],"usage":{"input_tokens":3,"output_tokens":5,"total_tokens":8}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"object":"chat.completion"`) || !strings.Contains(string(got), `"content":"world"`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_CodexToOpenAI_ReasoningAndUsageDetails(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawResp := []byte(`{"id":"resp_1","object":"response","status":"completed","model":"gpt-4o","output":[{"type":"reasoning","content":[{"type":"reasoning_text","text":"step by step"}],"encrypted_content":"enc_1"},{"type":"message","role":"assistant","content":[{"type":"output_text","text":"world"}]}],"usage":{"input_tokens":21,"input_tokens_details":{"cached_tokens":7},"output_tokens":5,"output_tokens_details":{"reasoning_tokens":13},"cache_creation_input_tokens":11,"total_tokens":26}}`) |
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.Codex, protocol.OpenAI, "gpt-4o", nil, nil, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| body := string(got) |
| if !strings.Contains(body, `"reasoning_content":"step by step"`) || !strings.Contains(body, `"encrypted_content":"enc_1"`) { |
| t.Fatalf("unexpected reasoning translation: %s", got) |
| } |
| if !strings.Contains(body, `"cached_tokens":7`) || !strings.Contains(body, `"cache_creation_input_tokens":11`) || !strings.Contains(body, `"reasoning_tokens":13`) { |
| t.Fatalf("unexpected usage translation: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_CodexToOpenAI(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gpt-4o","instructions":"be careful","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"},{"type":"input_file","file_id":"file_123","filename":"doc.pdf"}]},{"type":"function_call_output","call_id":"call_1","output":"done"}]}`) |
| got, err := reg.TranslateRequest(protocol.Codex, protocol.OpenAI, "gpt-4o", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"role":"system"`) || !strings.Contains(string(got), `"be careful"`) { |
| t.Fatalf("expected system message, got %s", got) |
| } |
| if !strings.Contains(string(got), `"type":"file"`) || !strings.Contains(string(got), `"role":"tool"`) { |
| t.Fatalf("unexpected translated openai request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_CodexToOpenAI_BuiltinWebSearch(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gpt-4o","tools":[{"type":"web_search","user_location":{"type":"approximate","country":"US"}}],"tool_choice":{"type":"web_search"},"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| got, err := reg.TranslateRequest(protocol.Codex, protocol.OpenAI, "gpt-4o", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| var req struct { |
| Tools []map[string]any `json:"tools"` |
| ToolChoice map[string]any `json:"tool_choice"` |
| } |
| if err := json.Unmarshal(got, &req); err != nil { |
| t.Fatalf("unmarshal translated request: %v", err) |
| } |
| if len(req.Tools) != 1 || req.Tools[0]["type"] != "web_search" { |
| t.Fatalf("unexpected builtin tools: %+v", req.Tools) |
| } |
| location, ok := req.Tools[0]["user_location"].(map[string]any) |
| if !ok || location["country"] != "US" || location["type"] != "approximate" { |
| t.Fatalf("unexpected builtin tool options: %+v", req.Tools[0]) |
| } |
| if req.ToolChoice["type"] != "web_search" { |
| t.Fatalf("unexpected builtin tool choice: %+v", req.ToolChoice) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_OpenAIToCodex(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-5-codex","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`) |
| translatedReq := []byte(`{"model":"gpt-5-codex","messages":[{"role":"user","content":"hello"}]}`) |
| rawResp := []byte(`{"id":"chatcmpl_1","object":"chat.completion","created":0,"model":"gpt-5-codex","choices":[{"index":0,"message":{"role":"assistant","content":"world"},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":5,"total_tokens":8}}`) |
|
|
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.OpenAI, protocol.Codex, "gpt-5-codex", rawReq, translatedReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"object":"response"`) || !strings.Contains(string(got), `"type":"output_text"`) || !strings.Contains(string(got), `"text":"world"`) { |
| t.Fatalf("unexpected translated response: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_OpenAIToCodex_ReasoningAndUsageDetails(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawResp := []byte(`{"id":"chatcmpl_1","object":"chat.completion","created":0,"model":"gpt-5-codex","choices":[{"index":0,"message":{"role":"assistant","content":"world","reasoning_content":"step by step","reasoning":[{"type":"reasoning","text":"step by step","encrypted_content":"enc_1"}]},"finish_reason":"stop"}],"usage":{"prompt_tokens":21,"prompt_tokens_details":{"cached_tokens":7},"completion_tokens":5,"completion_tokens_details":{"reasoning_tokens":13},"cache_creation_input_tokens":11,"total_tokens":26}}`) |
| got, err := reg.TranslateResponseNonStream(context.Background(), protocol.OpenAI, protocol.Codex, "gpt-5-codex", nil, nil, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| body := string(got) |
| if !strings.Contains(body, `"type":"reasoning"`) || !strings.Contains(body, `"type":"reasoning_text"`) || !strings.Contains(body, `"text":"step by step"`) || !strings.Contains(body, `"encrypted_content":"enc_1"`) { |
| t.Fatalf("unexpected reasoning translation: %s", got) |
| } |
| if !strings.Contains(body, `"cached_tokens":7`) || !strings.Contains(body, `"cache_creation_input_tokens":11`) || !strings.Contains(body, `"reasoning_tokens":13`) { |
| t.Fatalf("unexpected usage translation: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToGemini_SupportsStructuredContent(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","tools":[{"type":"function","function":{"name":"search","parameters":{"type":"object"}}}],"tool_choice":"required","messages":[{"role":"assistant","content":[{"type":"text","text":"calling"}],"tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{\"query\":\"go\"}"}}]},{"role":"tool","tool_call_id":"call_1","content":"done"},{"role":"user","content":[{"type":"text","text":"hello"},{"type":"image_url","image_url":{"url":"https://example.com/a.png"}}]}]}`) |
| got, err := reg.TranslateRequest(protocol.OpenAI, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"functionDeclarations"`) || !strings.Contains(string(got), `"functionCall"`) || !strings.Contains(string(got), `"functionResponse"`) || !strings.Contains(string(got), `"fileUri":"https://example.com/a.png"`) { |
| t.Fatalf("unexpected translated gemini request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_AnthropicToGemini_SupportsStructuredContent(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"lookup","input":{"query":"go"}}]},{"role":"user","content":[{"type":"text","text":"hello"},{"type":"image","source":{"type":"url","url":"https://example.com/a.png","media_type":"image/png"}},{"type":"document","source":{"type":"base64","media_type":"application/pdf","data":"cGRm"},"title":"doc.pdf"},{"type":"tool_result","tool_use_id":"toolu_1","content":"done"}]}]}`) |
| got, err := reg.TranslateRequest(protocol.Anthropic, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"functionCall"`) || !strings.Contains(string(got), `"functionResponse"`) || !strings.Contains(string(got), `"inlineData"`) || !strings.Contains(string(got), `"fileUri":"https://example.com/a.png"`) { |
| t.Fatalf("unexpected translated gemini request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_CodexToGemini_SupportsStructuredContent(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"},{"type":"input_image","image_url":"https://example.com/a.png"},{"type":"input_file","file_id":"file_123","filename":"doc.pdf"}]},{"type":"function_call","call_id":"call_1","name":"tool","arguments":{"q":"go"}},{"type":"function_call_output","call_id":"call_1","output":"done"}]}`) |
| got, err := reg.TranslateRequest(protocol.Codex, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if !strings.Contains(string(got), `"functionCall"`) || !strings.Contains(string(got), `"functionResponse"`) || !strings.Contains(string(got), `"fileUri":"https://example.com/a.png"`) || !strings.Contains(string(got), `"fileUri":"file_123"`) { |
| t.Fatalf("unexpected translated gemini request: %s", got) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToGemini_RejectsUnknownStructuredContent(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":[{"type":"mystery","value":true}]}]}`) |
| _, err := reg.TranslateRequest(protocol.OpenAI, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err == nil { |
| t.Fatal("expected unsupported structured content error") |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_AnthropicToGemini_RejectsUnknownStructuredContent(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":[{"type":"mystery","value":true}]}]}`) |
| _, err := reg.TranslateRequest(protocol.Anthropic, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err == nil { |
| t.Fatal("expected unsupported structured content error") |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_CodexToGemini_RejectsUnknownStructuredContent(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| raw := []byte(`{"model":"gemini-2.5-pro","input":[{"type":"message","role":"user","content":[{"type":"mystery","value":true}]}]}`) |
| _, err := reg.TranslateRequest(protocol.Codex, protocol.Gemini, "gemini-2.5-pro", raw, false) |
| if err == nil { |
| t.Fatal("expected unsupported codex input error") |
| } |
| } |
|
|
| func TestBuildTransformPlan_SupportedTransformDefaults(t *testing.T) { |
| plan, err := protocol.BuildTransformPlan( |
| protocol.OpenAI, |
| protocol.Gemini, |
| "/v1/chat/completions", |
| "", |
| []byte(`{"model":"alias-model"}`), |
| nil, |
| "alias-model", |
| "", |
| true, |
| ) |
| if err != nil { |
| t.Fatalf("BuildTransformPlan failed: %v", err) |
| } |
| if !plan.NeedsTransform { |
| t.Fatal("expected transform plan to require protocol translation") |
| } |
| if got := string(plan.TranslatedBody); got != `{"model":"alias-model"}` { |
| t.Fatalf("expected prepared body to default to original body, got %s", got) |
| } |
| if got := plan.UpstreamPath; got != "/v1/chat/completions" { |
| t.Fatalf("expected upstream path to default to original path, got %s", got) |
| } |
| if got := plan.RequestModel(); got != "alias-model" { |
| t.Fatalf("expected request model alias-model, got %s", got) |
| } |
| if plan.RequestFamily != protocol.RequestFamilyChatCompletions { |
| t.Fatalf("expected chat_completions family, got %s", plan.RequestFamily) |
| } |
| } |
|
|
| func TestBuildTransformPlan_SupportsAnthropicToOpenAI(t *testing.T) { |
| plan, err := protocol.BuildTransformPlan( |
| protocol.Anthropic, |
| protocol.OpenAI, |
| "/v1/messages", |
| "", |
| []byte(`{"model":"gpt-4o"}`), |
| nil, |
| "gpt-4o", |
| "", |
| false, |
| ) |
| if err != nil { |
| t.Fatalf("BuildTransformPlan failed: %v", err) |
| } |
| if !plan.NeedsTransform || plan.RequestFamily != protocol.RequestFamilyMessages { |
| t.Fatalf("unexpected plan: %+v", plan) |
| } |
| } |
|
|
| func TestBuildTransformPlan_SupportsAnthropicToCodex(t *testing.T) { |
| plan, err := protocol.BuildTransformPlan( |
| protocol.Anthropic, |
| protocol.Codex, |
| "/v1/messages", |
| "", |
| []byte(`{"model":"gpt-5-codex"}`), |
| nil, |
| "gpt-5-codex", |
| "", |
| true, |
| ) |
| if err != nil { |
| t.Fatalf("BuildTransformPlan failed: %v", err) |
| } |
| if !plan.NeedsTransform || plan.RequestFamily != protocol.RequestFamilyMessages { |
| t.Fatalf("unexpected plan: %+v", plan) |
| } |
| } |
|
|
| func TestBuildTransformPlan_RejectsUnsupportedTransform(t *testing.T) { |
| _, err := protocol.BuildTransformPlan( |
| protocol.Gemini, |
| protocol.OpenAI, |
| "/v1/messages", |
| "/v1/messages", |
| []byte(`{"model":"gpt-4o"}`), |
| []byte(`{"model":"gpt-4o"}`), |
| "gpt-4o", |
| "gpt-4o", |
| false, |
| ) |
| if err == nil { |
| t.Fatal("expected unsupported transform error") |
| } |
| } |
|
|
| func TestBuildTransformPlan_SameProtocolNoOp(t *testing.T) { |
| t.Parallel() |
|
|
| plan, err := protocol.BuildTransformPlan( |
| protocol.Gemini, |
| protocol.Gemini, |
| "/v1beta/models/gemini-2.5-pro:generateContent", |
| "", |
| []byte(`{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}`), |
| nil, |
| "gemini-2.5-pro", |
| "", |
| true, |
| ) |
| if err != nil { |
| t.Fatalf("BuildTransformPlan failed: %v", err) |
| } |
| if plan.NeedsTransform { |
| t.Fatalf("expected same-protocol plan to skip translation, got %+v", plan) |
| } |
| if plan.RequestFamily != protocol.RequestFamilyGenerateContent { |
| t.Fatalf("expected generate_content family, got %s", plan.RequestFamily) |
| } |
| if got := string(plan.TranslatedBody); got != `{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}` { |
| t.Fatalf("expected translated body to reuse original body, got %s", got) |
| } |
| } |
|
|
| func TestBuildTransformPlan_SupportsOpenAIToCodex(t *testing.T) { |
| plan, err := protocol.BuildTransformPlan( |
| protocol.OpenAI, |
| protocol.Codex, |
| "/v1/chat/completions", |
| "", |
| []byte(`{"model":"gpt-5-codex"}`), |
| nil, |
| "gpt-5-codex", |
| "", |
| false, |
| ) |
| if err != nil { |
| t.Fatalf("BuildTransformPlan failed: %v", err) |
| } |
| if !plan.NeedsTransform || plan.RequestFamily != protocol.RequestFamilyChatCompletions { |
| t.Fatalf("unexpected plan: %+v", plan) |
| } |
| } |
|
|
| func TestBuildTransformPlan_SupportsCodexToOpenAI(t *testing.T) { |
| plan, err := protocol.BuildTransformPlan( |
| protocol.Codex, |
| protocol.OpenAI, |
| "/v1/responses", |
| "", |
| []byte(`{"model":"gpt-4o"}`), |
| nil, |
| "gpt-4o", |
| "", |
| false, |
| ) |
| if err != nil { |
| t.Fatalf("BuildTransformPlan failed: %v", err) |
| } |
| if !plan.NeedsTransform || plan.RequestFamily != protocol.RequestFamilyResponses { |
| t.Fatalf("unexpected plan: %+v", plan) |
| } |
| } |
|
|
| func TestBuildTransformPlan_SupportsCodexToAnthropicWithBasePathPrefix(t *testing.T) { |
| plan, err := protocol.BuildTransformPlan( |
| protocol.Codex, |
| protocol.Anthropic, |
| "/anthropic/v1/responses", |
| "/anthropic/v1/messages", |
| []byte(`{"model":"claude-3-5-sonnet"}`), |
| nil, |
| "claude-3-5-sonnet", |
| "", |
| false, |
| ) |
| if err != nil { |
| t.Fatalf("BuildTransformPlan failed: %v", err) |
| } |
| if !plan.NeedsTransform || plan.RequestFamily != protocol.RequestFamilyResponses { |
| t.Fatalf("unexpected plan: %+v", plan) |
| } |
| } |
|
|
| func TestBuildTransformPlan_RejectsUnsupportedFamilyForSupportedPair(t *testing.T) { |
| _, err := protocol.BuildTransformPlan( |
| protocol.OpenAI, |
| protocol.Codex, |
| "/v1/embeddings", |
| "", |
| []byte(`{"model":"gpt-5-codex"}`), |
| nil, |
| "gpt-5-codex", |
| "", |
| false, |
| ) |
| if err == nil { |
| t.Fatal("expected unsupported transform error") |
| } |
| } |
|
|
| func TestTransformPlan_ResponseModelPreservesClientAlias(t *testing.T) { |
| plan, err := protocol.BuildTransformPlan( |
| protocol.OpenAI, |
| protocol.Gemini, |
| "/v1/chat/completions", |
| "/v1beta/models/gemini-2.5-pro:generateContent", |
| []byte(`{"model":"alias-model"}`), |
| []byte(`{"model":"gemini-2.5-pro"}`), |
| "alias-model", |
| "gemini-2.5-pro", |
| false, |
| ) |
| if err != nil { |
| t.Fatalf("BuildTransformPlan failed: %v", err) |
| } |
| if got := plan.RequestModel(); got != "gemini-2.5-pro" { |
| t.Fatalf("expected request model gemini-2.5-pro, got %s", got) |
| } |
| if got := plan.ResponseModel(); got != "alias-model" { |
| t.Fatalf("expected response model alias-model, got %s", got) |
| } |
| } |
|
|
| func TestRegistry_SameProtocolNoOp(t *testing.T) { |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| rawReq := []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}`) |
| gotReq, err := reg.TranslateRequest(protocol.OpenAI, protocol.OpenAI, "gpt-4o", rawReq, false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| if string(gotReq) != string(rawReq) { |
| t.Fatalf("expected same request body, got %s", gotReq) |
| } |
|
|
| rawResp := []byte(`{"ok":true}`) |
| gotResp, err := reg.TranslateResponseNonStream(context.Background(), protocol.OpenAI, protocol.OpenAI, "gpt-4o", rawReq, rawReq, rawResp) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| if string(gotResp) != string(rawResp) { |
| t.Fatalf("expected same response body, got %s", gotResp) |
| } |
|
|
| gotStream, err := reg.TranslateResponseStream(context.Background(), protocol.OpenAI, protocol.OpenAI, "gpt-4o", rawReq, rawReq, []byte("data: [DONE]\n\n"), nil) |
| if err != nil { |
| t.Fatalf("TranslateResponseStream failed: %v", err) |
| } |
| if len(gotStream) != 1 || string(gotStream[0]) != "data: [DONE]\n\n" { |
| t.Fatalf("unexpected no-op stream chunks: %#v", gotStream) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_OpenAIToAnthropic_ToolCalls(t *testing.T) { |
| t.Parallel() |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| |
| req := `{ |
| "model": "claude-3-5-sonnet", |
| "messages": [ |
| {"role": "user", "content": "what is weather in Beijing?"}, |
| {"role": "assistant", "content": null, "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Beijing\"}"}}]}, |
| {"role": "tool", "tool_call_id": "call_1", "content": "sunny, 25C"} |
| ] |
| }` |
|
|
| out, err := reg.TranslateRequest(protocol.OpenAI, protocol.Anthropic, "claude-3-5-sonnet", []byte(req), false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| result := string(out) |
| |
| if !strings.Contains(result, `"type":"tool_use"`) { |
| t.Fatalf("expected type=tool_use in anthropic request, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"name":"get_weather"`) { |
| t.Fatalf("expected tool name get_weather, got:\n%s", result) |
| } |
| |
| if !strings.Contains(result, `"type":"tool_result"`) { |
| t.Fatalf("expected type=tool_result in anthropic request, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"tool_use_id":"call_1"`) { |
| t.Fatalf("expected tool_use_id=call_1, got:\n%s", result) |
| } |
| } |
|
|
| func TestRegistry_TranslateRequest_AnthropicToOpenAI_ToolCalls(t *testing.T) { |
| t.Parallel() |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| |
| req := `{ |
| "model": "gpt-4o", |
| "messages": [ |
| {"role": "user", "content": [{"type": "text", "text": "search for cats"}]}, |
| {"role": "assistant", "content": [{"type": "tool_use", "id": "tu_1", "name": "search", "input": {"query": "cats"}}]}, |
| {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tu_1", "content": "many cats found"}]} |
| ] |
| }` |
|
|
| out, err := reg.TranslateRequest(protocol.Anthropic, protocol.OpenAI, "gpt-4o", []byte(req), false) |
| if err != nil { |
| t.Fatalf("TranslateRequest failed: %v", err) |
| } |
| result := string(out) |
| |
| if !strings.Contains(result, `"tool_calls"`) { |
| t.Fatalf("expected tool_calls in openai request, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"name":"search"`) { |
| t.Fatalf("expected tool name search, got:\n%s", result) |
| } |
| |
| if !strings.Contains(result, `"role":"tool"`) { |
| t.Fatalf("expected role=tool, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"tool_call_id":"tu_1"`) { |
| t.Fatalf("expected tool_call_id=tu_1, got:\n%s", result) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_OpenAIToAnthropic_ToolCalls(t *testing.T) { |
| t.Parallel() |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| resp := `{ |
| "id": "chatcmpl-tc1", |
| "object": "chat.completion", |
| "model": "gpt-4o", |
| "choices": [{ |
| "index": 0, |
| "message": { |
| "role": "assistant", |
| "content": null, |
| "tool_calls": [{"id": "call_2", "type": "function", "function": {"name": "lookup", "arguments": "{\"key\":\"val\"}"}}] |
| }, |
| "finish_reason": "tool_calls" |
| }], |
| "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15} |
| }` |
|
|
| out, err := reg.TranslateResponseNonStream(context.Background(), protocol.OpenAI, protocol.Anthropic, "claude-3-5-sonnet", nil, nil, []byte(resp)) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| result := string(out) |
| if !strings.Contains(result, `"type":"tool_use"`) { |
| t.Fatalf("expected type=tool_use, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"name":"lookup"`) { |
| t.Fatalf("expected name=lookup, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"stop_reason":"tool_use"`) { |
| t.Fatalf("expected stop_reason=tool_use, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"key"`) || !strings.Contains(result, `"val"`) { |
| t.Fatalf("expected input args key=val in tool_use block, got:\n%s", result) |
| } |
| } |
|
|
| func TestRegistry_TranslateResponseNonStream_AnthropicToOpenAI_ToolCalls(t *testing.T) { |
| t.Parallel() |
| reg := protocol.NewRegistry() |
| builtin.Register(reg) |
|
|
| resp := `{ |
| "id": "msg_tc1", |
| "type": "message", |
| "role": "assistant", |
| "model": "claude-3-5-sonnet", |
| "content": [ |
| {"type": "tool_use", "id": "tu_2", "name": "calculate", "input": {"expr": "1+1"}} |
| ], |
| "stop_reason": "tool_use", |
| "usage": {"input_tokens": 8, "output_tokens": 4} |
| }` |
|
|
| out, err := reg.TranslateResponseNonStream(context.Background(), protocol.Anthropic, protocol.OpenAI, "gpt-4o", nil, nil, []byte(resp)) |
| if err != nil { |
| t.Fatalf("TranslateResponseNonStream failed: %v", err) |
| } |
| result := string(out) |
| if !strings.Contains(result, `"tool_calls"`) { |
| t.Fatalf("expected tool_calls, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"name":"calculate"`) { |
| t.Fatalf("expected name=calculate, got:\n%s", result) |
| } |
| if !strings.Contains(result, `"finish_reason":"tool_calls"`) { |
| t.Fatalf("expected finish_reason=tool_calls, got:\n%s", result) |
| } |
| if !strings.Contains(result, `1+1`) { |
| t.Fatalf("expected expr=1+1 in function arguments, got:\n%s", result) |
| } |
| } |
|
|
| func TestSupportedClientProtocolsForUpstream_BidirectionalMatrix(t *testing.T) { |
| tests := []struct { |
| upstream protocol.Protocol |
| want []protocol.Protocol |
| }{ |
| {upstream: protocol.OpenAI, want: []protocol.Protocol{protocol.Anthropic, protocol.Codex, protocol.Gemini}}, |
| {upstream: protocol.Anthropic, want: []protocol.Protocol{protocol.Codex, protocol.Gemini, protocol.OpenAI}}, |
| {upstream: protocol.Codex, want: []protocol.Protocol{protocol.Anthropic, protocol.Gemini, protocol.OpenAI}}, |
| {upstream: protocol.Gemini, want: []protocol.Protocol{protocol.Anthropic, protocol.Codex, protocol.OpenAI}}, |
| } |
|
|
| for _, tt := range tests { |
| got := protocol.SupportedClientProtocolsForUpstream(tt.upstream) |
| if len(got) != len(tt.want) { |
| t.Fatalf("upstream %s: expected %v, got %v", tt.upstream, tt.want, got) |
| } |
| for i, want := range tt.want { |
| if got[i] != want { |
| t.Fatalf("upstream %s: expected %v, got %v", tt.upstream, tt.want, got) |
| } |
| } |
| } |
| } |
|
|