Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions pkg/model/provider/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
joshbarrington marked this conversation as resolved.
return providerType == "github-copilot"
Comment thread
joshbarrington marked this conversation as resolved.
}

// ---------------------------------------------------------------------------
// Provider defaults
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" {
Comment thread
joshbarrington marked this conversation as resolved.
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 {
Expand All @@ -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
}
42 changes: 42 additions & 0 deletions pkg/model/provider/model_defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
{
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"])
}
})
}
}
Expand Down Expand Up @@ -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"))
Comment thread
joshbarrington marked this conversation as resolved.
Comment thread
joshbarrington marked this conversation as resolved.
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"))
Comment thread
joshbarrington marked this conversation as resolved.
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.
Expand Down
32 changes: 32 additions & 0 deletions pkg/model/provider/provider_defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment thread
joshbarrington marked this conversation as resolved.
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)
}
Comment thread
joshbarrington marked this conversation as resolved.