From 954d056dac80dd60dbb736ce73a50a962b17d84f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 19 Aug 2026 16:53:24 -0400 Subject: [PATCH 1/2] refactor(ai): flatten provider request options --- packages/ai/README.md | 14 ++- packages/ai/example/tutorial.ts | 4 +- .../ai/src/protocols/anthropic-messages.ts | 7 +- packages/ai/src/protocols/gemini.ts | 7 +- packages/ai/src/protocols/openai-responses.ts | 2 +- .../protocols/utils/open-responses-options.ts | 4 +- .../src/providers/google-vertex-responses.ts | 2 +- packages/ai/src/providers/google-vertex.ts | 8 +- .../src/providers/open-responses-options.ts | 6 +- packages/ai/src/providers/openai-options.ts | 13 +-- packages/ai/src/providers/openrouter.ts | 8 +- packages/ai/src/providers/xai.ts | 8 +- packages/ai/src/schema/options.ts | 14 +-- packages/ai/test/auth-options.types.ts | 21 +++-- packages/ai/test/compile.test.ts | 38 ++++---- packages/ai/test/continuation-scenarios.ts | 2 +- packages/ai/test/llm-option-types.types.ts | 14 ++- packages/ai/test/llm.test.ts | 10 +- .../anthropic-compatible.types.ts | 4 +- .../test/provider-options/anthropic.types.ts | 4 +- .../ai/test/provider-options/azure.types.ts | 4 +- .../google-vertex-chat.types.ts | 4 +- .../google-vertex-messages.types.ts | 4 +- .../google-vertex-responses.types.ts | 6 +- .../provider-options/google-vertex.types.ts | 4 +- .../ai/test/provider-options/google.types.ts | 22 ++--- .../openai-compatible-responses.types.ts | 12 +-- .../openai-compatible.types.ts | 4 +- .../ai/test/provider-options/openai.types.ts | 14 +-- .../test/provider-options/openrouter.types.ts | 30 +++--- .../ai/test/provider-options/xai.types.ts | 6 +- packages/ai/test/provider-package.test.ts | 32 +++---- .../test/provider/anthropic-messages.test.ts | 15 ++- packages/ai/test/provider/gemini.test.ts | 16 ++-- .../ai/test/provider/google-vertex.test.ts | 2 +- .../openai-chat-reasoning.recorded.test.ts | 2 +- packages/ai/test/provider/openai-chat.test.ts | 4 +- .../openai-compatible-responses.test.ts | 16 ++-- .../ai/test/provider/openai-responses.test.ts | 62 ++++++------- packages/ai/test/provider/openrouter.test.ts | 24 +++-- packages/ai/test/recorded-scenarios.ts | 12 +-- packages/ai/test/tool-runtime.test.ts | 2 +- packages/core/src/aisdk-native.ts | 38 +++----- packages/core/src/aisdk.ts | 40 +++++--- packages/core/test/aisdk-native.test.ts | 91 ++++++++----------- packages/core/test/aisdk.test.ts | 13 ++- 46 files changed, 300 insertions(+), 369 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index 9c3c7b82f684..ad0c548994d0 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -371,11 +371,23 @@ Request options in order of stability: 1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop). 2. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it. -3. **`providerOptions: { : {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing). +3. **`providerOptions: { ... }`** — flat options inferred from the selected model (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing). 4. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist. Route/provider defaults are overridden by request-level values for each axis. +The selected model supplies the provider-specific option type, so per-request overrides stay flat while the canonical runtime request remains provider-neutral: + +```ts +LLM.request({ + model, + prompt, + providerOptions: { + reasoningEffort: "low", + }, +}) +``` + ## Routes Adding a new model or deployment is usually 5-15 lines using `Route.make({ protocol, endpoint, auth, framing, ... })`. The route owns endpoint/auth/framing and the protocol owns body construction plus stream parsing. Transports are reusable IO templates that receive route endpoint/auth at compile time. Capability/catalog metadata lives outside this low-level package; unsupported request shapes fail during protocol lowering. See `AGENTS.md` for the architectural detail. diff --git a/packages/ai/example/tutorial.ts b/packages/ai/example/tutorial.ts index 9a7b1cec2bbd..a283264d50bd 100644 --- a/packages/ai/example/tutorial.ts +++ b/packages/ai/example/tutorial.ts @@ -22,7 +22,7 @@ const model = OpenAI.configure({ apiKey, generation: { maxTokens: 160 }, providerOptions: { - openai: { store: false }, + store: false, }, }).model("gpt-4o-mini") @@ -34,7 +34,7 @@ const model = OpenAI.configure({ // - `generation`: common controls such as max tokens, temperature, topP/topK, // penalties, seed, and stop sequences. // - `promptCacheKey`: stable cache affinity for protocols that support it. -// - `providerOptions`: namespaced provider-native behavior. For example, +// - `providerOptions`: model-typed provider-native behavior. For example, // OpenAI store behavior, Anthropic thinking, Gemini thinking config, or // OpenRouter routing/reasoning. // - `http`: last-resort serializable overlays for final request body, headers, diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 15353a4e528e..13070b936fe4 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -16,7 +16,6 @@ import { type JsonSchema, type LLMRequest, type MediaPart, - type ProviderOptions, type ProviderMetadata, type ToolCallPart, type ToolDefinition, @@ -52,9 +51,7 @@ export interface OptionsInput { readonly effort?: string } -export type ProviderOptionsInput = ProviderOptions & { - readonly anthropic?: OptionsInput -} +export type ProviderOptionsInput = OptionsInput // ============================================================================= // Request Body Schema @@ -593,7 +590,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( }) const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (request: LLMRequest) { - const input = request.providerOptions?.anthropic + const input = request.providerOptions return { thinking: yield* resolveThinking(input?.thinking), effort: typeof input?.effort === "string" ? input.effort : undefined, diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 23a02b8c6a81..7b21daba2beb 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -12,7 +12,6 @@ import { type JsonSchema, type LLMRequest, type MediaPart, - type ProviderOptions, type ProviderMetadata, type TextPart, type ToolCallPart, @@ -67,9 +66,7 @@ export interface OptionsInput { } } -export type ProviderOptionsInput = ProviderOptions & { - readonly gemini?: OptionsInput -} +export type ProviderOptionsInput = OptionsInput // ============================================================================= // Request Body Schema @@ -387,7 +384,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR }) const resolveOptions = (request: LLMRequest) => { - const input = request.providerOptions?.gemini + const input = request.providerOptions const value = input?.thinkingConfig const thinkingConfig = { thinkingBudget: diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 0adf5707ea11..7cfb5dad681b 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -261,7 +261,7 @@ export const route = Route.make({ endpoint, auth, transport, - defaults: { providerOptions: { openai: { store: false } } }, + defaults: { providerOptions: { store: false } }, }) export * as OpenAIResponses from "./openai-responses.js" diff --git a/packages/ai/src/protocols/utils/open-responses-options.ts b/packages/ai/src/protocols/utils/open-responses-options.ts index 10a71d842d4f..4877d17e6907 100644 --- a/packages/ai/src/protocols/utils/open-responses-options.ts +++ b/packages/ai/src/protocols/utils/open-responses-options.ts @@ -69,9 +69,7 @@ export type Resolved = Omit & { const decodeOptions = Schema.decodeUnknownOption(Options) export const resolve = (request: LLMRequest): Resolved => { - const input = Option.getOrUndefined( - decodeOptions(request.providerOptions?.[request.model.route.providerMetadataKey ?? "openresponses"]), - ) + const input = Option.getOrUndefined(decodeOptions(request.providerOptions)) if (!input) return {} return { ...input, diff --git a/packages/ai/src/providers/google-vertex-responses.ts b/packages/ai/src/providers/google-vertex-responses.ts index 1f4fd8ebed19..7b44542a4752 100644 --- a/packages/ai/src/providers/google-vertex-responses.ts +++ b/packages/ai/src/providers/google-vertex-responses.ts @@ -27,7 +27,7 @@ export interface Settings extends ProviderPackage.Settings { const route = OpenAICompatibleResponses.route.with({ id: "google-vertex-responses", provider: id, - providerOptions: { openresponses: { store: false } }, + providerOptions: { store: false }, }) export const routes = [route] diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index 9921889a9309..353194d768c0 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -6,16 +6,14 @@ import { Auth } from "../route/auth.js" import { Route, type RouteDefaultsInput } from "../route/client.js" import { Endpoint } from "../route/endpoint.js" import { Framing } from "../route/framing.js" -import { ProviderID, type LLMRequest, type ModelID, type ProviderOptions } from "../schema/index.js" +import { ProviderID, type LLMRequest, type ModelID } from "../schema/index.js" import { GoogleVertexShared } from "./google-vertex-shared.js" export interface GeminiOptionsInput extends Gemini.OptionsInput { readonly labels?: Readonly> } -export type GeminiProviderOptionsInput = ProviderOptions & { - readonly gemini?: GeminiOptionsInput -} +export type GeminiProviderOptionsInput = GeminiOptionsInput export const id = ProviderID.make("google-vertex") @@ -40,7 +38,7 @@ export type Settings = ProviderPackage.Settings & const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) { const body = yield* Gemini.protocol.body.from(request) - const value = request.providerOptions?.gemini?.labels + const value = request.providerOptions?.labels const labels = ProviderShared.isRecord(value) ? Object.fromEntries( Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"), diff --git a/packages/ai/src/providers/open-responses-options.ts b/packages/ai/src/providers/open-responses-options.ts index 71bc1e2d01cd..e31f1e3d84d1 100644 --- a/packages/ai/src/providers/open-responses-options.ts +++ b/packages/ai/src/providers/open-responses-options.ts @@ -1,10 +1,6 @@ import type { Options } from "../protocols/utils/open-responses-options.js" -import type { ProviderOptions } from "../schema/index.js" export type OpenResponsesOptionsInput = Options & { readonly [key: string]: unknown } - -export type OpenResponsesProviderOptionsInput = ProviderOptions & { - readonly openresponses?: OpenResponsesOptionsInput -} +export type OpenResponsesProviderOptionsInput = OpenResponsesOptionsInput export * as OpenResponsesProviderOptions from "./open-responses-options.js" diff --git a/packages/ai/src/providers/openai-options.ts b/packages/ai/src/providers/openai-options.ts index 5f97772fa2c4..ca85642ef3ac 100644 --- a/packages/ai/src/providers/openai-options.ts +++ b/packages/ai/src/providers/openai-options.ts @@ -1,20 +1,17 @@ -import type { ProviderOptions } from "../schema/index.js" -import { mergeProviderOptions } from "../schema/index.js" +import { mergeProviderOptions, type ProviderOptions } from "../schema/index.js" import type { OpenResponsesOptionsInput } from "./open-responses-options.js" export type { OpenAIResponseIncludable, OpenAIServiceTier } from "../protocols/utils/openai-options.js" export type OpenAIOptionsInput = OpenResponsesOptionsInput -export type OpenAIProviderOptionsInput = ProviderOptions & { - readonly openai?: OpenAIOptionsInput -} +export type OpenAIProviderOptionsInput = OpenAIOptionsInput const definedEntries = (input: Record) => Object.entries(input).filter((entry) => entry[1] !== undefined) const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): ProviderOptions | undefined => { - const openai = Object.fromEntries( + const result = Object.fromEntries( definedEntries({ store: options?.store, reasoningEffort: options?.reasoningEffort, @@ -24,8 +21,8 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide serviceTier: options?.serviceTier, }), ) - if (Object.keys(openai).length === 0) return undefined - return { openai } + if (Object.keys(result).length === 0) return undefined + return result } export const gpt5DefaultOptions = ( diff --git a/packages/ai/src/providers/openrouter.ts b/packages/ai/src/providers/openrouter.ts index 1700eed67e75..a222586645dd 100644 --- a/packages/ai/src/providers/openrouter.ts +++ b/packages/ai/src/providers/openrouter.ts @@ -4,7 +4,7 @@ import { Endpoint } from "../route/endpoint.js" import { Framing } from "../route/framing.js" import { Protocol } from "../route/protocol.js" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js" -import { ProviderID, type CacheHint, type ModelID, type ProviderOptions } from "../schema/index.js" +import { ProviderID, type CacheHint, type ModelID } from "../schema/index.js" import type { ProviderPackage } from "../provider-package.js" import * as OpenAICompatibleProfiles from "./openai-compatible-profile.js" import * as OpenAIChat from "../protocols/openai-chat.js" @@ -71,9 +71,7 @@ export interface OpenRouterOptions { }> } -export type OpenRouterProviderOptionsInput = ProviderOptions & { - readonly openrouter?: OpenRouterOptions -} +export type OpenRouterProviderOptionsInput = OpenRouterOptions export type LanguageModelOptions = Omit & ProviderAuthOption<"optional"> & { @@ -120,7 +118,7 @@ export const protocol = Protocol.make({ return { ...body, messages, - ...bodyOptions(request.providerOptions?.openrouter), + ...bodyOptions(request.providerOptions), ...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}), } as OpenRouterBody }), diff --git a/packages/ai/src/providers/xai.ts b/packages/ai/src/providers/xai.ts index 00eb09fdb9fb..f9ee0d927299 100644 --- a/packages/ai/src/providers/xai.ts +++ b/packages/ai/src/providers/xai.ts @@ -1,7 +1,7 @@ import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js" import { Route, type RouteDefaultsInput } from "../route/client.js" import { Endpoint } from "../route/endpoint.js" -import { HttpOptions, ProviderID, type ModelID, type ProviderOptions } from "../schema/index.js" +import { HttpOptions, ProviderID, type ModelID } from "../schema/index.js" import * as OpenAICompatibleProfiles from "./openai-compatible-profile.js" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat.js" import * as OpenAIChat from "../protocols/openai-chat.js" @@ -12,9 +12,7 @@ import type { ProviderPackage } from "../provider-package.js" export const id = ProviderID.make("xai") -export type XAIProviderOptionsInput = ProviderOptions & { - readonly xai?: OpenAIOptionsInput -} +export type XAIProviderOptionsInput = OpenAIOptionsInput export type LanguageModelOptions = Omit & ProviderAuthOption<"optional"> & { @@ -37,7 +35,7 @@ const responsesRoute = Route.make({ protocol: OpenAIResponses.protocol, endpoint: Endpoint.path("/responses", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }), transport: OpenAIResponses.httpTransport, - defaults: { providerOptions: { xai: { store: false } } }, + defaults: { providerOptions: { store: false } }, }) const chatRoute = Route.make({ diff --git a/packages/ai/src/schema/options.ts b/packages/ai/src/schema/options.ts index 17983d20f8a0..f0a9ed510873 100644 --- a/packages/ai/src/schema/options.ts +++ b/packages/ai/src/schema/options.ts @@ -36,22 +36,12 @@ const mergeStringRecords = ( return Object.keys(result).length === 0 ? undefined : result } -export const ProviderOptions = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)) +export const ProviderOptions = Schema.Record(Schema.String, Schema.Unknown) export type ProviderOptions = Schema.Schema.Type export const mergeProviderOptions = ( ...items: ReadonlyArray -): ProviderOptions | undefined => { - const result: Record> = {} - for (const item of items) { - if (!item) continue - for (const [provider, options] of Object.entries(item)) { - const merged = mergeJsonRecords(result[provider], options) - if (merged) result[provider] = merged - } - } - return Object.keys(result).length === 0 ? undefined : result -} +): ProviderOptions | undefined => mergeJsonRecords(...items) export class HttpOptions extends Schema.Class("AI.HttpOptions")({ body: Schema.optional(JsonSchema), diff --git a/packages/ai/test/auth-options.types.ts b/packages/ai/test/auth-options.types.ts index b11a87e024e8..a9e7c2e67b27 100644 --- a/packages/ai/test/auth-options.types.ts +++ b/packages/ai/test/auth-options.types.ts @@ -81,7 +81,7 @@ OpenAI.configure({ }).responses("gpt-4.1-mini") OpenAI.configure({ generation: { maxTokens: 100 }, - providerOptions: { openai: { store: false } }, + providerOptions: { store: false }, }).responses("gpt-4.1-mini") // @ts-expect-error OpenAI model selectors only accept model ids. @@ -97,7 +97,7 @@ OpenAI.configure({ bogus: true }) OpenAI.configure({ generation: { maxTokens: "many" } }) // @ts-expect-error provider-native options remain typed. -OpenAI.configure({ providerOptions: { openai: { store: "false" } } }) +OpenAI.configure({ providerOptions: { store: "false" } }) // @ts-expect-error auth is an override, so OpenAI rejects apiKey with auth. OpenAI.configure({ apiKey: "sk-test", auth: Auth.bearer("oauth-token") }) @@ -139,7 +139,8 @@ Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku") Anthropic.configure({ apiKey: "anthropic-key", providerOptions: { - anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 }, effort: "high" }, + thinking: { type: "enabled", budgetTokens: 1_024 }, + effort: "high", }, }).model("claude-haiku") // @ts-expect-error Anthropic model selectors only accept model ids. @@ -147,15 +148,15 @@ Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku", {}) // @ts-expect-error Anthropic package settings accept only one auth source. Anthropic.model("claude-sonnet-4-6", { apiKey: "anthropic-key", authToken: "anthropic-token" }) // @ts-expect-error Enabled Anthropic thinking requires a token budget. -Anthropic.configure({ providerOptions: { anthropic: { thinking: { type: "enabled" } } } }) +Anthropic.configure({ providerOptions: { thinking: { type: "enabled" } } }) // @ts-expect-error Anthropic thinking budgets must be numbers. -Anthropic.configure({ providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: "large" } } } }) +Anthropic.configure({ providerOptions: { thinking: { type: "enabled", budgetTokens: "large" } } }) AnthropicCompatible.configure({ apiKey: "messages-key", baseURL: "https://messages.example.com/v1", provider: "example", - providerOptions: { anthropic: { thinking: { type: "disabled" } } }, + providerOptions: { thinking: { type: "disabled" } }, }).model("compatible-model") // @ts-expect-error Anthropic-compatible providers require a base URL. AnthropicCompatible.configure({ apiKey: "messages-key" }) @@ -171,16 +172,16 @@ AnthropicCompatible.model("compatible-model", { Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash") Google.configure({ apiKey: "google-key", - providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } }, + providerOptions: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } }, }).model("gemini-2.5-flash") // @ts-expect-error Google model selectors only accept model ids. Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash", {}) // @ts-expect-error Gemini thinking budgets must be numbers. -Google.configure({ providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "large" } } } }) +Google.configure({ providerOptions: { thinkingConfig: { thinkingBudget: "large" } } }) GoogleVertex.configure({ apiKey: "vertex-key", - providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1_024 } } }, + providerOptions: { thinkingConfig: { thinkingBudget: 1_024 } }, }).model("gemini-3.5-flash") GoogleVertex.configure({ accessToken: "vertex-token", project: "project" }).model("gemini-3.5-flash") GoogleVertex.configure({ auth: Auth.bearer("vertex-token"), project: "project" }).model("gemini-3.5-flash") @@ -230,7 +231,7 @@ GoogleVertexResponses.configure({ GoogleVertexMessages.configure({ accessToken: "vertex-token", project: "project", - providerOptions: { anthropic: { thinking: { type: "adaptive", display: "omitted" }, effort: "low" } }, + providerOptions: { thinking: { type: "adaptive", display: "omitted" }, effort: "low" }, }).model("claude-sonnet-4-6") // @ts-expect-error Vertex Messages package settings do not accept API keys. GoogleVertexMessages.model("claude-sonnet-4-6", { apiKey: "vertex-key", project: "project" }) diff --git a/packages/ai/test/compile.test.ts b/packages/ai/test/compile.test.ts index c7aeebf16c5f..e65d10d8bc30 100644 --- a/packages/ai/test/compile.test.ts +++ b/packages/ai/test/compile.test.ts @@ -17,31 +17,25 @@ describe("request option precedence", () => { test("deep-merges provider option records and replaces arrays, primitives, and null", () => { const merged = mergeProviderOptions( { - openai: { - include: ["route"], - metadata: { route: true, shared: "route" }, - nullable: "route", - primitive: "route", - }, + include: ["route"], + metadata: { route: true, shared: "route" }, + nullable: "route", + primitive: "route", }, { - openai: { - include: ["model"], - metadata: { model: true, shared: "model" }, - nullable: null, - primitive: "model", - }, + include: ["model"], + metadata: { model: true, shared: "model" }, + nullable: null, + primitive: "model", }, - { openai: { metadata: { request: true }, primitive: false } }, + { metadata: { request: true }, primitive: false }, ) expect(merged).toEqual({ - openai: { - include: ["model"], - metadata: { route: true, model: true, request: true, shared: "model" }, - nullable: null, - primitive: false, - }, + include: ["model"], + metadata: { route: true, model: true, request: true, shared: "model" }, + nullable: null, + primitive: false, }) }) @@ -51,13 +45,13 @@ describe("request option precedence", () => { endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test"), generation: { maxTokens: 10, temperature: 1, stop: ["route"] }, - providerOptions: { openai: { store: false, reasoningEffort: "low" } }, + providerOptions: { store: false, reasoningEffort: "low" }, }) const model = route.model({ id: "gpt-4o-mini", defaults: { generation: { maxTokens: 20, temperature: 0.5, frequencyPenalty: 0.25, stop: ["model"] }, - providerOptions: { openai: { reasoningEffort: "medium" } }, + providerOptions: { reasoningEffort: "medium" }, }, }) const prepared = yield* compileRequest( @@ -65,7 +59,7 @@ describe("request option precedence", () => { model, prompt: "Say hello.", generation: { maxTokens: 30, topP: 0.9, stop: ["request"] }, - providerOptions: { openai: { store: true } }, + providerOptions: { store: true }, }), ) diff --git a/packages/ai/test/continuation-scenarios.ts b/packages/ai/test/continuation-scenarios.ts index c752d9d4bb24..5c9dd61c8eb1 100644 --- a/packages/ai/test/continuation-scenarios.ts +++ b/packages/ai/test/continuation-scenarios.ts @@ -105,7 +105,7 @@ export function continuationRequest(input: { tools: features.has("tool-call") ? [continuationTool] : [], cache: "none", providerOptions: features.has("encrypted-reasoning") - ? { openai: { store: false, include: ["reasoning.encrypted_content"], reasoningSummary: "auto" } } + ? { store: false, include: ["reasoning.encrypted_content"], reasoningSummary: "auto" } : undefined, generation: { maxTokens: 80, temperature: 0 }, }) diff --git a/packages/ai/test/llm-option-types.types.ts b/packages/ai/test/llm-option-types.types.ts index 30d785dde630..de9d009c6369 100644 --- a/packages/ai/test/llm-option-types.types.ts +++ b/packages/ai/test/llm-option-types.types.ts @@ -13,9 +13,7 @@ interface ExampleOptions { readonly mode?: "fast" | "thorough" } -type ExampleProviderOptions = ProviderOptions & { - readonly example?: ExampleOptions -} +type ExampleProviderOptions = ProviderOptions & ExampleOptions const model = OpenAIChat.route .with({ endpoint: { baseURL: "https://example.com/v1" } }) @@ -26,7 +24,7 @@ type StreamRequirements = T extends Stream.Stream = [A, B] extends [B, A] ? true : false type Assert = T -LLM.request({ model, prompt: "Hello", providerOptions: { example: { mode: "fast" } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { mode: "fast" } }) LLM.request({ model, prompt: "Hello", providerOptions: { future: { option: true } } }) const generated = LLM.generate(LLM.request({ model, prompt: "Hello" })) @@ -38,14 +36,14 @@ LLM.request({ model, prompt: "Hello", // @ts-expect-error Known provider options preserve their value types. - providerOptions: { example: { mode: "slow" } }, + providerOptions: { mode: "slow" }, }) const generatedObject = LLM.generateObject({ model, prompt: "Hello", schema: Schema.Struct({ answer: Schema.String }), - providerOptions: { example: { mode: "thorough" } }, + providerOptions: { mode: "thorough" }, }) type GenerateObjectRequirements = Assert, LLMClientService>> @@ -61,13 +59,13 @@ LLM.generateObject({ prompt: "Hello", jsonSchema: { type: "object" }, // @ts-expect-error Dynamic object generation uses the selected model's provider options. - providerOptions: { example: { mode: false } }, + providerOptions: { mode: false }, }) declare const generic: LanguageModel LLM.request({ model: generic, prompt: "Hello", providerOptions: { arbitrary: { option: true } } }) -const options: LanguageModelProviderOptions = { example: { mode: "fast" } } +const options: LanguageModelProviderOptions = { mode: "fast" } void (options satisfies LanguageModelProviderOptions) void (true satisfies GenerateRequirements) void (true satisfies StreamClientRequirements) diff --git a/packages/ai/test/llm.test.ts b/packages/ai/test/llm.test.ts index f2427c3ed59a..6a2fcb753828 100644 --- a/packages/ai/test/llm.test.ts +++ b/packages/ai/test/llm.test.ts @@ -59,18 +59,18 @@ describe("llm constructors", () => { provider: "fake", route: chatRoute.with({ generation: { maxTokens: 100, temperature: 1 }, - providerOptions: { openai: { store: false, metadata: { model: true } } }, + providerOptions: { store: false, metadata: { model: true } }, http: { body: { metadata: { model: true } }, headers: { "x-shared": "model" }, query: { model: "1" } }, }), }), prompt: "Say hello.", generation: { temperature: 0 }, - providerOptions: { openai: { store: true, metadata: { request: true } } }, + providerOptions: { store: true, metadata: { request: true } }, http: { body: { metadata: { request: true } }, headers: { "x-shared": "request" }, query: { request: "1" } }, }) expect(request.generation).toEqual({ temperature: 0 }) - expect(request.providerOptions).toEqual({ openai: { store: true, metadata: { request: true } } }) + expect(request.providerOptions).toEqual({ store: true, metadata: { request: true } }) expect(request.http).toEqual({ body: { metadata: { request: true } }, headers: { "x-shared": "request" }, @@ -123,7 +123,7 @@ describe("llm constructors", () => { defaults: { limits: { context: 128_000, output: 8_192 }, generation: { maxTokens: 1_024, stop: ["END"] }, - providerOptions: { openai: { parallelToolCalls: false } }, + providerOptions: { parallelToolCalls: false }, http: { body: { extra_body: true } }, }, compatibility: { toolSchema: "moonshot" }, @@ -132,7 +132,7 @@ describe("llm constructors", () => { expect(request.model.defaults?.limits).toEqual({ context: 128_000, output: 8_192 }) expect(request.model.defaults?.generation).toEqual({ maxTokens: 1_024, stop: ["END"] }) - expect(request.model.defaults?.providerOptions).toEqual({ openai: { parallelToolCalls: false } }) + expect(request.model.defaults?.providerOptions).toEqual({ parallelToolCalls: false }) expect(request.model.defaults?.http).toEqual({ body: { extra_body: true } }) expect(request.model.compatibility).toEqual({ toolSchema: "moonshot" }) expect(request.generation).toBeUndefined() diff --git a/packages/ai/test/provider-options/anthropic-compatible.types.ts b/packages/ai/test/provider-options/anthropic-compatible.types.ts index 6fd690e5ab1d..c55924c070eb 100644 --- a/packages/ai/test/provider-options/anthropic-compatible.types.ts +++ b/packages/ai/test/provider-options/anthropic-compatible.types.ts @@ -3,11 +3,11 @@ import { AnthropicCompatible } from "../../src/providers.js" const model = AnthropicCompatible.configure({ baseURL: "https://example.com" }).model("claude") -LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { effort: "high" } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { effort: "high" } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error Anthropic effort must be a string. - providerOptions: { anthropic: { effort: 1 } }, + providerOptions: { effort: 1 }, }) diff --git a/packages/ai/test/provider-options/anthropic.types.ts b/packages/ai/test/provider-options/anthropic.types.ts index 7d241643bd70..6da715749f90 100644 --- a/packages/ai/test/provider-options/anthropic.types.ts +++ b/packages/ai/test/provider-options/anthropic.types.ts @@ -3,11 +3,11 @@ import { Anthropic } from "../../src/providers.js" const model = Anthropic.provider.model("claude-sonnet-4-5") -LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { thinking: { type: "adaptive" } } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { thinking: { type: "adaptive" } } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error Anthropic thinking modes are a fixed union. - providerOptions: { anthropic: { thinking: { type: "automatic" } } }, + providerOptions: { thinking: { type: "automatic" } }, }) diff --git a/packages/ai/test/provider-options/azure.types.ts b/packages/ai/test/provider-options/azure.types.ts index 015c85bed9c3..a915e8fbd6f8 100644 --- a/packages/ai/test/provider-options/azure.types.ts +++ b/packages/ai/test/provider-options/azure.types.ts @@ -3,11 +3,11 @@ import { Azure } from "../../src/providers.js" const model = Azure.configure({ resourceName: "example" }).responses("deployment") -LLM.request({ model, prompt: "Hello", providerOptions: { openai: { store: false } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { store: false } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error Azure OpenAI store must be boolean. - providerOptions: { openai: { store: "false" } }, + providerOptions: { store: "false" }, }) diff --git a/packages/ai/test/provider-options/google-vertex-chat.types.ts b/packages/ai/test/provider-options/google-vertex-chat.types.ts index 45095dd5f445..2e748e65377b 100644 --- a/packages/ai/test/provider-options/google-vertex-chat.types.ts +++ b/packages/ai/test/provider-options/google-vertex-chat.types.ts @@ -3,11 +3,11 @@ import { GoogleVertexChat } from "../../src/providers.js" const model = GoogleVertexChat.configure({ accessToken: "test", project: "project" }).model("gemini") -LLM.request({ model, prompt: "Hello", providerOptions: { openai: { serviceTier: "priority" } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { serviceTier: "priority" } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error Vertex OpenAI-compatible service tiers use the OpenAI union. - providerOptions: { openai: { serviceTier: "premium" } }, + providerOptions: { serviceTier: "premium" }, }) diff --git a/packages/ai/test/provider-options/google-vertex-messages.types.ts b/packages/ai/test/provider-options/google-vertex-messages.types.ts index aff99972e9ce..709269b7df3f 100644 --- a/packages/ai/test/provider-options/google-vertex-messages.types.ts +++ b/packages/ai/test/provider-options/google-vertex-messages.types.ts @@ -3,11 +3,11 @@ import { GoogleVertexMessages } from "../../src/providers.js" const model = GoogleVertexMessages.configure({ accessToken: "test", project: "project" }).model("claude") -LLM.request({ model, prompt: "Hello", providerOptions: { anthropic: { effort: "medium" } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { effort: "medium" } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error Vertex Anthropic effort must be a string. - providerOptions: { anthropic: { effort: false } }, + providerOptions: { effort: false }, }) diff --git a/packages/ai/test/provider-options/google-vertex-responses.types.ts b/packages/ai/test/provider-options/google-vertex-responses.types.ts index 239244a64490..345e1fc82edb 100644 --- a/packages/ai/test/provider-options/google-vertex-responses.types.ts +++ b/packages/ai/test/provider-options/google-vertex-responses.types.ts @@ -3,12 +3,12 @@ import { GoogleVertexResponses } from "../../src/providers.js" const model = GoogleVertexResponses.configure({ accessToken: "test", project: "project" }).model("gemini") -LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { textVerbosity: "high" } } }) -LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { textVerbosity: "verbose" } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { textVerbosity: "high" } }) +LLM.request({ model, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error Vertex Responses verbosity must be a string. - providerOptions: { openresponses: { textVerbosity: 1 } }, + providerOptions: { textVerbosity: 1 }, }) diff --git a/packages/ai/test/provider-options/google-vertex.types.ts b/packages/ai/test/provider-options/google-vertex.types.ts index 618f9069daa8..255e7bc06360 100644 --- a/packages/ai/test/provider-options/google-vertex.types.ts +++ b/packages/ai/test/provider-options/google-vertex.types.ts @@ -6,12 +6,12 @@ const model = GoogleVertex.provider.configure({ apiKey: "test" }).model("gemini- LLM.request({ model, prompt: "Hello", - providerOptions: { gemini: { thinkingConfig: { includeThoughts: true } } }, + providerOptions: { thinkingConfig: { includeThoughts: true } }, }) LLM.request({ model, prompt: "Hello", // @ts-expect-error Vertex Gemini includeThoughts must be boolean. - providerOptions: { gemini: { thinkingConfig: { includeThoughts: "yes" } } }, + providerOptions: { thinkingConfig: { includeThoughts: "yes" } }, }) diff --git a/packages/ai/test/provider-options/google.types.ts b/packages/ai/test/provider-options/google.types.ts index 9aae75efcb6f..83d8fe93383a 100644 --- a/packages/ai/test/provider-options/google.types.ts +++ b/packages/ai/test/provider-options/google.types.ts @@ -6,17 +6,15 @@ const model = Google.provider.model("gemini-2.5-pro") LLM.request({ model, prompt: "Hello", - providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1024 } } }, + providerOptions: { thinkingConfig: { thinkingBudget: 1024 } }, }) LLM.request({ model, prompt: "Hello", providerOptions: { - gemini: { - // @ts-expect-error Gemini safety settings require a threshold for every category. - safetySettings: [{ category: "HARM_CATEGORY_HATE_SPEECH" }], - }, + // @ts-expect-error Gemini safety settings require a threshold for every category. + safetySettings: [{ category: "HARM_CATEGORY_HATE_SPEECH" }], }, }) @@ -24,12 +22,10 @@ LLM.request({ model, prompt: "Hello", providerOptions: { - gemini: { - cachedContent: "cachedContents/example", - safetySettings: [{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" }], - serviceTier: "future-tier", - thinkingConfig: { thinkingLevel: "high", includeThoughts: true }, - }, + cachedContent: "cachedContents/example", + safetySettings: [{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" }], + serviceTier: "future-tier", + thinkingConfig: { thinkingLevel: "high", includeThoughts: true }, }, }) @@ -37,11 +33,11 @@ LLM.request({ model, prompt: "Hello", // @ts-expect-error Gemini thinking budgets must be numeric. - providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "large" } } }, + providerOptions: { thinkingConfig: { thinkingBudget: "large" } }, }) LLM.request({ model, prompt: "Hello", - providerOptions: { gemini: { thinkingConfig: { thinkingLevel: "maximum" } } }, + providerOptions: { thinkingConfig: { thinkingLevel: "maximum" } }, }) diff --git a/packages/ai/test/provider-options/openai-compatible-responses.types.ts b/packages/ai/test/provider-options/openai-compatible-responses.types.ts index 428ef5187292..96a7c512e5f8 100644 --- a/packages/ai/test/provider-options/openai-compatible-responses.types.ts +++ b/packages/ai/test/provider-options/openai-compatible-responses.types.ts @@ -3,15 +3,15 @@ import { OpenAICompatibleResponses } from "../../src/providers.js" const model = OpenAICompatibleResponses.configure({ baseURL: "https://example.com" }).model("model") -LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { reasoningSummary: "detailed" } } }) -LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { reasoningEffort: "high" } } }) -LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { reasoningEffort: "experimental" } } }) -LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { textVerbosity: "low" } } }) -LLM.request({ model, prompt: "Hello", providerOptions: { openresponses: { textVerbosity: "verbose" } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { reasoningSummary: "detailed" } }) +LLM.request({ model, prompt: "Hello", providerOptions: { reasoningEffort: "high" } }) +LLM.request({ model, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } }) +LLM.request({ model, prompt: "Hello", providerOptions: { textVerbosity: "low" } }) +LLM.request({ model, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error Open Responses reasoning summaries use a fixed union. - providerOptions: { openresponses: { reasoningSummary: "full" } }, + providerOptions: { reasoningSummary: "full" }, }) diff --git a/packages/ai/test/provider-options/openai-compatible.types.ts b/packages/ai/test/provider-options/openai-compatible.types.ts index 75044689975f..ccd0f8e6a85e 100644 --- a/packages/ai/test/provider-options/openai-compatible.types.ts +++ b/packages/ai/test/provider-options/openai-compatible.types.ts @@ -3,11 +3,11 @@ import { OpenAICompatible } from "../../src/providers.js" const model = OpenAICompatible.deepseek.model("deepseek-chat") -LLM.request({ model, prompt: "Hello", providerOptions: { openai: { store: false } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { store: false } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error OpenAI-compatible store must be boolean. - providerOptions: { openai: { store: "false" } }, + providerOptions: { store: "false" }, }) diff --git a/packages/ai/test/provider-options/openai.types.ts b/packages/ai/test/provider-options/openai.types.ts index e0cfe9d273df..6419070b3555 100644 --- a/packages/ai/test/provider-options/openai.types.ts +++ b/packages/ai/test/provider-options/openai.types.ts @@ -4,18 +4,18 @@ import { OpenAI } from "../../src/providers.js" const selected = OpenAI.responses("gpt-5") const chat = OpenAI.chat("gpt-4o-mini") -LLM.request({ model: selected, prompt: "Hello", providerOptions: { openai: { reasoningEffort: "high" } } }) -LLM.request({ model: selected, prompt: "Hello", providerOptions: { openai: { reasoningEffort: "experimental" } } }) -LLM.request({ model: selected, prompt: "Hello", providerOptions: { openai: { textVerbosity: "low" } } }) -LLM.request({ model: selected, prompt: "Hello", providerOptions: { openai: { textVerbosity: "verbose" } } }) -LLM.request({ model: chat, prompt: "Hello", providerOptions: { openai: { reasoningEffort: "max" } } }) -LLM.request({ model: chat, prompt: "Hello", providerOptions: { openai: { reasoningEffort: "experimental" } } }) +LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffort: "high" } }) +LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } }) +LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "low" } }) +LLM.request({ model: selected, prompt: "Hello", providerOptions: { textVerbosity: "verbose" } }) +LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "max" } }) +LLM.request({ model: chat, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } }) LLM.request({ model: selected, prompt: "Hello", // @ts-expect-error OpenAI reasoning effort must be a string. - providerOptions: { openai: { reasoningEffort: 1 } }, + providerOptions: { reasoningEffort: 1 }, }) OpenAI.configure({ diff --git a/packages/ai/test/provider-options/openrouter.types.ts b/packages/ai/test/provider-options/openrouter.types.ts index 90e3894a01ec..c0dbe392419a 100644 --- a/packages/ai/test/provider-options/openrouter.types.ts +++ b/packages/ai/test/provider-options/openrouter.types.ts @@ -3,27 +3,25 @@ import { OpenRouter } from "../../src/providers.js" const model = OpenRouter.provider.model("anthropic/claude-sonnet-4.5") -LLM.request({ model, prompt: "Hello", providerOptions: { openrouter: { usage: true } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { usage: true } }) LLM.request({ model, prompt: "Hello", providerOptions: { - openrouter: { - models: ["google/gemini-3.1-pro"], - provider: { - order: ["anthropic"], - require_parameters: true, - data_collection: "future-policy", - sort: "future-sort", - max_price: { prompt: "0.50" }, - }, - reasoning: { effort: "future-effort", exclude: false }, - plugins: [{ id: "future-plugin", enabled: true }], - web_search_options: { engine: "future-engine" }, - debug: { echo_upstream_body: true }, - user: "user_123", + models: ["google/gemini-3.1-pro"], + provider: { + order: ["anthropic"], + require_parameters: true, + data_collection: "future-policy", + sort: "future-sort", + max_price: { prompt: "0.50" }, }, + reasoning: { effort: "future-effort", exclude: false }, + plugins: [{ id: "future-plugin", enabled: true }], + web_search_options: { engine: "future-engine" }, + debug: { echo_upstream_body: true }, + user: "user_123", }, }) @@ -31,5 +29,5 @@ LLM.request({ model, prompt: "Hello", // @ts-expect-error OpenRouter usage must be boolean or an option record. - providerOptions: { openrouter: { usage: "yes" } }, + providerOptions: { usage: "yes" }, }) diff --git a/packages/ai/test/provider-options/xai.types.ts b/packages/ai/test/provider-options/xai.types.ts index 47da83ecb143..9bad4a83b717 100644 --- a/packages/ai/test/provider-options/xai.types.ts +++ b/packages/ai/test/provider-options/xai.types.ts @@ -3,12 +3,12 @@ import { XAI } from "../../src/providers.js" const model = XAI.provider.model("grok-4") -LLM.request({ model, prompt: "Hello", providerOptions: { xai: { reasoningEffort: "high" } } }) -LLM.request({ model, prompt: "Hello", providerOptions: { xai: { reasoningEffort: "experimental" } } }) +LLM.request({ model, prompt: "Hello", providerOptions: { reasoningEffort: "high" } }) +LLM.request({ model, prompt: "Hello", providerOptions: { reasoningEffort: "experimental" } }) LLM.request({ model, prompt: "Hello", // @ts-expect-error xAI's OpenAI-compatible reasoning effort must be a string. - providerOptions: { xai: { reasoningEffort: true } }, + providerOptions: { reasoningEffort: true }, }) diff --git a/packages/ai/test/provider-package.test.ts b/packages/ai/test/provider-package.test.ts index c06c928f5652..fef3d8a9037b 100644 --- a/packages/ai/test/provider-package.test.ts +++ b/packages/ai/test/provider-package.test.ts @@ -47,11 +47,11 @@ describe("provider package entrypoints", () => { } const openrouter = OpenRouter.model("anthropic/claude-sonnet-4", { ...settings, - providerOptions: { openrouter: { usage: true } }, + providerOptions: { usage: true }, }) const xai = XAI.model("grok-4", { ...settings, - providerOptions: { xai: { reasoningEffort: "high" } }, + providerOptions: { reasoningEffort: "high" }, }) for (const selected of [openrouter, xai]) { @@ -60,8 +60,8 @@ describe("provider package entrypoints", () => { expect(selected.route.defaults.http?.body).toEqual(settings.body) expect(selected.route.defaults.limits).toEqual(settings.limits) } - expect(openrouter.route.defaults.providerOptions).toEqual({ openrouter: { usage: true } }) - expect(xai.route.defaults.providerOptions).toMatchObject({ xai: { reasoningEffort: "high", store: false } }) + expect(openrouter.route.defaults.providerOptions).toEqual({ usage: true }) + expect(xai.route.defaults.providerOptions).toMatchObject({ reasoningEffort: "high", store: false }) }) test("maps package settings onto the executable model", () => { @@ -89,7 +89,7 @@ describe("provider package entrypoints", () => { headers: { "x-application": "opencode" }, body: { service_tier: "priority" }, limits: { context: 200_000, output: 64_000 }, - providerOptions: { openresponses: { reasoningEffort: "low", store: true } }, + providerOptions: { reasoningEffort: "low", store: true }, }) expect(String(selected.provider)).toBe("example") @@ -101,9 +101,7 @@ describe("provider package entrypoints", () => { expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" }) expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) - expect(selected.route.defaults.providerOptions).toEqual({ - openresponses: { reasoningEffort: "low", store: true }, - }) + expect(selected.route.defaults.providerOptions).toEqual({ reasoningEffort: "low", store: true }) }) test("maps Anthropic-compatible settings onto the executable model", async () => { @@ -115,7 +113,7 @@ describe("provider package entrypoints", () => { headers: { "x-application": "opencode" }, body: { metadata: { user_id: "user_1" } }, limits: { context: 200_000, output: 64_000 }, - providerOptions: { anthropic: { effort: "low" } }, + providerOptions: { effort: "low" }, }) expect(String(selected.provider)).toBe("example") @@ -127,19 +125,17 @@ describe("provider package entrypoints", () => { expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) expect(selected.route.defaults.http?.body).toEqual({ metadata: { user_id: "user_1" } }) expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 }) - expect(selected.route.defaults.providerOptions).toEqual({ anthropic: { effort: "low" } }) + expect(selected.route.defaults.providerOptions).toEqual({ effort: "low" }) }) test("maps Anthropic provider options onto the executable model", async () => { const Anthropic = await import("@opencode-ai/ai/providers/anthropic") const selected = Anthropic.model("claude-sonnet-4-6", { apiKey: "fixture", - providerOptions: { anthropic: { thinking: { type: "adaptive" } } }, + providerOptions: { thinking: { type: "adaptive" } }, }) - expect(selected.route.defaults.providerOptions).toEqual({ - anthropic: { thinking: { type: "adaptive" } }, - }) + expect(selected.route.defaults.providerOptions).toEqual({ thinking: { type: "adaptive" } }) }) test("requires an Anthropic-compatible base URL at runtime", async () => { @@ -233,7 +229,7 @@ describe("provider package entrypoints", () => { headers: { "x-application": "opencode" }, body: { safetySettings: [] }, limits: { context: 1_000_000, output: 65_536 }, - providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1_024 } } }, + providerOptions: { thinkingConfig: { thinkingBudget: 1_024 } }, }) expect(selected.route.id).toBe("gemini") @@ -241,9 +237,7 @@ describe("provider package entrypoints", () => { expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" }) expect(selected.route.defaults.http?.body).toEqual({ safetySettings: [] }) expect(selected.route.defaults.limits).toEqual({ context: 1_000_000, output: 65_536 }) - expect(selected.route.defaults.providerOptions).toEqual({ - gemini: { thinkingConfig: { thinkingBudget: 1_024 } }, - }) + expect(selected.route.defaults.providerOptions).toEqual({ thinkingConfig: { thinkingBudget: 1_024 } }) }) test("selects Vertex entrypoints with the same model contract", async () => { @@ -305,7 +299,7 @@ describe("provider package entrypoints", () => { baseURL: "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/endpoints/openapi", path: "/responses", }) - expect(responses.route.defaults.providerOptions).toEqual({ openresponses: { store: false } }) + expect(responses.route.defaults.providerOptions).toEqual({ store: false }) }) test("rejects conflicting Vertex auth settings at runtime", async () => { diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 8c9dedf9e53e..8eb9ef31394b 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -63,7 +63,8 @@ describe("Anthropic Messages route", () => { const prepared = yield* compileRequest( LLMRequest.update(request, { providerOptions: { - anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, + thinking: { type: "adaptive", display: "summarized" }, + effort: "low", }, }), ) @@ -79,17 +80,17 @@ describe("Anthropic Messages route", () => { Effect.gen(function* () { const enabled = yield* compileRequest( LLMRequest.update(request, { - providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } }, + providerOptions: { thinking: { type: "enabled", budgetTokens: 1_024 } }, }), ) const legacy = yield* compileRequest( LLMRequest.update(request, { - providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } }, + providerOptions: { thinking: { type: "enabled", budget_tokens: 2_048 } }, }), ) const disabled = yield* compileRequest( LLMRequest.update(request, { - providerOptions: { anthropic: { thinking: { type: "disabled" } } }, + providerOptions: { thinking: { type: "disabled" } }, }), ) @@ -103,7 +104,7 @@ describe("Anthropic Messages route", () => { Effect.gen(function* () { const error = yield* compileRequest( LLMRequest.update(request, { - providerOptions: { anthropic: { thinking: { type: "enabled" } } }, + providerOptions: { thinking: { type: "enabled" } }, }), ).pipe(Effect.flip) @@ -1062,9 +1063,7 @@ describe("Anthropic Messages route", () => { ), ) - expect(response.toolCalls).toMatchObject([ - { id: "call_1", name: "lookup", input: { query: "weather" } }, - ]) + expect(response.toolCalls).toMatchObject([{ id: "call_1", name: "lookup", input: { query: "weather" } }]) expect(response.finishReason).toEqual({ normalized: "tool-calls", raw: "tool_use" }) }), ) diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 83a418165d32..60d536736dc0 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -48,28 +48,26 @@ describe("Gemini route", () => { const prepared = yield* compileRequest( LLMRequest.update(request, { providerOptions: { - gemini: { - cachedContent: "cachedContents/example", - safetySettings: [{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" }], - serviceTier: "priority", - thinkingConfig: { thinkingBudget: 0, includeThoughts: false, thinkingLevel: "high" }, - }, + cachedContent: "cachedContents/example", + safetySettings: [{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" }], + serviceTier: "priority", + thinkingConfig: { thinkingBudget: 0, includeThoughts: false, thinkingLevel: "high" }, }, }), ) const filtered = yield* compileRequest( LLMRequest.update(request, { - providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } }, + providerOptions: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } }, }), ) const defaulted = yield* compileRequest( LLMRequest.update(request, { - providerOptions: { gemini: { thinkingConfig: { thinkingLevel: "high" } } }, + providerOptions: { thinkingConfig: { thinkingLevel: "high" } }, }), ) const emptySafetySettings = yield* compileRequest( LLMRequest.update(request, { - providerOptions: { gemini: { safetySettings: [] } }, + providerOptions: { safetySettings: [] }, }), ) diff --git a/packages/ai/test/provider/google-vertex.test.ts b/packages/ai/test/provider/google-vertex.test.ts index c0b310ffd12c..a2275d602f63 100644 --- a/packages/ai/test/provider/google-vertex.test.ts +++ b/packages/ai/test/provider/google-vertex.test.ts @@ -62,7 +62,7 @@ describe("Google Vertex providers", () => { accessToken: "vertex-token", project: "vertex-project", providerOptions: { - gemini: { labels: { component: "opencode", environment: "test" } }, + labels: { component: "opencode", environment: "test" }, }, }).model("gemini-3.5-flash"), prompt: "Say hello.", diff --git a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts index b4c859bccef0..57b147d2df5f 100644 --- a/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts +++ b/packages/ai/test/provider/openai-chat-reasoning.recorded.test.ts @@ -15,7 +15,7 @@ const cases = [ model: LanguageModel.update( OpenRouter.configure({ apiKey: process.env.OPENROUTER_API_KEY ?? "fixture", - providerOptions: { openrouter: { reasoning: { max_tokens: 1024 } } }, + providerOptions: { reasoning: { max_tokens: 1024 } }, }).model("anthropic/claude-sonnet-4.6"), { compatibility: { reasoningField: "reasoning" } }, ), diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index e7a8532e2ef6..3fc672aa12a4 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -165,7 +165,7 @@ describe("OpenAI Chat route", () => { LLM.request({ model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-4o-mini"), prompt: "think", - providerOptions: { openai: { reasoningEffort: "max" } }, + providerOptions: { reasoningEffort: "max" }, }), ) @@ -221,7 +221,7 @@ describe("OpenAI Chat route", () => { LLM.request({ model, prompt: "think", - providerOptions: { openai: { reasoningEffort: "experimental" } }, + providerOptions: { reasoningEffort: "experimental" }, }), ) diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts index 005d07aac65f..7d38e4267cc9 100644 --- a/packages/ai/test/provider/openai-compatible-responses.test.ts +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -118,20 +118,18 @@ describe("Open Responses-compatible route", () => { }), ) - it.effect("reads standard options from the Open Responses namespace", () => + it.effect("reads standard Open Responses options", () => Effect.gen(function* () { const model = configure({ apiKey: "test-key", baseURL: "https://responses.example.test/v1", providerOptions: { - openresponses: { - reasoningEffort: "low", - store: true, - truncation: "auto", - allowedTools: { toolNames: ["lookup"] }, - maxToolCalls: 2, - parallelToolCalls: false, - }, + reasoningEffort: "low", + store: true, + truncation: "auto", + allowedTools: { toolNames: ["lookup"] }, + maxToolCalls: 2, + parallelToolCalls: false, }, }).model("example-model") const prepared = yield* compileRequest( diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index db4d9f6ea13d..dab5c5fe406e 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -159,8 +159,8 @@ describe("OpenAI Responses route", () => { it.effect("lowers semantic service tier options", () => Effect.gen(function* () { - const input = LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "priority" } } }) - expect(input.providerOptions).toEqual({ openai: { serviceTier: "priority" } }) + const input = LLMRequest.update(request, { providerOptions: { serviceTier: "priority" } }) + expect(input.providerOptions).toEqual({ serviceTier: "priority" }) const prepared = yield* compileRequest(input) expect(prepared.body).toMatchObject({ service_tier: "priority" }) @@ -171,7 +171,7 @@ describe("OpenAI Responses route", () => { it.effect("passes through custom OpenAI reasoning effort strings", () => Effect.gen(function* () { const prepared = yield* compileRequest( - LLMRequest.update(request, { providerOptions: { openai: { reasoningEffort: "experimental" } } }), + LLMRequest.update(request, { providerOptions: { reasoningEffort: "experimental" } }), ) expect(prepared.body.reasoning).toEqual({ effort: "experimental" }) @@ -181,7 +181,7 @@ describe("OpenAI Responses route", () => { it.effect("passes through custom OpenAI text verbosity strings", () => Effect.gen(function* () { const prepared = yield* compileRequest( - LLMRequest.update(request, { providerOptions: { openai: { textVerbosity: "verbose" } } }), + LLMRequest.update(request, { providerOptions: { textVerbosity: "verbose" } }), ) expect(prepared.body.text).toEqual({ verbosity: "verbose" }) @@ -191,7 +191,7 @@ describe("OpenAI Responses route", () => { it.effect("omits unsupported semantic service tiers", () => Effect.gen(function* () { const prepared = yield* compileRequest( - LLMRequest.update(request, { providerOptions: { openai: { serviceTier: "unsupported" } } }), + LLMRequest.update(request, { providerOptions: { serviceTier: "unsupported" } }), ) expect(prepared.body).not.toHaveProperty("service_tier") @@ -1290,15 +1290,13 @@ describe("OpenAI Responses route", () => { ], toolChoice: "none", providerOptions: { - openai: { - reasoningEffort: "high", - reasoningSummary: "auto", - include: ["reasoning.encrypted_content"], - truncation: "disabled", - allowedTools: { toolNames: ["read", "grep"], mode: "required" }, - maxToolCalls: 4, - parallelToolCalls: false, - }, + reasoningEffort: "high", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + truncation: "disabled", + allowedTools: { toolNames: ["read", "grep"], mode: "required" }, + maxToolCalls: 4, + parallelToolCalls: false, }, }), ) @@ -1329,9 +1327,7 @@ describe("OpenAI Responses route", () => { model, prompt: "hi", providerOptions: { - openai: { - include: ["reasoning.encrypted_content", "code_interpreter_call.outputs", "web_search_call.results"], - }, + include: ["reasoning.encrypted_content", "code_interpreter_call.outputs", "web_search_call.results"], }, }), ) @@ -1350,7 +1346,7 @@ describe("OpenAI Responses route", () => { LLM.request({ model, prompt: "hi", - providerOptions: { openai: { include: ["reasoning.encrypted_content", "bogus.thing"] } }, + providerOptions: { include: ["reasoning.encrypted_content", "bogus.thing"] }, }), ) @@ -1360,9 +1356,7 @@ describe("OpenAI Responses route", () => { it.effect("treats an explicit empty include as no include at all", () => Effect.gen(function* () { - const prepared = yield* compileRequest( - LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }), - ) + const prepared = yield* compileRequest(LLM.request({ model, prompt: "hi", providerOptions: { include: [] } })) expect(prepared.body.include).toBeUndefined() }), @@ -1371,7 +1365,7 @@ describe("OpenAI Responses route", () => { it.effect("passes an unknown includable value through", () => Effect.gen(function* () { const prepared = yield* compileRequest( - LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }), + LLM.request({ model, prompt: "hi", providerOptions: { include: ["bogus.thing"] } }), ) expect(prepared.body.include).toEqual(["bogus.thing"]) @@ -1380,9 +1374,7 @@ describe("OpenAI Responses route", () => { it.effect("omits include when no include is set", () => Effect.gen(function* () { - const prepared = yield* compileRequest( - LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }), - ) + const prepared = yield* compileRequest(LLM.request({ model, prompt: "hi", providerOptions: { store: false } })) expect(prepared.body.include).toBeUndefined() }), @@ -1413,7 +1405,7 @@ describe("OpenAI Responses route", () => { LLM.request({ model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"), prompt: "hi", - providerOptions: { openai: { include: [] } }, + providerOptions: { include: [] }, }), ) @@ -1737,7 +1729,7 @@ describe("OpenAI Responses route", () => { it.effect("streams each reasoning summary part as a separate block", () => Effect.gen(function* () { const response = yield* LLMClient.generate( - LLMRequest.update(request, { providerOptions: { openai: { store: false } } }), + LLMRequest.update(request, { providerOptions: { store: false } }), ).pipe( Effect.provide( fixedResponse( @@ -1791,9 +1783,7 @@ describe("OpenAI Responses route", () => { it.effect("closes reasoning summary parts when storage is not disabled", () => Effect.gen(function* () { - const response = yield* LLMClient.generate( - LLMRequest.update(request, { providerOptions: { openai: { store: true } } }), - ).pipe( + const response = yield* LLMClient.generate(LLMRequest.update(request, { providerOptions: { store: true } })).pipe( Effect.provide( fixedResponse( sseEvents( @@ -1847,7 +1837,7 @@ describe("OpenAI Responses route", () => { ]), Message.user("Summarize it."), ], - providerOptions: { openai: { store: false } }, + providerOptions: { store: false }, }), ).pipe( Effect.provide( @@ -1906,7 +1896,7 @@ describe("OpenAI Responses route", () => { { type: "text", text: "After." }, ]), ], - providerOptions: { openai: { store: false } }, + providerOptions: { store: false }, }), ) @@ -1936,7 +1926,7 @@ describe("OpenAI Responses route", () => { }, ]), ], - providerOptions: { openai: { store: true } }, + providerOptions: { store: true }, }), ) @@ -1969,7 +1959,7 @@ describe("OpenAI Responses route", () => { ]), Message.user("Continue."), ], - providerOptions: { openai: { store: true } }, + providerOptions: { store: true }, }), ) @@ -2042,7 +2032,7 @@ describe("OpenAI Responses route", () => { }, ]), ], - providerOptions: { openai: { store: false } }, + providerOptions: { store: false }, }), ) @@ -2082,7 +2072,7 @@ describe("OpenAI Responses route", () => { ]), Message.user("Summarize it."), ], - providerOptions: { openai: { store: false } }, + providerOptions: { store: false }, }), ) diff --git a/packages/ai/test/provider/openrouter.test.ts b/packages/ai/test/provider/openrouter.test.ts index 93399cdbd70a..c8fdce503ce4 100644 --- a/packages/ai/test/provider/openrouter.test.ts +++ b/packages/ai/test/provider/openrouter.test.ts @@ -141,7 +141,7 @@ describe("OpenRouter", () => { LLM.request({ model: OpenRouter.configure({ apiKey: "test-key", - providerOptions: { openrouter: { usage: false } }, + providerOptions: { usage: false }, }).model("openai/gpt-4o-mini"), cache: "none", prompt: "Hello", @@ -159,17 +159,15 @@ describe("OpenRouter", () => { model: OpenRouter.configure({ apiKey: "test-key", providerOptions: { - openrouter: { - usage: true, - reasoning: { effort: "high" }, - models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"], - provider: { order: ["anthropic", "google"], require_parameters: true }, - plugins: [{ id: "response-healing" }], - web_search_options: { engine: "native", max_results: 3 }, - debug: { echo_upstream_body: true }, - user: "user_123", - future_option: { enabled: true }, - }, + usage: true, + reasoning: { effort: "high" }, + models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"], + provider: { order: ["anthropic", "google"], require_parameters: true }, + plugins: [{ id: "response-healing" }], + web_search_options: { engine: "native", max_results: 3 }, + debug: { echo_upstream_body: true }, + user: "user_123", + future_option: { enabled: true }, }, }).model("anthropic/claude-3.7-sonnet:thinking"), prompt: "Think briefly.", @@ -210,7 +208,7 @@ describe("OpenRouter", () => { LLM.request({ model: OpenRouter.configure({ apiKey: "test-key", - providerOptions: { openrouter: invalid }, + providerOptions: invalid, }).model("openai/gpt-4o-mini"), prompt: "Hello", }), diff --git a/packages/ai/test/recorded-scenarios.ts b/packages/ai/test/recorded-scenarios.ts index 29f593301128..9e3f80299437 100644 --- a/packages/ai/test/recorded-scenarios.ts +++ b/packages/ai/test/recorded-scenarios.ts @@ -181,12 +181,10 @@ const normalizeImageText = (value: string) => .trim() const encryptedReasoningOptions = { - openai: { - store: false, - include: ["reasoning.encrypted_content"], - reasoningEffort: "low", - reasoningSummary: "auto", - }, + store: false, + include: ["reasoning.encrypted_content"], + reasoningEffort: "low", + reasoningSummary: "auto", } as const type AssistantTextExpectation = string | RegExp @@ -388,7 +386,7 @@ const runReasoningScenario = (context: GoldenScenarioContext) => user("Think briefly, then reply exactly with: Hello!"), assistant.expectText(/^Hello!?$/, { system: "Show concise reasoning when the provider supports visible reasoning summaries.", - providerOptions: { openai: { reasoningEffort: "low", reasoningSummary: "auto" } }, + providerOptions: { reasoningEffort: "low", reasoningSummary: "auto" }, maxTokens: context.maxTokens ?? 120, assert: (response) => expect(response.usage?.reasoningTokens ?? 0).toBeGreaterThan(0), }), diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index e3665bd7a9e4..9f91a8851c0a 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -636,7 +636,7 @@ describe("LLMClient tools", () => { .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .model({ id: "gpt-5.5" }), prompt: "Use the tool.", - providerOptions: { openai: { store: false, include: ["reasoning.encrypted_content"] } }, + providerOptions: { store: false, include: ["reasoning.encrypted_content"] }, }), tools: { get_weather }, }).pipe(Stream.runCollect, Effect.provide(layer)) diff --git a/packages/core/src/aisdk-native.ts b/packages/core/src/aisdk-native.ts index 8485edf4d9e1..d23424a0d408 100644 --- a/packages/core/src/aisdk-native.ts +++ b/packages/core/src/aisdk-native.ts @@ -27,7 +27,7 @@ export function map(input: MapInput): Mapping | undefined { ...baseSettings, ...mapAPIKey(input.settings), ...(typeof input.settings.authToken === "string" ? { authToken: input.settings.authToken } : {}), - ...mapAnthropicOptions(input.settings), + ...mapProviderOptions(input.settings, ["apiKey", "authToken", "baseURL"]), }, } case "@ai-sdk/amazon-bedrock": @@ -89,10 +89,8 @@ export function map(input: MapInput): Mapping | undefined { ...(isRecord(input.settings.thinking) || typeof input.settings.effort === "string" ? { providerOptions: { - anthropic: { - ...(isRecord(input.settings.thinking) ? { thinking: input.settings.thinking } : {}), - ...(typeof input.settings.effort === "string" ? { effort: input.settings.effort } : {}), - }, + ...(isRecord(input.settings.thinking) ? { thinking: input.settings.thinking } : {}), + ...(typeof input.settings.effort === "string" ? { effort: input.settings.effort } : {}), }, } : {}), @@ -108,13 +106,7 @@ export function map(input: MapInput): Mapping | undefined { ...(typeof input.settings.organization === "string" ? { organization: input.settings.organization } : {}), ...(typeof input.settings.project === "string" ? { project: input.settings.project } : {}), ...(isStringRecord(input.settings.queryParams) ? { queryParams: input.settings.queryParams } : {}), - ...mapProviderOptions(input.settings, "openai", [ - "apiKey", - "baseURL", - "organization", - "project", - "queryParams", - ]), + ...mapProviderOptions(input.settings, ["apiKey", "baseURL", "organization", "project", "queryParams"]), }, } case "@ai-sdk/openai-compatible": @@ -125,7 +117,7 @@ export function map(input: MapInput): Mapping | undefined { ...baseSettings, ...mapAPIKey(input.settings), provider: input.providerID, - ...mapProviderOptions(input.settings, "openai", ["apiKey", "baseURL"]), + ...mapProviderOptions(input.settings, ["apiKey", "baseURL"]), }, } case "@openrouter/ai-sdk-provider": @@ -142,18 +134,10 @@ export function map(input: MapInput): Mapping | undefined { } } -function mapAnthropicOptions(settings: Readonly>) { - return mapProviderOptions(settings, "anthropic", ["apiKey", "authToken", "baseURL"]) -} - -function mapProviderOptions( - settings: Readonly>, - key: string, - excluded: ReadonlyArray, -) { +function mapProviderOptions(settings: Readonly>, excluded: ReadonlyArray) { const options = Object.fromEntries(Object.entries(settings).filter(([name]) => !excluded.includes(name))) if (Object.keys(options).length === 0) return {} - return { providerOptions: { [key]: options } } + return { providerOptions: options } } function mapBedrockMantle(input: MapInput, baseSettings: Readonly>): Mapping | undefined { @@ -286,7 +270,7 @@ function mapOpenAIOptions(settings: Readonly>) { ...(typeof settings.serviceTier === "string" ? { serviceTier: settings.serviceTier } : {}), } if (Object.keys(options).length === 0) return {} - return { providerOptions: { openai: options } } + return { providerOptions: options } } function mapBaseSettings(settings: Readonly>) { @@ -317,7 +301,7 @@ function mapGoogleOptions(settings: Readonly>, extra: Re ...extra, } if (Object.keys(options).length === 0) return {} - return { providerOptions: { gemini: options } } + return { providerOptions: options } } function mapOpenRouter( @@ -369,7 +353,7 @@ function mapOpenRouterOptions(settings: Readonly>) { ), ) if (Object.keys(options).length === 0) return {} - return { providerOptions: { openrouter: options } } + return { providerOptions: options } } function isStringRecord(value: unknown): value is Readonly> { @@ -382,5 +366,5 @@ function mapXAIOptions(settings: Readonly>) { ...(typeof settings.store === "boolean" ? { store: settings.store } : {}), } if (Object.keys(options).length === 0) return {} - return { providerOptions: { xai: options } } + return { providerOptions: options } } diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 7f1906c8e225..cb4471fb6251 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -307,12 +307,6 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) { const packageName = Provider.packageName(info.package!) const projected = mapBodyToProviderOptions(info, packageName) const optionKey = providerOptionKey(packageName, info.providerID) - const providerOptions = (() => { - if (projected.settings === undefined) return - if (packageName === "@ai-sdk/gateway") return gatewayProviderOptions(info.modelID ?? info.id, projected.settings) - if (packageName === "@ai-sdk/azure") return { openai: projected.settings, azure: projected.settings } - return { [optionKey]: projected.settings } - })() const route: AnyRoute = { id: `ai-sdk:${packageName}`, provider: ProviderID.make(info.providerID), @@ -335,11 +329,11 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) { headers: info.headers, }, limits: { context: info.limit.context, input: info.limit.input, output: info.limit.output }, - providerOptions, + providerOptions: projected.settings, }, body: { schema: Schema.Unknown, - from: (request) => Effect.succeed(callOptions(request)), + from: (request) => Effect.succeed(callOptions(request, packageName, info.modelID ?? info.id, optionKey)), }, with: () => route, model: (input) => @@ -414,7 +408,12 @@ function mapBodyToProviderOptions(model: Info, packageName: string) { } } -function callOptions(request: LLMRequest): LanguageModelV3CallOptions { +function callOptions( + request: LLMRequest, + packageName: string | undefined, + modelID: ID, + optionKey: string, +): LanguageModelV3CallOptions { return { prompt: prompt(request), maxOutputTokens: request.generation?.maxTokens, @@ -428,7 +427,7 @@ function callOptions(request: LLMRequest): LanguageModelV3CallOptions { tools: request.tools.map(tool), toolChoice: toolChoice(request.toolChoice), headers: request.http?.headers, - providerOptions: providerOptions(request.providerOptions), + providerOptions: requestProviderOptions(request.providerOptions, packageName, modelID, optionKey), } } @@ -526,7 +525,7 @@ function assistantPart(part: ContentPart): AssistantContent { case "media": return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }] case "reasoning": - return [{ type: "reasoning", text: part.text, providerOptions: providerOptions(part.providerMetadata) }] + return [{ type: "reasoning", text: part.text, providerOptions: metadataProviderOptions(part.providerMetadata) }] case "tool-call": return [ { @@ -535,7 +534,7 @@ function assistantPart(part: ContentPart): AssistantContent { toolName: part.name, input: part.input, providerExecuted: part.providerExecuted, - providerOptions: providerOptions(part.providerMetadata), + providerOptions: metadataProviderOptions(part.providerMetadata), }, ] case "tool-result": @@ -551,7 +550,7 @@ function toolResultPart(part: ContentPart): ToolResultContent[] { toolCallId: part.id, toolName: part.name, output: toolOutput(part.result), - providerOptions: providerOptions(part.providerMetadata), + providerOptions: metadataProviderOptions(part.providerMetadata), }, ] } @@ -595,7 +594,20 @@ function toolChoice(input: LLMRequest["toolChoice"]): LanguageModelV3ToolChoice return { type: input.type } } -function providerOptions(input: LLMRequest["providerOptions"]): SharedV3ProviderOptions | undefined { +function requestProviderOptions( + input: LLMRequest["providerOptions"], + packageName: string | undefined, + modelID: ID, + optionKey: string, +): SharedV3ProviderOptions | undefined { + if (!input) return undefined + const options = jsonObject(input) + if (packageName === "@ai-sdk/gateway") return gatewayProviderOptions(modelID, options) + if (packageName === "@ai-sdk/azure") return { openai: options, azure: options } + return { [optionKey]: options } +} + +function metadataProviderOptions(input: ProviderMetadata | undefined): SharedV3ProviderOptions | undefined { if (!input) return undefined return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)])) } diff --git a/packages/core/test/aisdk-native.test.ts b/packages/core/test/aisdk-native.test.ts index 25539557c9c7..f234c28858ab 100644 --- a/packages/core/test/aisdk-native.test.ts +++ b/packages/core/test/aisdk-native.test.ts @@ -22,16 +22,14 @@ describe("AISDKNative", () => { settings: { apiKey: "secret", baseURL: "https://api.meta.ai/v1", - organization: "org", providerOptions: { - openai: { - reasoningEffort: "xhigh", - reasoningSummary: "auto", - include: ["reasoning.encrypted_content"], - instructions: "Follow the repository instructions.", - truncation: "auto", - }, + reasoningEffort: "xhigh", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + instructions: "Follow the repository instructions.", + truncation: "auto", }, + organization: "org", }, }) expect(map("@ai-sdk/openai-compatible", { baseURL: "https://example.com/v1", reasoningEffort: "high" })).toEqual({ @@ -39,7 +37,7 @@ describe("AISDKNative", () => { settings: { baseURL: "https://example.com/v1", provider: "test-provider", - providerOptions: { openai: { reasoningEffort: "high" } }, + providerOptions: { reasoningEffort: "high" }, }, }) }) @@ -58,10 +56,8 @@ describe("AISDKNative", () => { authToken: "token", baseURL: "https://anthropic.example/v1", providerOptions: { - anthropic: { - thinking: { type: "adaptive", display: "summarized" }, - effort: "high", - }, + thinking: { type: "adaptive", display: "summarized" }, + effort: "high", }, }, }) @@ -81,7 +77,8 @@ describe("AISDKNative", () => { project: "project", location: "us-central1", providerOptions: { - gemini: { labels: { environment: "test" }, thinkingConfig: { thinkingLevel: "high" } }, + labels: { environment: "test" }, + thinkingConfig: { thinkingLevel: "high" }, }, }, }) @@ -115,7 +112,7 @@ describe("AISDKNative", () => { apiVersion: "2025-01-01-preview", queryParams: { feature: "enabled" }, useDeploymentBasedUrls: true, - providerOptions: { openai: { reasoningEffort: "high" } }, + providerOptions: { reasoningEffort: "high" }, }, }) expect(map("@ai-sdk/azure", { ...settings, useCompletionUrls: true }, "custom-deployment")?.package).toBe( @@ -193,11 +190,9 @@ describe("AISDKNative", () => { baseURL: "https://mantle.test/v1", region: "us-west-2", providerOptions: { - openai: { - reasoningEffort: "high", - reasoningSummary: "auto", - include: ["reasoning.encrypted_content"], - }, + reasoningEffort: "high", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], }, }, headers: { "x-test": "value" }, @@ -246,7 +241,7 @@ describe("AISDKNative", () => { region: "eu-west-1", }, baseURL: "https://bedrock-mantle.eu-west-1.api.aws/v1", - providerOptions: { openai: { store: false } }, + providerOptions: { store: false }, }, }) }) @@ -278,12 +273,10 @@ describe("AISDKNative", () => { package: "@opencode-ai/ai/providers/openrouter", settings: { providerOptions: { - openrouter: { - models: ["anthropic/claude-sonnet-4.6"], - provider: { only: ["anthropic"], require_parameters: true }, - reasoning: { effort: "high" }, - future_option: { enabled: true }, - }, + models: ["anthropic/claude-sonnet-4.6"], + provider: { only: ["anthropic"], require_parameters: true }, + reasoning: { effort: "high" }, + future_option: { enabled: true }, }, }, headers: { @@ -312,15 +305,13 @@ describe("AISDKNative", () => { package: "@opencode-ai/ai/providers/google", settings: { providerOptions: { - gemini: { - cachedContent: "cachedContents/example", - safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }], - serviceTier: "flex", - thinkingConfig: { - thinkingBudget: 0, - includeThoughts: false, - thinkingLevel: "high", - }, + cachedContent: "cachedContents/example", + safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }], + serviceTier: "flex", + thinkingConfig: { + thinkingBudget: 0, + includeThoughts: false, + thinkingLevel: "high", }, }, }, @@ -330,7 +321,7 @@ describe("AISDKNative", () => { test("maps Google thinking settings independently", () => { for (const thinkingConfig of [{ thinkingBudget: -1 }, { includeThoughts: true }, { thinkingLevel: "medium" }]) { expect(map("@ai-sdk/google", { thinkingConfig })).toMatchObject({ - settings: { providerOptions: { gemini: { thinkingConfig } } }, + settings: { providerOptions: { thinkingConfig } }, }) } }) @@ -345,11 +336,9 @@ describe("AISDKNative", () => { ).toMatchObject({ settings: { providerOptions: { - gemini: { - cachedContent: "cachedContents/example", - safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }], - serviceTier: "future-tier", - }, + cachedContent: "cachedContents/example", + safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }], + serviceTier: "future-tier", }, }, }) @@ -374,10 +363,8 @@ describe("AISDKNative", () => { location: "eu", project: "vertex-project", providerOptions: { - gemini: { - labels: { component: "opencode", environment: "test" }, - thinkingConfig: { thinkingLevel: "high" }, - }, + labels: { component: "opencode", environment: "test" }, + thinkingConfig: { thinkingLevel: "high" }, }, }, headers: { "x-test": "value" }, @@ -403,10 +390,8 @@ describe("AISDKNative", () => { location: "eu", project: "vertex-project", providerOptions: { - anthropic: { - thinking: { type: "adaptive", display: "summarized" }, - effort: "high", - }, + thinking: { type: "adaptive", display: "summarized" }, + effort: "high", }, }, headers: { "x-test": "value" }, @@ -427,10 +412,8 @@ describe("AISDKNative", () => { apiKey: "secret", baseURL: "https://xai.example/v1", providerOptions: { - xai: { - reasoningEffort: "custom", - store: true, - }, + reasoningEffort: "custom", + store: true, }, }, }) diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index 7f3a3f0d7859..5120c01826ee 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -94,10 +94,19 @@ it.effect("projects request settings, headers, and body overlays", () => headers: { "x-test": "header" }, body: { safety_setting: "strict" }, }) - const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" })) + const prepared = yield* compileRequest( + LLM.request({ + model: resolved, + prompt: "Hello", + providerOptions: { safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }] }, + }), + ) expect(prepared.body.providerOptions).toEqual({ - google: { thinkingConfig: { thinkingBudget: 1024 } }, + google: { + thinkingConfig: { thinkingBudget: 1024 }, + safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }], + }, }) expect(prepared.body.headers).toEqual({ "x-test": "header" }) expect(body).toEqual({ safety_setting: "strict" }) From 6a4385b12fa69010fbe859ce46d230bafff8d831 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 19 Aug 2026 17:13:21 -0400 Subject: [PATCH 2/2] test(ai): complete flat option coverage --- packages/ai/test/recorded-scenarios.ts | 3 +- packages/core/test/model-resolver.test.ts | 48 +++++++++-------------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/packages/ai/test/recorded-scenarios.ts b/packages/ai/test/recorded-scenarios.ts index 9e3f80299437..23a3c3e592e9 100644 --- a/packages/ai/test/recorded-scenarios.ts +++ b/packages/ai/test/recorded-scenarios.ts @@ -302,8 +302,7 @@ const runTextScenario = (context: GoldenScenarioContext) => assistant.expectText(/^Hello!?$/, { system: "You are concise.", maxTokens: context.maxTokens ?? 40, - providerOptions: - context.model.route.id === "gemini" ? { gemini: { thinkingConfig: { thinkingBudget: 0 } } } : undefined, + providerOptions: context.model.route.id === "gemini" ? { thinkingConfig: { thinkingBudget: 0 } } : undefined, }), ]) diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 6a6c83a50627..772abab0896b 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -495,12 +495,10 @@ describe("ModelResolver", () => { temperature: 0.2, }) expect(resolved.route.defaults.providerOptions).toEqual({ - openai: { - store: false, - reasoningEffort: "xhigh", - reasoningSummary: "auto", - include: ["reasoning.encrypted_content"], - }, + store: false, + reasoningEffort: "xhigh", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], }) const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" })) expect(prepared.body).toMatchObject({ @@ -569,7 +567,7 @@ describe("ModelResolver", () => { custom_extension: { enabled: true }, }) expect(resolved.route.defaults.providerOptions).toEqual({ - anthropic: { thinking: { type: "enabled", budgetTokens: 12000 } }, + thinking: { type: "enabled", budgetTokens: 12000 }, }) }), ) @@ -851,48 +849,46 @@ describe("ModelResolver", () => { include: ["reasoning.encrypted_content"], }, { - openai: { - reasoningEffort: "xhigh", - reasoningSummary: "auto", - include: ["reasoning.encrypted_content"], - }, + reasoningEffort: "xhigh", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], }, ], [ "@ai-sdk/anthropic", "@opencode-ai/ai/providers/anthropic", { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, - { anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" } }, + { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, ], [ "@ai-sdk/openai-compatible", "@opencode-ai/ai/providers/openai-compatible", { reasoningEffort: "high" }, - { openai: { reasoningEffort: "high" } }, + { reasoningEffort: "high" }, ], [ "@ai-sdk/google", "@opencode-ai/ai/providers/google", { thinkingConfig: { thinkingLevel: "high" } }, - { gemini: { thinkingConfig: { thinkingLevel: "high" } } }, + { thinkingConfig: { thinkingLevel: "high" } }, ], [ "@ai-sdk/google-vertex", "@opencode-ai/ai/providers/google-vertex", { thinkingConfig: { thinkingLevel: "high" } }, - { gemini: { thinkingConfig: { thinkingLevel: "high" } } }, + { thinkingConfig: { thinkingLevel: "high" } }, ], [ "@openrouter/ai-sdk-provider", "@opencode-ai/ai/providers/openrouter", { reasoning: { effort: "high" } }, - { openrouter: { reasoning: { effort: "high" } } }, + { reasoning: { effort: "high" } }, ], [ "@ai-sdk/xai", "@opencode-ai/ai/providers/xai", { reasoningEffort: "high" }, - { xai: { reasoningEffort: "high" } }, + { reasoningEffort: "high" }, ], ] as const @@ -1008,10 +1004,8 @@ describe("ModelResolver", () => { location: "eu", project: "vertex-project", providerOptions: { - anthropic: { - thinking: { type: "adaptive", display: "summarized" }, - effort: "high", - }, + thinking: { type: "adaptive", display: "summarized" }, + effort: "high", }, }) return LanguageModel.make({ id: modelID, provider: "native-provider", route: native.route }) @@ -1084,15 +1078,11 @@ describe("ModelResolver", () => { ) expect(google.route.id).toBe("gemini") - expect(google.route.defaults.providerOptions).toEqual({ - gemini: { thinkingConfig: { thinkingBudget: 1_024 } }, - }) + expect(google.route.defaults.providerOptions).toEqual({ thinkingConfig: { thinkingBudget: 1_024 } }) expect(openrouter.route.id).toBe("openrouter") - expect(openrouter.route.defaults.providerOptions).toEqual({ openrouter: { reasoning: { effort: "high" } } }) + expect(openrouter.route.defaults.providerOptions).toEqual({ reasoning: { effort: "high" } }) expect(xai.route.id).toBe("openai-responses") - expect(xai.route.defaults.providerOptions).toEqual({ - xai: { reasoningEffort: "high", store: false }, - }) + expect(xai.route.defaults.providerOptions).toEqual({ reasoningEffort: "high", store: false }) expect(bedrock.route.id).toBe("bedrock-converse") expect(bedrock.route.defaults.generation).toEqual({ topP: 0.8 }) expect(bedrock.route.defaults.http?.body).toEqual({ serviceTier: { type: "priority" } })