diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index 8896a9dfdb..2c16d5bc0d 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -365,6 +365,19 @@ func GetGitHubCopilotModels() []*ModelInfo { SupportedEndpoints: []string{"/responses"}, Thinking: &ThinkingSupport{Levels: []string{"none", "low", "medium", "high", "xhigh"}}, }, + { + ID: "gpt-5.4", + Object: "model", + Created: now, + OwnedBy: "github-copilot", + Type: "github-copilot", + DisplayName: "GPT-5.4", + Description: "OpenAI GPT-5.4 via GitHub Copilot", + ContextLength: 200000, + MaxCompletionTokens: 32768, + SupportedEndpoints: []string{"/responses"}, + Thinking: &ThinkingSupport{Levels: []string{"none", "low", "medium", "high", "xhigh"}}, + }, { ID: "claude-haiku-4.5", Object: "model", diff --git a/internal/runtime/executor/github_copilot_executor.go b/internal/runtime/executor/github_copilot_executor.go index b86146a31a..a5770e078f 100644 --- a/internal/runtime/executor/github_copilot_executor.go +++ b/internal/runtime/executor/github_copilot_executor.go @@ -577,9 +577,33 @@ func useGitHubCopilotResponsesEndpoint(sourceFormat sdktranslator.Format, model return true } baseModel := strings.ToLower(thinking.ParseSuffix(model).ModelName) + if info := registry.GetGlobalRegistry().GetModelInfo(baseModel, githubCopilotAuthType); info != nil { + return len(info.SupportedEndpoints) > 0 && !containsEndpoint(info.SupportedEndpoints, githubCopilotChatPath) && containsEndpoint(info.SupportedEndpoints, githubCopilotResponsesPath) + } + if info := lookupGitHubCopilotStaticModelInfo(baseModel); info != nil { + return len(info.SupportedEndpoints) > 0 && !containsEndpoint(info.SupportedEndpoints, githubCopilotChatPath) && containsEndpoint(info.SupportedEndpoints, githubCopilotResponsesPath) + } return strings.Contains(baseModel, "codex") } +func lookupGitHubCopilotStaticModelInfo(model string) *registry.ModelInfo { + for _, info := range registry.GetStaticModelDefinitionsByChannel(githubCopilotAuthType) { + if info != nil && strings.EqualFold(info.ID, model) { + return info + } + } + return nil +} + +func containsEndpoint(endpoints []string, endpoint string) bool { + for _, item := range endpoints { + if item == endpoint { + return true + } + } + return false +} + // flattenAssistantContent converts assistant message content from array format // to a joined string. GitHub Copilot requires assistant content as a string; // sending it as an array causes Claude models to re-answer all previous prompts. diff --git a/internal/runtime/executor/github_copilot_executor_test.go b/internal/runtime/executor/github_copilot_executor_test.go index 66d5ce924f..dfc332f24d 100644 --- a/internal/runtime/executor/github_copilot_executor_test.go +++ b/internal/runtime/executor/github_copilot_executor_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" "github.com/tidwall/gjson" ) @@ -70,6 +71,29 @@ func TestUseGitHubCopilotResponsesEndpoint_CodexModel(t *testing.T) { } } +func TestUseGitHubCopilotResponsesEndpoint_RegistryResponsesOnlyModel(t *testing.T) { + t.Parallel() + if !useGitHubCopilotResponsesEndpoint(sdktranslator.FromString("openai"), "gpt-5.4") { + t.Fatal("expected responses-only registry model to use /responses") + } +} + +func TestUseGitHubCopilotResponsesEndpoint_DynamicRegistryWinsOverStatic(t *testing.T) { + t.Parallel() + + reg := registry.GetGlobalRegistry() + clientID := "github-copilot-test-client" + reg.RegisterClient(clientID, "github-copilot", []*registry.ModelInfo{{ + ID: "gpt-5.4", + SupportedEndpoints: []string{"/chat/completions", "/responses"}, + }}) + defer reg.UnregisterClient(clientID) + + if useGitHubCopilotResponsesEndpoint(sdktranslator.FromString("openai"), "gpt-5.4") { + t.Fatal("expected dynamic registry definition to take precedence over static fallback") + } +} + func TestUseGitHubCopilotResponsesEndpoint_DefaultChat(t *testing.T) { t.Parallel() if useGitHubCopilotResponsesEndpoint(sdktranslator.FromString("openai"), "claude-3-5-sonnet") {