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
17 changes: 16 additions & 1 deletion packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,21 @@ function modeOptions(model: Model, body: Record<string, unknown> | undefined) {
return { ...rest, reasoningMode: reasoning.mode }
}

// OpenRouter route modifiers are request-time routing suffixes (e.g. `openrouter/openai/gpt-4:floor`),
// not catalog model keys. Resolve a suffixed reference against its base catalog model while keeping
// the suffixed ID for provider requests and the reference ID for session state.
// https://openrouter.ai/docs/guides/routing/model-variants
const OPENROUTER_ROUTE_MODIFIERS = [":floor", ":nitro", ":exacto", ":online"]

function routeModifierModel(provider: Info, modelID: ModelV2.ID): Model | undefined {
if (provider.id !== ProviderV2.ID.openrouter) return undefined
const suffix = OPENROUTER_ROUTE_MODIFIERS.find((modifier) => modelID.endsWith(modifier))
if (!suffix) return undefined
const base = provider.models[modelID.slice(0, -suffix.length)]
if (!base) return undefined
return { ...base, id: modelID, api: { ...base.api, id: modelID } }
}

function modelSuggestions(provider: Info | undefined, modelID: ModelV2.ID, enableExperimentalModels: boolean) {
const available = provider
? Object.keys(provider.models).filter((id) => {
Expand Down Expand Up @@ -1882,7 +1897,7 @@ const layer = Layer.effect(
return yield* new ModelNotFoundError({ providerID, modelID, suggestions })
}

const info = provider.models[modelID]
const info = provider.models[modelID] ?? routeModifierModel(provider, modelID)
if (!info) {
const current = modelSuggestions(provider, modelID, runtimeFlags.enableExperimentalModels)
const suggestions = current.length
Expand Down
23 changes: 23 additions & 0 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,29 @@ it.instance("getModel throws ModelNotFoundError for invalid provider", () =>
}),
)

it.instance("getModel resolves OpenRouter route-modifier suffixes against the base model", () =>
Effect.gen(function* () {
yield* set("OPENROUTER_API_KEY", "test-api-key")
const model = yield* Provider.use.getModel(
ProviderV2.ID.openrouter,
ModelV2.ID.make("openai/gpt-5.6-luna:floor"),
)
expect(String(model.id)).toBe("openai/gpt-5.6-luna:floor")
expect(model.api.id).toBe("openai/gpt-5.6-luna:floor")
expect(model.api.npm).toBe("@openrouter/ai-sdk-provider")
}),
)

it.instance("getModel throws ModelNotFoundError for unknown route-modifier base", () =>
Effect.gen(function* () {
yield* set("OPENROUTER_API_KEY", "test-api-key")
const exit = yield* Provider.use
.getModel(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/nonexistent-model:floor"))
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
}),
)

// Pure synchronous unit tests — no Effect runtime needed.

test("parseModel correctly parses provider/model string", () => {
Expand Down
Loading