From e3c7b09d577c0c95b5540e3909d21ad711ff255c Mon Sep 17 00:00:00 2001 From: Joshua Barrington Date: Thu, 7 May 2026 21:56:02 +0200 Subject: [PATCH] default the api_type as openai_responses for appropriate github copilot models --- pkg/model/provider/defaults.go | 30 ++++++++++++++ pkg/model/provider/model_defaults_test.go | 42 ++++++++++++++++++++ pkg/model/provider/provider_defaults_test.go | 32 +++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/pkg/model/provider/defaults.go b/pkg/model/provider/defaults.go index 3f817c8b3..75530b236 100644 --- a/pkg/model/provider/defaults.go +++ b/pkg/model/provider/defaults.go @@ -43,6 +43,11 @@ func isOpenAICompatibleProvider(providerType string) bool { return exists && alias.APIType == "openai" } +// isGithubCopilotProvider returns true if the provider type is "github-copilot". +func isGithubCopilotProvider(providerType string) bool { + return providerType == "github-copilot" +} + // --------------------------------------------------------------------------- // Provider defaults // --------------------------------------------------------------------------- @@ -191,6 +196,9 @@ func cloneModelConfig(cfg *latest.ModelConfig) *latest.ModelConfig { // // NOTE: max_tokens is NOT set here; see teamloader and runtime/model_switcher. func applyModelDefaults(cfg *latest.ModelConfig) { + // Set appropriate github copilot api_type. + applyGithubCopilotAPIType(cfg) + // Explicitly disabled → normalise to nil so providers never see it. if cfg.ThinkingBudget.IsDisabled() { cfg.ThinkingBudget = nil @@ -239,6 +247,19 @@ func ensureInterleavedThinking(cfg *latest.ModelConfig, providerType string) { } } +func applyGithubCopilotAPIType(cfg *latest.ModelConfig) { + if isGithubCopilotProvider(cfg.Provider) && isCopilotResponsesModel(cfg.Model) { + if cfg.ProviderOpts == nil { + cfg.ProviderOpts = make(map[string]any) + } + // If it's not set, or was set to openai_chatcompletions by the generic fallback, override it. + // User explicit openai_chatcompletions is unsupported for these models. + if apiType, ok := cfg.ProviderOpts["api_type"].(string); !ok || apiType == "" || apiType == "openai_chatcompletions" { + cfg.ProviderOpts["api_type"] = "openai_responses" + } + } +} + // needsInterleavedThinking reports whether a (provider, model) pair refers to // a Claude model on a host that supports the interleaved-thinking beta. func needsInterleavedThinking(providerType, model string) bool { @@ -250,3 +271,12 @@ func needsInterleavedThinking(providerType, model string) bool { } return false } + +// isCopilotResponsesModel returns true if the model is a GitHub Copilot model that requires the openai_responses API type. +func isCopilotResponsesModel(model string) bool { + switch model { + case "gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.4-mini", "gpt-5.4-nano": + return true + } + return false +} diff --git a/pkg/model/provider/model_defaults_test.go b/pkg/model/provider/model_defaults_test.go index 6fbcc49ed..955b29f0d 100644 --- a/pkg/model/provider/model_defaults_test.go +++ b/pkg/model/provider/model_defaults_test.go @@ -13,12 +13,14 @@ func TestApplyModelDefaults(t *testing.T) { t.Parallel() boolPtr := func(v bool) *bool { return &v } + strPtr := func(v string) *string { return &v } tests := []struct { name string config *latest.ModelConfig wantBudget *latest.ThinkingBudget // nil means no thinking wantInterleaved *bool // nil means key must not exist + wantAPIType *string // nil means key must not exist }{ // --- OpenAI: only o-series gets defaults --- { @@ -138,6 +140,28 @@ func TestApplyModelDefaults(t *testing.T) { config: &latest.ModelConfig{Provider: "openai", Model: "gpt-4o", ThinkingBudget: &latest.ThinkingBudget{Effort: "none"}}, }, + // --- GitHub Copilot: api_type defaults and overrides --- + { + name: "github-copilot: responses model defaults to openai_responses", + config: &latest.ModelConfig{Provider: "github-copilot", Model: "gpt-5.3-codex"}, + wantAPIType: strPtr("openai_responses"), + }, + { + name: "github-copilot: responses model overrides openai_chatcompletions", + config: &latest.ModelConfig{Provider: "github-copilot", Model: "gpt-5.3-codex", ProviderOpts: map[string]any{"api_type": "openai_chatcompletions"}}, + wantAPIType: strPtr("openai_responses"), + }, + { + name: "github-copilot: responses model preserves explicit openai_responses", + config: &latest.ModelConfig{Provider: "github-copilot", Model: "gpt-5.3-codex", ProviderOpts: map[string]any{"api_type": "openai_responses"}}, + wantAPIType: strPtr("openai_responses"), + }, + { + name: "github-copilot: responses model preserves custom api_type", + config: &latest.ModelConfig{Provider: "github-copilot", Model: "gpt-5.3-codex", ProviderOpts: map[string]any{"api_type": "custom_hypothetical_type"}}, + wantAPIType: strPtr("custom_hypothetical_type"), + }, + // --- Unknown / other providers: no effect --- { name: "unknown provider: no effect", @@ -173,6 +197,12 @@ func TestApplyModelDefaults(t *testing.T) { require.NotNil(t, tt.config.ProviderOpts) assert.Equal(t, *tt.wantInterleaved, tt.config.ProviderOpts["interleaved_thinking"]) } + + // Check api_type if wantAPIType is specified. + if tt.wantAPIType != nil { + require.NotNil(t, tt.config.ProviderOpts) + assert.Equal(t, *tt.wantAPIType, tt.config.ProviderOpts["api_type"]) + } }) } } @@ -265,6 +295,18 @@ func TestApplyProviderDefaults_DoesNotModifyOriginal(t *testing.T) { assert.Equal(t, "original_value", original.ProviderOpts["custom_key"]) } +func TestIsCopilotResponsesModel(t *testing.T) { + t.Parallel() + + assert.True(t, isCopilotResponsesModel("gpt-5.3-codex")) + assert.True(t, isCopilotResponsesModel("gpt-5.2-codex")) + assert.True(t, isCopilotResponsesModel("gpt-5.4-mini")) + assert.True(t, isCopilotResponsesModel("gpt-5.4-nano")) + assert.False(t, isCopilotResponsesModel("gpt-4o")) + assert.False(t, isCopilotResponsesModel("claude-sonnet-4-5")) + assert.False(t, isCopilotResponsesModel("")) +} + // TestApplyProviderDefaults_InheritsAuthFromProviderConfig verifies that a // ProviderConfig's Auth block is inherited by models that don't override it, // while a model-level Auth always wins. diff --git a/pkg/model/provider/provider_defaults_test.go b/pkg/model/provider/provider_defaults_test.go index a1685e20c..0478506e4 100644 --- a/pkg/model/provider/provider_defaults_test.go +++ b/pkg/model/provider/provider_defaults_test.go @@ -414,3 +414,35 @@ func TestApplyProviderDefaults_AliasFallback(t *testing.T) { assert.Empty(t, cfg.BaseURL) assert.Empty(t, cfg.TokenKey) } + +func TestIsGithubCopilotProvider(t *testing.T) { + t.Parallel() + + assert.True(t, isGithubCopilotProvider("github-copilot")) + assert.False(t, isGithubCopilotProvider("openai")) + assert.False(t, isGithubCopilotProvider("")) +} + +func TestGithubCopilotApiType(t *testing.T) { + t.Parallel() + + cfg := &latest.ModelConfig{ + Provider: "github-copilot", + Model: "gpt-5.3-codex", + } + enhancedCfg := applyProviderDefaults(cfg, nil) + apiType := resolveProviderType(enhancedCfg) + + assert.Equal(t, "openai_responses", apiType) + + // test when it is a custom provider + customProviders := map[string]latest.ProviderConfig{ + "github-copilot": { + Provider: "github-copilot", + }, + } + enhancedCfg2 := applyProviderDefaults(cfg, customProviders) + apiType2 := resolveProviderType(enhancedCfg2) + + assert.Equal(t, "openai_responses", apiType2) +}