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
28 changes: 14 additions & 14 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ export const BuiltinCommandNameSchema = z.enum([
"start-work",
])

export const FallbackChainEntrySchema = z.object({
providers: z.array(z.string()),
model: z.string(),
variant: z.string().optional(),
})
export const FallbackChainSchema = z.array(FallbackChainEntrySchema)

export const AgentOverrideConfigSchema = z.object({
/** @deprecated Use `category` instead. Model is inherited from category defaults. */
model: z.string().optional(),
Expand Down Expand Up @@ -135,6 +142,8 @@ export const AgentOverrideConfigSchema = z.object({
textVerbosity: z.enum(["low", "medium", "high"]).optional(),
/** Provider-specific options. Passed directly to OpenCode SDK. */
providerOptions: z.record(z.string(), z.unknown()).optional(),
/** Custom fallback chain for model resolution. Overrides built-in agent fallback chains. */
fallback_chain: FallbackChainSchema.optional(),
})

export const AgentOverridesSchema = z.object({
Expand Down Expand Up @@ -189,6 +198,8 @@ export const CategoryConfigSchema = z.object({
prompt_append: z.string().optional(),
/** Mark agent as unstable - forces background mode for monitoring. Auto-enabled for gemini/minimax models. */
is_unstable_agent: z.boolean().optional(),
/** Custom fallback chain for model resolution. Each entry specifies providers, model, and optional variant. */
fallback_chain: FallbackChainSchema.optional(),
})

export const BuiltinCategoryNameSchema = z.enum([
Expand Down
197 changes: 197 additions & 0 deletions src/shared/agent-variant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,201 @@ describe("resolveVariantForModel", () => {
// then
expect(variant).toBe("max")
})

test("matches model with antigravity- prefix to base model", () => {
// #given
const config = {} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: "antigravity-claude-opus-4-5" }

// #when
const variant = resolveVariantForModel(config, "sisyphus", model)

// #then
expect(variant).toBe("max")
})

test("uses user-defined fallback_chain when provided for agent", () => {
// #given
const config = {
agents: {
sisyphus: {
fallback_chain: [
{ providers: ["custom-provider"], model: "custom-model", variant: "custom-variant" }
]
}
}
} as OhMyOpenCodeConfig
const model = { providerID: "custom-provider", modelID: "custom-model" }

// #when
const variant = resolveVariantForModel(config, "sisyphus", model)

// #then
expect(variant).toBe("custom-variant")
})

test("uses user-defined fallback_chain when provided for category", () => {
// #given
const config = {
agents: {
"custom-agent": { category: "my-category" }
},
categories: {
"my-category": {
model: "some/model",
fallback_chain: [
{ providers: ["google"], model: "gemini-3-pro", variant: "high" }
]
}
}
} as OhMyOpenCodeConfig
const model = { providerID: "google", modelID: "antigravity-gemini-3-pro" }

// #when
const variant = resolveVariantForModel(config, "custom-agent", model)

// #then
expect(variant).toBe("high")
})

test("falls back to agent variant when no fallback chain matches", () => {
// #given
const config = {
agents: {
"custom-agent": { variant: "fallback-variant" }
}
} as OhMyOpenCodeConfig
const model = { providerID: "unknown-provider", modelID: "unknown-model" }

// #when
const variant = resolveVariantForModel(config, "custom-agent", model)

// #then
expect(variant).toBe("fallback-variant")
})

test("falls back to category variant when no fallback chain matches", () => {
// #given
const config = {
agents: {
"custom-agent": { category: "my-category" }
},
categories: {
"my-category": {
model: "some/model",
variant: "category-fallback-variant"
}
}
} as OhMyOpenCodeConfig
const model = { providerID: "unknown-provider", modelID: "unknown-model" }

// #when
const variant = resolveVariantForModel(config, "custom-agent", model)

// #then
expect(variant).toBe("category-fallback-variant")
})

// Edge case tests for PR #1307 fix
test("does not select variant when provider matches but model does not", () => {
// given
const config = {
agents: {
testAgent: {
fallback_chain: [
{ providers: ["anthropic"], model: "claude-opus-4", variant: "max" }
]
}
}
} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: "claude-sonnet-4" } // Different model

// when
const variant = resolveVariantForModel(config, "testAgent", model)

// then
expect(variant).toBeUndefined()
})

test("selects variant when provider matches and entry has no model", () => {
// given
const config = {
agents: {
testAgent: {
fallback_chain: [
{ providers: ["anthropic"], variant: "high" } // No model specified
]
}
}
} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: "any-model" }

// when
const variant = resolveVariantForModel(config, "testAgent", model)

// then
expect(variant).toBe("high")
})

test("does not match when modelID is undefined but entry requires model", () => {
// given
const config = {
agents: {
testAgent: {
fallback_chain: [
{ providers: ["anthropic"], model: "claude-opus-4", variant: "max" }
]
}
}
} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: undefined } as any // Forced undefined

// when
const variant = resolveVariantForModel(config, "testAgent", model)

// then
expect(variant).toBeUndefined()
})

test("skips mismatched entries and finds later matching entry", () => {
// given
const config = {
agents: {
testAgent: {
fallback_chain: [
{ providers: ["anthropic"], model: "claude-opus-4", variant: "max" }, // Wrong model
{ providers: ["anthropic"], model: "claude-sonnet-4", variant: "high" } // Correct
]
}
}
} as OhMyOpenCodeConfig
const model = { providerID: "anthropic", modelID: "claude-sonnet-4" }

// when
const variant = resolveVariantForModel(config, "testAgent", model)

// then
expect(variant).toBe("high")
})

test("respects model matching for entries with multiple providers", () => {
// given
const config = {
agents: {
testAgent: {
fallback_chain: [
{ providers: ["anthropic", "openai"], model: "gpt-4", variant: "high" }
]
}
}
} as OhMyOpenCodeConfig
// Provider matches (anthropic) but model (claude-opus-4) doesn't match gpt-4
const model = { providerID: "anthropic", modelID: "claude-opus-4" }

// when
const variant = resolveVariantForModel(config, "testAgent", model)

// then
expect(variant).toBeUndefined()
})
})
Loading