package protocol_test import ( "bytes" "context" "encoding/json" "strings" "testing" "ccLoad/internal/protocol" "ccLoad/internal/protocol/builtin" ) func TestRegistry_TranslateRequest_GeminiToCodex(t *testing.T) { t.Parallel() reg := protocol.NewRegistry() builtin.Register(reg) raw := []byte(`{"systemInstruction":{"parts":[{"text":"be careful"}]},"contents":[{"role":"user","parts":[{"text":"hello"}]},{"role":"model","parts":[{"functionCall":{"name":"lookup","args":{"query":"go"}}}]},{"role":"user","parts":[{"functionResponse":{"name":"lookup","response":{"result":"done"}}}]}],"tools":[{"functionDeclarations":[{"name":"lookup","description":"lookup docs","parameters":{"type":"object"}}]}],"toolConfig":{"functionCallingConfig":{"mode":"ANY","allowedFunctionNames":["lookup"]}}}`) got, err := reg.TranslateRequest(protocol.Gemini, protocol.Codex, "gpt-5-codex", raw, true) if err != nil { t.Fatalf("TranslateRequest failed: %v", err) } var req struct { Instructions string `json:"instructions"` Stream bool `json:"stream"` Input []map[string]any `json:"input"` Tools []map[string]any `json:"tools"` ToolChoice map[string]any `json:"tool_choice"` } if err := json.Unmarshal(got, &req); err != nil { t.Fatalf("unmarshal codex request: %v", err) } if req.Instructions != "be careful" || !req.Stream { t.Fatalf("unexpected codex request metadata: %+v", req) } if len(req.Input) != 3 { t.Fatalf("unexpected codex input: %+v", req.Input) } if req.Input[0]["type"] != "message" || req.Input[0]["role"] != "user" { t.Fatalf("unexpected codex message input: %+v", req.Input[0]) } content, ok := req.Input[0]["content"].([]any) if !ok || len(content) != 1 { t.Fatalf("unexpected codex message content: %+v", req.Input[0]["content"]) } part, ok := content[0].(map[string]any) if !ok || part["type"] != "input_text" || part["text"] != "hello" { t.Fatalf("unexpected codex message part: %+v", content[0]) } if req.Input[1]["type"] != "function_call" || req.Input[1]["call_id"] != "call_1" || req.Input[1]["name"] != "lookup" { t.Fatalf("unexpected codex function_call: %+v", req.Input[1]) } args, ok := req.Input[1]["arguments"].(string) if !ok || args != `{"query":"go"}` { t.Fatalf("unexpected codex function_call args: %+v", req.Input[1]["arguments"]) } if req.Input[2]["type"] != "function_call_output" || req.Input[2]["call_id"] != "call_1" || req.Input[2]["name"] != "lookup" || req.Input[2]["output"] != "done" { t.Fatalf("unexpected codex function_call_output: %+v", req.Input[2]) } if len(req.Tools) != 1 || req.Tools[0]["type"] != "function" || req.Tools[0]["name"] != "lookup" || req.Tools[0]["description"] != "lookup docs" { t.Fatalf("unexpected codex tools: %+v", req.Tools) } if req.ToolChoice["type"] != "function" || req.ToolChoice["name"] != "lookup" { t.Fatalf("unexpected codex tool choice: %+v", req.ToolChoice) } } func TestRegistry_TranslateResponseNonStream_CodexToGemini(t *testing.T) { t.Parallel() reg := protocol.NewRegistry() builtin.Register(reg) rawResp := []byte(`{"id":"resp_1","object":"response","status":"completed","model":"gpt-5-codex","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hello"}]},{"type":"function_call","call_id":"call_1","name":"lookup","arguments":{"query":"go"}}],"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 resp struct { Candidates []struct { Content struct { Parts []struct { Text string `json:"text"` FunctionCall struct { Name string `json:"name"` Args map[string]any `json:"args"` } `json:"functionCall"` } `json:"parts"` } `json:"content"` FinishReason string `json:"finishReason"` } `json:"candidates"` ResponseID string `json:"responseId"` ModelVersion string `json:"modelVersion"` UsageMetadata struct { PromptTokenCount int64 `json:"promptTokenCount"` CandidatesTokenCount int64 `json:"candidatesTokenCount"` TotalTokenCount int64 `json:"totalTokenCount"` } `json:"usageMetadata"` } if err := json.Unmarshal(got, &resp); err != nil { t.Fatalf("unmarshal gemini response: %v", err) } if len(resp.Candidates) != 1 || len(resp.Candidates[0].Content.Parts) != 2 { t.Fatalf("unexpected candidates: %+v", resp) } if resp.Candidates[0].Content.Parts[0].Text != "hello" { t.Fatalf("unexpected text part: %+v", resp.Candidates[0].Content.Parts[0]) } if resp.Candidates[0].Content.Parts[1].FunctionCall.Name != "lookup" || resp.Candidates[0].Content.Parts[1].FunctionCall.Args["query"] != "go" { t.Fatalf("unexpected function call part: %+v", resp.Candidates[0].Content.Parts[1]) } if resp.Candidates[0].FinishReason != "STOP" || resp.ResponseID != "resp_1" || resp.ModelVersion != "gemini-2.5-pro" { t.Fatalf("unexpected gemini metadata: %+v", resp) } if resp.UsageMetadata.PromptTokenCount != 3 || resp.UsageMetadata.CandidatesTokenCount != 5 || resp.UsageMetadata.TotalTokenCount != 8 { t.Fatalf("unexpected gemini usage: %+v", resp.UsageMetadata) } } func TestRegistry_TranslateResponseStream_CodexToGemini(t *testing.T) { t.Parallel() reg := protocol.NewRegistry() builtin.Register(reg) var state any textChunk, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Gemini, "gemini-2.5-pro", nil, nil, []byte("event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"hello\"}\n\n"), &state) if err != nil { t.Fatalf("response.output_text.delta failed: %v", err) } if len(textChunk) != 1 || !strings.Contains(string(textChunk[0]), `"text":"hello"`) { t.Fatalf("unexpected gemini text chunk: %#v", textChunk) } toolChunk, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Gemini, "gemini-2.5-pro", nil, nil, []byte("event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"function_call\",\"call_id\":\"call_1\",\"name\":\"lookup\",\"arguments\":{\"query\":\"go\"}}}\n\n"), &state) if err != nil { t.Fatalf("response.output_item.done failed: %v", err) } if len(toolChunk) != 1 || !strings.Contains(string(toolChunk[0]), `"functionCall":{"name":"lookup","args":{"query":"go"}}`) { t.Fatalf("unexpected gemini tool chunk: %#v", toolChunk) } done, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Gemini, "gemini-2.5-pro", nil, nil, []byte("event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5-codex\",\"usage\":{\"input_tokens\":3,\"output_tokens\":5,\"total_tokens\":8}}}\n\n"), &state) if err != nil { t.Fatalf("response.completed failed: %v", err) } if len(done) != 1 || !strings.Contains(string(done[0]), `"promptTokenCount":3`) || !strings.Contains(string(done[0]), `"candidatesTokenCount":5`) || !strings.Contains(string(done[0]), `"finishReason":"STOP"`) { t.Fatalf("unexpected gemini done chunk: %#v", done) } } func TestRegistry_TranslateResponseStream_CodexToGemini_StringArguments(t *testing.T) { t.Parallel() reg := protocol.NewRegistry() builtin.Register(reg) var state any toolChunk, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Gemini, "gemini-2.5-pro", nil, nil, []byte("event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"function_call\",\"call_id\":\"call_skill_1\",\"name\":\"Skill\",\"arguments\":\"{\\\"args\\\":\\\"skill: \\\\\\\"superpowers:using-superpowers\\\\\\\"\\\",\\\"skill\\\":\\\"superpowers:using-superpowers\\\"}\"}}\n\n"), &state) if err != nil { t.Fatalf("response.output_item.done failed: %v", err) } if len(toolChunk) != 1 { t.Fatalf("expected one gemini tool chunk, got %#v", toolChunk) } var payload map[string]any if err := json.Unmarshal(bytes.TrimPrefix(bytes.TrimSpace(toolChunk[0]), []byte("data: ")), &payload); err != nil { t.Fatalf("unmarshal gemini chunk: %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", toolChunk[0]) } } func TestRegistry_TranslateResponseStream_CodexToGemini_CompletionWithoutUsageStillStops(t *testing.T) { t.Parallel() reg := protocol.NewRegistry() builtin.Register(reg) var state any done, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Gemini, "gemini-2.5-pro", nil, nil, []byte("event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5-codex\"}}\n\n"), &state) if err != nil { t.Fatalf("response.completed failed: %v", err) } if len(done) != 1 { t.Fatalf("expected one gemini completion chunk, got %#v", done) } if !strings.Contains(string(done[0]), `"finishReason":"STOP"`) || !strings.Contains(string(done[0]), `"responseId":"resp_1"`) { t.Fatalf("unexpected gemini done chunk: %#v", done) } if strings.Contains(string(done[0]), `"usageMetadata"`) { t.Fatalf("expected completion without usage metadata, got %#v", done) } } func TestRegistry_TranslateResponseStream_CodexToGemini_ReasoningWithText(t *testing.T) { t.Parallel() reg := protocol.NewRegistry() builtin.Register(reg) chunk := "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"reasoning\",\"id\":\"rs_01\",\"summary\":[{\"type\":\"summary_text\",\"text\":\"step by step\"}]}}\n\n" var state any out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Gemini, "gemini-2.5-pro", nil, nil, []byte(chunk), &state) if err != nil { t.Fatalf("stream error: %v", err) } if len(out) == 0 { t.Fatalf("expected gemini text chunk for reasoning, got none") } result := string(bytes.Join(out, nil)) if !strings.Contains(result, `"step by step"`) { t.Fatalf("expected reasoning text in output, got:\n%s", result) } } func TestRegistry_TranslateResponseStream_CodexToGemini_ReasoningEmpty(t *testing.T) { t.Parallel() reg := protocol.NewRegistry() builtin.Register(reg) // reasoning item 无 summary text(空 summary 数组) chunk := "event: response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"reasoning\",\"id\":\"rs_02\",\"summary\":[]}}\n\n" var state any out, err := reg.TranslateResponseStream(context.Background(), protocol.Codex, protocol.Gemini, "gemini-2.5-pro", nil, nil, []byte(chunk), &state) if err != nil { t.Fatalf("stream error: %v", err) } // 空 reasoning → 静默忽略,无输出 if len(out) != 0 { t.Fatalf("expected no output for empty reasoning, got: %v", out) } }