Spaces:
Paused
Paused
| package executor | |
| import ( | |
| "strings" | |
| "testing" | |
| sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" | |
| "github.com/tidwall/gjson" | |
| ) | |
| func TestGitHubCopilotNormalizeModel_StripsSuffix(t *testing.T) { | |
| t.Parallel() | |
| tests := []struct { | |
| name string | |
| model string | |
| wantModel string | |
| }{ | |
| { | |
| name: "suffix stripped", | |
| model: "claude-opus-4.6(medium)", | |
| wantModel: "claude-opus-4.6", | |
| }, | |
| { | |
| name: "no suffix unchanged", | |
| model: "claude-opus-4.6", | |
| wantModel: "claude-opus-4.6", | |
| }, | |
| { | |
| name: "different suffix stripped", | |
| model: "gpt-4o(high)", | |
| wantModel: "gpt-4o", | |
| }, | |
| { | |
| name: "numeric suffix stripped", | |
| model: "gemini-2.5-pro(8192)", | |
| wantModel: "gemini-2.5-pro", | |
| }, | |
| } | |
| e := &GitHubCopilotExecutor{} | |
| for _, tt := range tests { | |
| t.Run(tt.name, func(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"model":"` + tt.model + `","messages":[]}`) | |
| got := e.normalizeModel(tt.model, body) | |
| gotModel := gjson.GetBytes(got, "model").String() | |
| if gotModel != tt.wantModel { | |
| t.Fatalf("normalizeModel() model = %q, want %q", gotModel, tt.wantModel) | |
| } | |
| }) | |
| } | |
| } | |
| func TestUseGitHubCopilotResponsesEndpoint_OpenAIResponseSource(t *testing.T) { | |
| t.Parallel() | |
| if !useGitHubCopilotResponsesEndpoint(sdktranslator.FromString("openai-response"), "claude-3-5-sonnet") { | |
| t.Fatal("expected openai-response source to use /responses") | |
| } | |
| } | |
| func TestUseGitHubCopilotResponsesEndpoint_CodexModel(t *testing.T) { | |
| t.Parallel() | |
| if !useGitHubCopilotResponsesEndpoint(sdktranslator.FromString("openai"), "gpt-5-codex") { | |
| t.Fatal("expected codex model to use /responses") | |
| } | |
| } | |
| func TestUseGitHubCopilotResponsesEndpoint_DefaultChat(t *testing.T) { | |
| t.Parallel() | |
| if useGitHubCopilotResponsesEndpoint(sdktranslator.FromString("openai"), "claude-3-5-sonnet") { | |
| t.Fatal("expected default openai source with non-codex model to use /chat/completions") | |
| } | |
| } | |
| func TestNormalizeGitHubCopilotChatTools_KeepFunctionOnly(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"tools":[{"type":"function","function":{"name":"ok"}},{"type":"code_interpreter"}],"tool_choice":"auto"}`) | |
| got := normalizeGitHubCopilotChatTools(body) | |
| tools := gjson.GetBytes(got, "tools").Array() | |
| if len(tools) != 1 { | |
| t.Fatalf("tools len = %d, want 1", len(tools)) | |
| } | |
| if tools[0].Get("type").String() != "function" { | |
| t.Fatalf("tool type = %q, want function", tools[0].Get("type").String()) | |
| } | |
| } | |
| func TestNormalizeGitHubCopilotChatTools_InvalidToolChoiceDowngradeToAuto(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"tools":[],"tool_choice":{"type":"function","function":{"name":"x"}}}`) | |
| got := normalizeGitHubCopilotChatTools(body) | |
| if gjson.GetBytes(got, "tool_choice").String() != "auto" { | |
| t.Fatalf("tool_choice = %s, want auto", gjson.GetBytes(got, "tool_choice").Raw) | |
| } | |
| } | |
| func TestNormalizeGitHubCopilotResponsesInput_MissingInputExtractedFromSystemAndMessages(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"system":"sys text","messages":[{"role":"user","content":"user text"},{"role":"assistant","content":[{"type":"text","text":"assistant text"}]}]}`) | |
| got := normalizeGitHubCopilotResponsesInput(body) | |
| in := gjson.GetBytes(got, "input") | |
| if !in.IsArray() { | |
| t.Fatalf("input type = %v, want array", in.Type) | |
| } | |
| raw := in.Raw | |
| if !strings.Contains(raw, "sys text") || !strings.Contains(raw, "user text") || !strings.Contains(raw, "assistant text") { | |
| t.Fatalf("input = %s, want structured array with all texts", raw) | |
| } | |
| if gjson.GetBytes(got, "messages").Exists() { | |
| t.Fatal("messages should be removed after conversion") | |
| } | |
| if gjson.GetBytes(got, "system").Exists() { | |
| t.Fatal("system should be removed after conversion") | |
| } | |
| } | |
| func TestNormalizeGitHubCopilotResponsesInput_NonStringInputStringified(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"input":{"foo":"bar"}}`) | |
| got := normalizeGitHubCopilotResponsesInput(body) | |
| in := gjson.GetBytes(got, "input") | |
| if in.Type != gjson.String { | |
| t.Fatalf("input type = %v, want string", in.Type) | |
| } | |
| if !strings.Contains(in.String(), "foo") { | |
| t.Fatalf("input = %q, want stringified object", in.String()) | |
| } | |
| } | |
| func TestNormalizeGitHubCopilotResponsesTools_FlattenFunctionTools(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"tools":[{"type":"function","function":{"name":"sum","description":"d","parameters":{"type":"object"}}},{"type":"web_search"}]}`) | |
| got := normalizeGitHubCopilotResponsesTools(body) | |
| tools := gjson.GetBytes(got, "tools").Array() | |
| if len(tools) != 1 { | |
| t.Fatalf("tools len = %d, want 1", len(tools)) | |
| } | |
| if tools[0].Get("name").String() != "sum" { | |
| t.Fatalf("tools[0].name = %q, want sum", tools[0].Get("name").String()) | |
| } | |
| if !tools[0].Get("parameters").Exists() { | |
| t.Fatal("expected parameters to be preserved") | |
| } | |
| } | |
| func TestNormalizeGitHubCopilotResponsesTools_ClaudeFormatTools(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"tools":[{"name":"Bash","description":"Run commands","input_schema":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}},{"name":"Read","description":"Read files","input_schema":{"type":"object","properties":{"path":{"type":"string"}}}}]}`) | |
| got := normalizeGitHubCopilotResponsesTools(body) | |
| tools := gjson.GetBytes(got, "tools").Array() | |
| if len(tools) != 2 { | |
| t.Fatalf("tools len = %d, want 2", len(tools)) | |
| } | |
| if tools[0].Get("type").String() != "function" { | |
| t.Fatalf("tools[0].type = %q, want function", tools[0].Get("type").String()) | |
| } | |
| if tools[0].Get("name").String() != "Bash" { | |
| t.Fatalf("tools[0].name = %q, want Bash", tools[0].Get("name").String()) | |
| } | |
| if tools[0].Get("description").String() != "Run commands" { | |
| t.Fatalf("tools[0].description = %q, want 'Run commands'", tools[0].Get("description").String()) | |
| } | |
| if !tools[0].Get("parameters").Exists() { | |
| t.Fatal("expected parameters to be set from input_schema") | |
| } | |
| if tools[0].Get("parameters.properties.command").Exists() != true { | |
| t.Fatal("expected parameters.properties.command to exist") | |
| } | |
| if tools[1].Get("name").String() != "Read" { | |
| t.Fatalf("tools[1].name = %q, want Read", tools[1].Get("name").String()) | |
| } | |
| } | |
| func TestNormalizeGitHubCopilotResponsesTools_FlattenToolChoiceFunctionObject(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"tool_choice":{"type":"function","function":{"name":"sum"}}}`) | |
| got := normalizeGitHubCopilotResponsesTools(body) | |
| if gjson.GetBytes(got, "tool_choice.type").String() != "function" { | |
| t.Fatalf("tool_choice.type = %q, want function", gjson.GetBytes(got, "tool_choice.type").String()) | |
| } | |
| if gjson.GetBytes(got, "tool_choice.name").String() != "sum" { | |
| t.Fatalf("tool_choice.name = %q, want sum", gjson.GetBytes(got, "tool_choice.name").String()) | |
| } | |
| } | |
| func TestNormalizeGitHubCopilotResponsesTools_InvalidToolChoiceDowngradeToAuto(t *testing.T) { | |
| t.Parallel() | |
| body := []byte(`{"tool_choice":{"type":"function"}}`) | |
| got := normalizeGitHubCopilotResponsesTools(body) | |
| if gjson.GetBytes(got, "tool_choice").String() != "auto" { | |
| t.Fatalf("tool_choice = %s, want auto", gjson.GetBytes(got, "tool_choice").Raw) | |
| } | |
| } | |
| func TestTranslateGitHubCopilotResponsesNonStreamToClaude_TextMapping(t *testing.T) { | |
| t.Parallel() | |
| resp := []byte(`{"id":"resp_1","model":"gpt-5-codex","output":[{"type":"message","content":[{"type":"output_text","text":"hello"}]}],"usage":{"input_tokens":3,"output_tokens":5}}`) | |
| out := translateGitHubCopilotResponsesNonStreamToClaude(resp) | |
| if gjson.Get(out, "type").String() != "message" { | |
| t.Fatalf("type = %q, want message", gjson.Get(out, "type").String()) | |
| } | |
| if gjson.Get(out, "content.0.type").String() != "text" { | |
| t.Fatalf("content.0.type = %q, want text", gjson.Get(out, "content.0.type").String()) | |
| } | |
| if gjson.Get(out, "content.0.text").String() != "hello" { | |
| t.Fatalf("content.0.text = %q, want hello", gjson.Get(out, "content.0.text").String()) | |
| } | |
| } | |
| func TestTranslateGitHubCopilotResponsesNonStreamToClaude_ToolUseMapping(t *testing.T) { | |
| t.Parallel() | |
| resp := []byte(`{"id":"resp_2","model":"gpt-5-codex","output":[{"type":"function_call","id":"fc_1","call_id":"call_1","name":"sum","arguments":"{\"a\":1}"}],"usage":{"input_tokens":1,"output_tokens":2}}`) | |
| out := translateGitHubCopilotResponsesNonStreamToClaude(resp) | |
| if gjson.Get(out, "content.0.type").String() != "tool_use" { | |
| t.Fatalf("content.0.type = %q, want tool_use", gjson.Get(out, "content.0.type").String()) | |
| } | |
| if gjson.Get(out, "content.0.name").String() != "sum" { | |
| t.Fatalf("content.0.name = %q, want sum", gjson.Get(out, "content.0.name").String()) | |
| } | |
| if gjson.Get(out, "stop_reason").String() != "tool_use" { | |
| t.Fatalf("stop_reason = %q, want tool_use", gjson.Get(out, "stop_reason").String()) | |
| } | |
| } | |
| func TestTranslateGitHubCopilotResponsesStreamToClaude_TextLifecycle(t *testing.T) { | |
| t.Parallel() | |
| var param any | |
| created := translateGitHubCopilotResponsesStreamToClaude([]byte(`data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5-codex"}}`), ¶m) | |
| if len(created) == 0 || !strings.Contains(created[0], "message_start") { | |
| t.Fatalf("created events = %#v, want message_start", created) | |
| } | |
| delta := translateGitHubCopilotResponsesStreamToClaude([]byte(`data: {"type":"response.output_text.delta","delta":"he"}`), ¶m) | |
| joinedDelta := strings.Join(delta, "") | |
| if !strings.Contains(joinedDelta, "content_block_start") || !strings.Contains(joinedDelta, "text_delta") { | |
| t.Fatalf("delta events = %#v, want content_block_start + text_delta", delta) | |
| } | |
| completed := translateGitHubCopilotResponsesStreamToClaude([]byte(`data: {"type":"response.completed","response":{"usage":{"input_tokens":7,"output_tokens":9}}}`), ¶m) | |
| joinedCompleted := strings.Join(completed, "") | |
| if !strings.Contains(joinedCompleted, "message_delta") || !strings.Contains(joinedCompleted, "message_stop") { | |
| t.Fatalf("completed events = %#v, want message_delta + message_stop", completed) | |
| } | |
| } | |