Skip to content

feat(ai): accept "provider/model" strings as models - #60514

Open
rekram1-node wants to merge 2 commits into
devfrom
string-model-ids
Open

rekram1-node wants to merge 2 commits into
devfrom
string-model-ids

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

I made model accept a "provider/model" string anywhere a text, image, or decision model is accepted, in both the Promise and Effect APIs. A model object still works for anything that needs settings.

await ai.llm.generate({ model: "openai/gpt-5.4", prompt: "Hi" })
await ai.llm.generate({ model: OpenAI.model("gpt-5.4", { baseURL }), prompt: "Hi" })

The text before the first / picks the provider and the rest is the model id, so openrouter/openai/gpt-5.4 sends openai/gpt-5.4 to OpenRouter. The string loads that provider's default model, imageModel or decisionModel with its env key and no settings. Each provider module is imported only when a string names it, inside the Effect, so failures are a typed AIError:

  • unknown provider or no /: lists the known ids
  • provider without that kind of model: Provider "anthropic" has no image models
  • provider that needs settings, such as azure/...: the provider's own error (requires resourceName or baseURL)

Provider options for a string model are untyped. LLM.request still needs a model object because it builds the request synchronously, and so does compact, which takes a built request. This is a stopgap until strings resolve through a bundled catalog.

Accepted ids are each provider module's id, plus these models.dev ids where ours differ:

models.dev id provider
fireworks-ai fireworks
vercel vercel-ai-gateway
google-vertex-anthropic google-vertex (Messages API)

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor" font-family="ui-monospace,monospace" font-size="11">
  <style>
    .s{animation:s 3s ease-in-out infinite}
    .m{animation:m 3s ease-in-out infinite}
    @keyframes s{0%,35%{opacity:1;transform:translateX(0)}55%,100%{opacity:.25;transform:translateX(6px)}}
    @keyframes m{0%,40%{opacity:.3}60%,100%{opacity:1}}
    @media (prefers-reduced-motion:reduce){.s,.m{animation:none}}
  </style>
  <g class="s"><rect x="8" y="22" width="112" height="28" rx="6" opacity=".5"/><text x="18" y="40" fill="currentColor" stroke="none">"openai/gpt-5.4"</text></g>
  <path d="M128 36h22m-6-5 6 5-6 5" opacity=".6"/>
  <g class="m"><rect x="158" y="14" width="74" height="44" rx="8"/><text x="168" y="33" fill="currentColor" stroke="none">OpenAI</text><text x="168" y="48" fill="currentColor" stroke="none" opacity=".6">.model()</text></g>
</svg>

Added by the opencode bot (AI-generated).

Public API: model accepts "provider/model" strings in the LLM, Image and Decision APIs and the ai client (backwards compatible)

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change works as described. I ran the new model-string tests and the full packages/ai suite, and all pass. The package's typecheck script needs bun check, which wasn't available here, so I ran its tsc fallback for both configs (including the new *.types.ts) instead; it passes. I also type-checked packages/core, which uses this package, and it passes. Strings resolve lazily inside the Effect, failures come back as a typed InvalidRequest, Object.hasOwn rejects names like constructor/x, and model objects and already-built requests are passed through unchanged.

Public API (@opencode/ai, backwards compatible):

  • model now also accepts a "provider/model" string in:
    • LLM.generate, stream, loop and generateObject
    • Image.generate, stream, start and resume
    • Decision.generate
    • the matching ai.llm, ai.image and ai.decision methods
  • The Model constraints of RequestInput, LoopInput, GenerateObjectOptions, GenerateObjectDynamicOptions, ImageRequestInput and DecisionRequestInput widen from X to X | string.
  • LanguageModelProviderOptions, ImageModelOptions and DecisionModelOptions now map string to the untyped options instead of never.
  • LLM.request and compact still need a model object.

Existing callers that pass model objects are unaffected.

One small inline note on the type test.

Comment thread packages/ai/test/model-string.types.ts Outdated
const ids = ["openai/gpt-5.4-nano", "anthropic/claude-haiku-4-5"] as const
declare const configured: string

LLM.generate({ model: "openai/gpt-5.4", prompt: "Hi", providerOptions: { openai: { store: false } } })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

providerOptions is flat everywhere else (it goes to the route as-is, e.g. request.providerOptions?.store), so { openai: { store: false } } only type-checks because string models take an untyped Record<string, unknown>. At runtime it sends an openai key that the OpenAI route ignores. Could this use the flat shape ({ store: false }), so the test doesn't suggest a namespaced form that doesn't work?

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit fixes my earlier point: the string-model type test now uses flat providerOptions. The type-test config (tsconfig.types.json) still passes, and I have nothing new to flag. The public API impact is the same as before: model also accepts "provider/model" strings, and existing code that passes model objects still works.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant