mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(ai): flatten provider request options (#43513)
This commit is contained in:
+13
-1
@@ -372,11 +372,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: { <provider>: {...} }`** — 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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -69,9 +69,7 @@ export type Resolved = Omit<Options, "allowedTools"> & {
|
||||
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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<Record<string, string>>
|
||||
}
|
||||
|
||||
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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<string, unknown>) =>
|
||||
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 = (
|
||||
|
||||
@@ -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<RouteDefaultsInput, "providerOptions"> &
|
||||
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
|
||||
}),
|
||||
|
||||
@@ -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<RouteDefaultsInput, "providerOptions"> &
|
||||
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({
|
||||
|
||||
@@ -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<typeof ProviderOptions>
|
||||
|
||||
export const mergeProviderOptions = (
|
||||
...items: ReadonlyArray<ProviderOptions | undefined>
|
||||
): ProviderOptions | undefined => {
|
||||
const result: Record<string, Record<string, unknown>> = {}
|
||||
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<HttpOptions>("AI.HttpOptions")({
|
||||
body: Schema.optional(JsonSchema),
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
@@ -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> = T extends Stream.Stream<infer _A, infer _E, infer R
|
||||
type Equal<A, B> = [A, B] extends [B, A] ? true : false
|
||||
type Assert<T extends true> = 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<Equal<Requirements<typeof generatedObject>, 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<typeof model> = { example: { mode: "fast" } }
|
||||
const options: LanguageModelProviderOptions<typeof model> = { mode: "fast" }
|
||||
void (options satisfies LanguageModelProviderOptions<typeof model>)
|
||||
void (true satisfies GenerateRequirements)
|
||||
void (true satisfies StreamClientRequirements)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
@@ -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" } },
|
||||
})
|
||||
|
||||
@@ -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" },
|
||||
})
|
||||
|
||||
@@ -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" },
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
@@ -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" } },
|
||||
})
|
||||
|
||||
@@ -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" } },
|
||||
})
|
||||
|
||||
@@ -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" },
|
||||
})
|
||||
|
||||
@@ -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" },
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" },
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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" })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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: [] },
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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" } },
|
||||
),
|
||||
|
||||
@@ -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" },
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
@@ -304,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,
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -388,7 +385,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),
|
||||
}),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<Record<string, unknown>>) {
|
||||
return mapProviderOptions(settings, "anthropic", ["apiKey", "authToken", "baseURL"])
|
||||
}
|
||||
|
||||
function mapProviderOptions(
|
||||
settings: Readonly<Record<string, unknown>>,
|
||||
key: string,
|
||||
excluded: ReadonlyArray<string>,
|
||||
) {
|
||||
function mapProviderOptions(settings: Readonly<Record<string, unknown>>, excluded: ReadonlyArray<string>) {
|
||||
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<Record<string, unknown>>): Mapping | undefined {
|
||||
@@ -286,7 +270,7 @@ function mapOpenAIOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
...(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<Record<string, unknown>>) {
|
||||
@@ -317,7 +301,7 @@ function mapGoogleOptions(settings: Readonly<Record<string, unknown>>, 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<Record<string, unknown>>) {
|
||||
),
|
||||
)
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: { openrouter: options } }
|
||||
return { providerOptions: options }
|
||||
}
|
||||
|
||||
function isStringRecord(value: unknown): value is Readonly<Record<string, string>> {
|
||||
@@ -382,5 +366,5 @@ function mapXAIOptions(settings: Readonly<Record<string, unknown>>) {
|
||||
...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
|
||||
}
|
||||
if (Object.keys(options).length === 0) return {}
|
||||
return { providerOptions: { xai: options } }
|
||||
return { providerOptions: options }
|
||||
}
|
||||
|
||||
+26
-14
@@ -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)]))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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" } })
|
||||
|
||||
Reference in New Issue
Block a user