diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 867ef2ca2fa8..ebca9cc6faf7 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1357,6 +1357,21 @@ function modeOptions(model: Model, body: Record | 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) => { @@ -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 diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 32d4e3a39b10..748a21886d2c 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -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", () => {