feat(core): flatten provider config and load native packages (#35563)

Co-authored-by: Dax Raad <d@ironbay.co>
This commit is contained in:
Shoubhit Dash 2026-07-07 01:48:56 +05:30 committed by GitHub
commit e57d9ca390
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 2593 additions and 1984 deletions

View file

@ -84,7 +84,7 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[
cost: defaultCost(model), cost: defaultCost(model),
limit: model.limit, limit: model.limit,
status: model.status, status: model.status,
variants: Object.fromEntries(model.variants.map((variant) => [variant.id, {}])), variants: Object.fromEntries((model.variants ?? []).map((variant) => [variant.id, {}])),
} }
grouped.set(provider.id, provider) grouped.set(provider.id, provider)
} }
@ -141,10 +141,7 @@ export async function loadRunCommands(sdk: OpenCodeClient, directory: string): P
sdk.command.list(location(directory)), sdk.command.list(location(directory)),
sdk.skill.list(location(directory)), sdk.skill.list(location(directory)),
]) ])
return [ return [...commands.data.map(runCommand), ...skills.data.filter((skill) => skill.slash !== false).map(runSkill)]
...commands.data.map(runCommand),
...skills.data.filter((skill) => skill.slash !== false).map(runSkill),
]
} }
export async function loadRunReferences(sdk: OpenCodeClient, directory: string): Promise<RunReference[]> { export async function loadRunReferences(sdk: OpenCodeClient, directory: string): Promise<RunReference[]> {

View file

@ -2009,39 +2009,24 @@ export type ModelListOutput = {
} }
readonly data: ReadonlyArray<{ readonly data: ReadonlyArray<{
readonly id: string readonly id: string
readonly modelID: string
readonly providerID: string readonly providerID: string
readonly family?: string readonly family?: string
readonly name: string readonly name: string
readonly api: readonly package?: string
| { readonly settings?: { readonly [x: string]: JsonValue }
readonly id: string readonly headers?: { readonly [x: string]: string }
readonly type: "aisdk" readonly body?: { readonly [x: string]: JsonValue }
readonly package: string
readonly url?: string
readonly settings?: { readonly [x: string]: JsonValue }
}
| {
readonly id: string
readonly type: "native"
readonly url?: string
readonly settings: { readonly [x: string]: JsonValue }
}
readonly capabilities: { readonly capabilities: {
readonly tools: boolean readonly tools: boolean
readonly input: ReadonlyArray<string> readonly input: ReadonlyArray<string>
readonly output: ReadonlyArray<string> readonly output: ReadonlyArray<string>
} }
readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue }
readonly variant?: string
}
readonly variants: ReadonlyArray<{ readonly variants: ReadonlyArray<{
readonly id: string readonly id: string
readonly settings: { readonly [x: string]: JsonValue } readonly settings?: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string } readonly headers?: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue } readonly body?: { readonly [x: string]: JsonValue }
}> }>
readonly time: { readonly released: number } readonly time: { readonly released: number }
readonly cost: ReadonlyArray<{ readonly cost: ReadonlyArray<{
@ -2070,39 +2055,24 @@ export type ModelDefaultOutput = {
} }
readonly data: { readonly data: {
readonly id: string readonly id: string
readonly modelID: string
readonly providerID: string readonly providerID: string
readonly family?: string readonly family?: string
readonly name: string readonly name: string
readonly api: readonly package?: string
| { readonly settings?: { readonly [x: string]: JsonValue }
readonly id: string readonly headers?: { readonly [x: string]: string }
readonly type: "aisdk" readonly body?: { readonly [x: string]: JsonValue }
readonly package: string
readonly url?: string
readonly settings?: { readonly [x: string]: JsonValue }
}
| {
readonly id: string
readonly type: "native"
readonly url?: string
readonly settings: { readonly [x: string]: JsonValue }
}
readonly capabilities: { readonly capabilities: {
readonly tools: boolean readonly tools: boolean
readonly input: ReadonlyArray<string> readonly input: ReadonlyArray<string>
readonly output: ReadonlyArray<string> readonly output: ReadonlyArray<string>
} }
readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue }
readonly variant?: string
}
readonly variants: ReadonlyArray<{ readonly variants: ReadonlyArray<{
readonly id: string readonly id: string
readonly settings: { readonly [x: string]: JsonValue } readonly settings?: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string } readonly headers?: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue } readonly body?: { readonly [x: string]: JsonValue }
}> }>
readonly time: { readonly released: number } readonly time: { readonly released: number }
readonly cost: ReadonlyArray<{ readonly cost: ReadonlyArray<{
@ -2150,19 +2120,10 @@ export type ProviderListOutput = {
readonly integrationID?: string readonly integrationID?: string
readonly name: string readonly name: string
readonly disabled?: boolean readonly disabled?: boolean
readonly api: readonly package: string
| { readonly settings?: { readonly [x: string]: JsonValue }
readonly type: "aisdk" readonly headers?: { readonly [x: string]: string }
readonly package: string readonly body?: { readonly [x: string]: JsonValue }
readonly url?: string
readonly settings?: { readonly [x: string]: JsonValue }
}
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue }
}
}> }>
} }
@ -2184,19 +2145,10 @@ export type ProviderGetOutput = {
readonly integrationID?: string readonly integrationID?: string
readonly name: string readonly name: string
readonly disabled?: boolean readonly disabled?: boolean
readonly api: readonly package: string
| { readonly settings?: { readonly [x: string]: JsonValue }
readonly type: "aisdk" readonly headers?: { readonly [x: string]: string }
readonly package: string readonly body?: { readonly [x: string]: JsonValue }
readonly url?: string
readonly settings?: { readonly [x: string]: JsonValue }
}
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
readonly headers: { readonly [x: string]: string }
readonly body: { readonly [x: string]: JsonValue }
}
} }
} }

View file

@ -1,13 +1,43 @@
export * as AISDK from "./aisdk" export * as AISDK from "./aisdk"
import { makeLocationNode } from "./effect/app-node" import { makeLocationNode } from "./effect/app-node"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type {
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect" JSONSchema7,
JSONValue,
LanguageModelV3,
LanguageModelV3CallOptions,
LanguageModelV3FunctionTool,
LanguageModelV3Message,
LanguageModelV3Prompt,
LanguageModelV3StreamPart,
LanguageModelV3ToolChoice,
SharedV3ProviderOptions,
} from "@ai-sdk/provider"
import {
FinishReason,
InvalidProviderOutputReason,
LLMEvent,
LLMError,
Model,
ProviderID,
ProviderMetadata,
ToolResultValue,
UnknownProviderReason,
type ContentPart,
type LLMRequest,
type ToolDefinition,
type UsageInput,
} from "@opencode-ai/llm"
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/llm/route"
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
import { State } from "./state" import { State } from "./state"
type SDK = any type SDK = any
type UserContent = Extract<LanguageModelV3Message, { role: "user" }>["content"]
type AssistantContent = Extract<LanguageModelV3Message, { role: "assistant" }>["content"]
type ToolResultContent = Extract<AssistantContent[number], { type: "tool-result" }>
export interface SDKEvent { export interface SDKEvent {
readonly model: ModelV2.Info readonly model: ModelV2.Info
@ -74,10 +104,10 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
function prepareOptions(model: ModelV2.Info, pkg: string) { function prepareOptions(model: ModelV2.Info, pkg: string) {
const options: Record<string, any> = { const options: Record<string, any> = {
name: model.providerID, name: model.providerID,
...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}), ...(model.settings ?? {}),
...model.request.body, headers: model.headers,
body: model.body,
} }
if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
const customFetch = options.fetch const customFetch = options.fetch
const chunkTimeout = options.chunkTimeout const chunkTimeout = options.chunkTimeout
@ -110,6 +140,13 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
} }
} }
if (typeof opts.body === "string" && model.body !== undefined) {
const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body))
}
}
const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, { const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
...opts, ...opts,
timeout: false, timeout: false,
@ -142,17 +179,29 @@ export interface Interface {
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent> readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent> readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError> readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
readonly model: (model: ModelV2.Info) => Effect.Effect<Model, InitError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
const layer = Layer.effect( export const locationLayer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = [] let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = [] let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
const languages = new Map<string, LanguageModelV3>() const languages = new Map<string, LanguageModelV3>()
const sdks = new Map<string, SDK>() const sdks = new Map<string, SDK>()
const functionIDs = new WeakMap<object, number>()
let nextFunctionID = 0
const cacheKey = (input: unknown) =>
JSON.stringify(input, (_key, value: unknown) => {
if (typeof value !== "function") return value
const existing = functionIDs.get(value)
if (existing !== undefined) return `function:${existing}`
const id = nextFunctionID++
functionIDs.set(value, id)
return `function:${id}`
}) ?? ""
const register = <Event>( const register = <Event>(
hooks: () => ((event: Event) => Effect.Effect<void> | void)[], hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
@ -196,24 +245,36 @@ const layer = Layer.effect(
runSDK: (event) => run(sdkHooks, event), runSDK: (event) => run(sdkHooks, event),
runLanguage: (event) => run(languageHooks, event), runLanguage: (event) => run(languageHooks, event),
language: Effect.fn("AISDK.language")(function* (model) { language: Effect.fn("AISDK.language")(function* (model) {
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}` const key = cacheKey({
providerID: model.providerID,
id: model.id,
modelID: model.modelID,
package: model.package,
settings: model.settings,
headers: model.headers,
body: model.body,
limit: model.limit,
})
const existing = languages.get(key) const existing = languages.get(key)
if (existing) return existing if (existing) return existing
if (model.api.type !== "aisdk") if (!ProviderV2.isAISDK(model.package))
return yield* new InitError({ return yield* new InitError({
providerID: model.providerID, providerID: model.providerID,
cause: new Error(`Unsupported api ${model.api.type}`), cause: new Error(`Unsupported package ${model.package}`),
}) })
const options = prepareOptions(model, model.api.package) const packageName = ProviderV2.packageName(model.package) ?? ""
const sdkKey = JSON.stringify({ const options = prepareOptions(model, packageName)
const sdkKey = cacheKey({
providerID: model.providerID, providerID: model.providerID,
api: model.api, package: packageName,
options, settings: model.settings,
headers: model.headers,
body: model.body,
}) })
const sdk = const sdk =
sdks.get(sdkKey) ?? sdks.get(sdkKey) ??
(yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk (yield* service.runSDK({ model, package: packageName, options }).pipe(initError(model.providerID))).sdk
if (!sdk) if (!sdk)
return yield* new InitError({ return yield* new InitError({
providerID: model.providerID, providerID: model.providerID,
@ -221,15 +282,396 @@ const layer = Layer.effect(
}) })
sdks.set(sdkKey, sdk) sdks.set(sdkKey, sdk)
const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID)) const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe( const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.modelID ?? model.id)).pipe(
initError(model.providerID), initError(model.providerID),
) )
languages.set(key, language) languages.set(key, language)
return language return language
}), }),
model: Effect.fn("AISDK.model")(function* (model) {
return modelFromLanguage(model, yield* service.language(model))
}),
}) })
return service return service
}), }),
) )
export const node = makeLocationNode({ service: Service, layer, deps: [] }) export const defaultLayer = locationLayer
function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
const settings = requestSettings(info.settings)
const optionKey = providerOptionKey(ProviderV2.packageName(info.package), info.providerID)
const route: AnyRoute = {
id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
provider: ProviderID.make(info.providerID),
protocol: "ai-sdk",
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
auth: Auth.none,
transport: {
id: "ai-sdk",
prepare: (input) => Effect.succeed(input.body),
frames: () => Stream.empty,
},
defaults: {
headers: info.headers,
http:
info.body === undefined && info.headers === undefined
? undefined
: { body: info.body === undefined ? undefined : { ...info.body }, headers: info.headers },
limits: { context: info.limit.context, output: info.limit.output },
providerOptions: settings === undefined ? undefined : { [optionKey]: settings },
},
body: {
schema: Schema.Unknown,
from: (request) => Effect.succeed(callOptions(request)),
},
with: () => route,
model: (input) => Model.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
prepareTransport: (body) => Effect.succeed(body),
streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
}
return Model.make({ id: info.modelID ?? info.id, provider: info.providerID, route })
}
function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
if (packageName === "@ai-sdk/google") return "google"
if (packageName === "@ai-sdk/google-vertex") return "vertex"
if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic"
if (packageName === "@ai-sdk/amazon-bedrock" || packageName === "@ai-sdk/amazon-bedrock/mantle") return "bedrock"
if (packageName === "@ai-sdk/azure") return "azure"
if (packageName === "@openrouter/ai-sdk-provider") return "openrouter"
if (packageName?.startsWith("@ai-sdk/")) return packageName.slice("@ai-sdk/".length)
return providerID
}
function requestSettings(settings: Readonly<Record<string, unknown>> | undefined) {
if (settings === undefined) return undefined
const result = Object.fromEntries(
Object.entries(settings).filter(
([key]) => !["apiKey", "authToken", "baseURL", "chunkTimeout", "fetch", "timeout"].includes(key),
),
)
return Object.keys(result).length === 0 ? undefined : result
}
function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
return {
prompt: prompt(request),
maxOutputTokens: request.generation?.maxTokens ?? request.model.route.defaults.limits?.output,
temperature: request.generation?.temperature,
stopSequences: request.generation?.stop === undefined ? undefined : [...request.generation.stop],
topP: request.generation?.topP,
topK: request.generation?.topK,
presencePenalty: request.generation?.presencePenalty,
frequencyPenalty: request.generation?.frequencyPenalty,
seed: request.generation?.seed,
responseFormat: responseFormat(request),
tools: request.tools.map(tool),
toolChoice: toolChoice(request.toolChoice),
headers: request.http?.headers,
providerOptions: providerOptions(request.providerOptions),
}
}
function prompt(request: LLMRequest): LanguageModelV3Prompt {
const system = request.system
.map((part) => part.text)
.filter(Boolean)
.join("\n\n")
const messages = request.messages.flatMap(message)
if (!system.length) return messages
return [{ role: "system", content: system }, ...messages]
}
function message(input: LLMRequest["messages"][number]): LanguageModelV3Message[] {
switch (input.role) {
case "system":
return [{ role: "system", content: input.content.flatMap(text).join("\n\n") }]
case "user":
return [{ role: "user", content: input.content.flatMap(userPart) }]
case "assistant":
return [{ role: "assistant", content: input.content.flatMap(assistantPart) }]
case "tool": {
const content = input.content.flatMap(toolResultPart)
return content.length ? [{ role: "tool", content }] : []
}
}
}
function text(part: ContentPart) {
return part.type === "text" ? [part.text] : []
}
function userPart(part: ContentPart): UserContent {
if (part.type === "text") return [{ type: "text", text: part.text }]
if (part.type === "media")
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
return []
}
function assistantPart(part: ContentPart): AssistantContent {
switch (part.type) {
case "text":
return [{ type: "text", text: part.text }]
case "media":
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
case "reasoning":
return [{ type: "reasoning", text: part.text }]
case "tool-call":
return [
{
type: "tool-call",
toolCallId: part.id,
toolName: part.name,
input: part.input,
providerExecuted: part.providerExecuted,
},
]
case "tool-result":
return toolResultPart(part)
}
}
function toolResultPart(part: ContentPart): ToolResultContent[] {
if (part.type !== "tool-result") return []
return [
{
type: "tool-result",
toolCallId: part.id,
toolName: part.name,
output: toolOutput(part.result),
},
]
}
function toolOutput(result: ToolResultValue) {
switch (result.type) {
case "text":
case "error":
return { type: "text" as const, value: messageValue(result.value) }
}
return { type: "json" as const, value: jsonValue(result.value) }
}
function tool(input: ToolDefinition): LanguageModelV3FunctionTool {
return {
type: "function",
name: input.name,
description: input.description,
inputSchema: input.inputSchema as JSONSchema7,
}
}
function toolChoice(input: LLMRequest["toolChoice"]): LanguageModelV3ToolChoice | undefined {
if (!input) return undefined
if (input.type === "tool") return input.name === undefined ? undefined : { type: "tool", toolName: input.name }
return { type: input.type }
}
function responseFormat(request: LLMRequest): LanguageModelV3CallOptions["responseFormat"] {
if (request.responseFormat?.type === "json")
return { type: "json", schema: request.responseFormat.schema as JSONSchema7 }
if (request.responseFormat) return { type: "text" }
}
function providerOptions(input: LLMRequest["providerOptions"]): SharedV3ProviderOptions | undefined {
if (!input) return undefined
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
}
function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions) {
const state = { step: 0, toolNames: {} as Record<string, string> }
return Stream.concat(
Stream.make(LLMEvent.stepStart({ index: state.step })),
Stream.unwrap(
Effect.tryPromise({
try: () => language.doStream(options),
catch: (error) => llmError("doStream", error),
}).pipe(
Effect.map((result) =>
Stream.fromReadableStream({
evaluate: () => result.stream,
onError: (error) => llmError("readStream", error),
}).pipe(
Stream.mapEffect((event) => streamPartEvents(state, event)),
Stream.flatMap((events) => Stream.fromIterable(events)),
),
),
),
),
)
}
function streamPartEvents(
state: { step: number; toolNames: Record<string, string> },
event: LanguageModelV3StreamPart,
): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> {
switch (event.type) {
case "stream-start":
case "response-metadata":
case "raw":
case "file":
case "source":
case "tool-approval-request":
return Effect.succeed([])
case "text-start":
return Effect.succeed([
LLMEvent.textStart({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
])
case "text-delta":
return Effect.succeed([
LLMEvent.textDelta({
id: event.id,
text: event.delta,
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "text-end":
return Effect.succeed([
LLMEvent.textEnd({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
])
case "reasoning-start":
return Effect.succeed([
LLMEvent.reasoningStart({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
])
case "reasoning-delta":
return Effect.succeed([
LLMEvent.reasoningDelta({
id: event.id,
text: event.delta,
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "reasoning-end":
return Effect.succeed([
LLMEvent.reasoningEnd({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
])
case "tool-input-start":
state.toolNames[event.id] = event.toolName
return Effect.succeed([
LLMEvent.toolInputStart({
id: event.id,
name: event.toolName,
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "tool-input-delta":
return Effect.succeed([
LLMEvent.toolInputDelta({ id: event.id, name: state.toolNames[event.id] ?? "unknown", text: event.delta }),
])
case "tool-input-end":
return Effect.succeed([
LLMEvent.toolInputEnd({
id: event.id,
name: state.toolNames[event.id] ?? "unknown",
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "tool-call":
state.toolNames[event.toolCallId] = event.toolName
return Effect.succeed([
LLMEvent.toolCall({
id: event.toolCallId,
name: event.toolName,
input: parseToolInput(event.input),
providerExecuted: event.providerExecuted,
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "tool-result":
delete state.toolNames[event.toolCallId]
return Effect.succeed([
LLMEvent.toolResult({
id: event.toolCallId,
name: event.toolName,
result: ToolResultValue.make(event.result, event.isError ? "error" : "json"),
providerExecuted: true,
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "finish":
return Effect.succeed([
LLMEvent.stepFinish({
index: state.step++,
reason: finishReason(event.finishReason),
usage: usage(event.usage),
providerMetadata: providerMetadata(event.providerMetadata),
}),
LLMEvent.finish({
reason: finishReason(event.finishReason),
usage: usage(event.usage),
providerMetadata: providerMetadata(event.providerMetadata),
}),
])
case "error":
return Effect.fail(llmError("stream", event.error))
}
}
function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["usage"]): UsageInput | undefined {
const output = {
inputTokens: input.inputTokens.total,
nonCachedInputTokens: input.inputTokens.noCache,
cacheReadInputTokens: input.inputTokens.cacheRead,
cacheWriteInputTokens: input.inputTokens.cacheWrite,
outputTokens: input.outputTokens.total,
reasoningTokens: input.outputTokens.reasoning,
totalTokens:
input.inputTokens.total === undefined || input.outputTokens.total === undefined
? undefined
: input.inputTokens.total + input.outputTokens.total,
}
return Object.values(output).some((value) => value !== undefined) ? output : undefined
}
function finishReason(value: unknown): FinishReason {
return Schema.is(FinishReason)(value) ? value : "unknown"
}
function providerMetadata(value: unknown) {
return Schema.is(ProviderMetadata)(value) ? value : undefined
}
function parseToolInput(value: string) {
try {
return JSON.parse(value) as unknown
} catch {
return value
}
}
function jsonObject(input: Record<string, unknown>) {
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonValue(value)]))
}
function jsonValue(input: unknown): JSONValue {
try {
const encoded = JSON.stringify(input)
return encoded === undefined ? null : (JSON.parse(encoded) as JSONValue)
} catch {
return messageValue(input)
}
}
function messageValue(input: unknown) {
if (typeof input === "string") return input
try {
return JSON.stringify(input) ?? String(input)
} catch {
return String(input)
}
}
function llmError(method: string, error: unknown) {
const reason =
error instanceof LLMError
? new InvalidProviderOutputReason({ message: error.message })
: new UnknownProviderReason({ message: error instanceof Error ? error.message : String(error) })
return new LLMError({
module: "AISDK",
method,
reason,
})
}
export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })

View file

@ -66,39 +66,21 @@ const layer = Layer.effect(
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => { const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
if (provider.disabled) return false if (provider.disabled) return false
if (typeof provider.request.body.apiKey === "string") return true if (typeof provider.settings?.apiKey === "string") return true
if (integration?.connections.length) return true if (integration?.connections.length) return true
return provider.integrationID === undefined && !integration return provider.integrationID === undefined && !integration
} }
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => { const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
const api =
model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0
? { ...provider.api, id: model.api.id }
: model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } }
: model.api.type === "aisdk" && provider.api.type === "aisdk"
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
: model.api
const request = {
settings: { ...provider.request.settings, ...model.request.settings },
headers: { ...provider.request.headers, ...model.request.headers },
body: { ...provider.request.body, ...model.request.body },
variant: model.request.variant,
}
return ModelV2.Info.make({ return ModelV2.Info.make({
...model, ...model,
api, package: model.package ?? provider.package,
request, settings: ProviderV2.mergeOverlay(provider.settings, model.settings),
headers: ProviderV2.mergeHeaders(provider.headers, model.headers),
body: ProviderV2.mergeOverlay(provider.body, model.body),
}) })
} }
const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => {
if (typeof item.request.body.baseURL !== "string") return
item.api.url = item.request.body.baseURL
delete item.request.body.baseURL
}
const state = State.create<Data, Draft>({ const state = State.create<Data, Draft>({
initial: () => ({ providers: new Map() }), initial: () => ({ providers: new Map() }),
draft: (draft) => { draft: (draft) => {
@ -116,7 +98,6 @@ const layer = Layer.effect(
draft.providers.set(providerID, current) draft.providers.set(providerID, current)
} }
fn(current.provider) fn(current.provider)
normalizeApi(current.provider)
}, },
remove: (providerID) => { remove: (providerID) => {
draft.providers.delete(providerID) draft.providers.delete(providerID)
@ -139,7 +120,6 @@ const layer = Layer.effect(
fn(model) fn(model)
model.id = modelID model.id = modelID
model.providerID = providerID model.providerID = providerID
normalizeApi(model)
}, },
remove: (providerID, modelID) => { remove: (providerID, modelID) => {
draft.providers.get(providerID)?.models.delete(modelID) draft.providers.get(providerID)?.models.delete(modelID)

View file

@ -19,6 +19,7 @@ import { ConfigCommand } from "./config/command"
import { ConfigFormatter } from "./config/formatter" import { ConfigFormatter } from "./config/formatter"
import { ConfigLSP } from "./config/lsp" import { ConfigLSP } from "./config/lsp"
import { ConfigMCP } from "./config/mcp" import { ConfigMCP } from "./config/mcp"
import { ConfigModel } from "./config/model"
import { ConfigPlugin } from "./config/plugin" import { ConfigPlugin } from "./config/plugin"
import { ConfigProvider } from "./config/provider" import { ConfigProvider } from "./config/provider"
import { ConfigReference } from "./config/reference" import { ConfigReference } from "./config/reference"
@ -35,7 +36,7 @@ export class Info extends Schema.Class<Info>("Config.Info")({
shell: Schema.String.pipe(Schema.optional).annotate({ shell: Schema.String.pipe(Schema.optional).annotate({
description: "Default shell to use for terminal and shell tool execution", description: "Default shell to use for terminal and shell tool execution",
}), }),
model: Schema.String.pipe(Schema.optional).annotate({ model: ConfigModel.Selection.pipe(Schema.optional).annotate({
description: "Default model to use when no session or agent model is selected", description: "Default model to use when no session or agent model is selected",
}), }),
default_agent: Schema.String.pipe(Schema.optional).annotate({ default_agent: Schema.String.pipe(Schema.optional).annotate({

View file

@ -3,6 +3,7 @@ export * as ConfigAgent from "./agent"
import { Schema } from "effect" import { Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission" import { Permission } from "@opencode-ai/schema/permission"
import { ConfigProvider } from "./provider" import { ConfigProvider } from "./provider"
import { ConfigModel } from "./model"
import { PositiveInt } from "../schema" import { PositiveInt } from "../schema"
export const Color = Schema.Union([ export const Color = Schema.Union([
@ -11,8 +12,7 @@ export const Color = Schema.Union([
]) ])
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({ export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
model: Schema.String.pipe(Schema.optional), model: ConfigModel.Selection.pipe(Schema.optional),
variant: Schema.String.pipe(Schema.optional),
request: ConfigProvider.Request.pipe(Schema.optional), request: ConfigProvider.Request.pipe(Schema.optional),
system: Schema.String.pipe(Schema.optional), system: Schema.String.pipe(Schema.optional),
description: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional),

View file

@ -1,12 +1,12 @@
export * as ConfigCommand from "./command" export * as ConfigCommand from "./command"
import { Schema } from "effect" import { Schema } from "effect"
import { ConfigModel } from "./model"
export class Info extends Schema.Class<Info>("ConfigV2.Command")({ export class Info extends Schema.Class<Info>("ConfigV2.Command")({
template: Schema.String, template: Schema.String,
description: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional),
agent: Schema.String.pipe(Schema.optional), agent: Schema.String.pipe(Schema.optional),
model: Schema.String.pipe(Schema.optional), model: ConfigModel.Selection.pipe(Schema.optional),
variant: Schema.String.pipe(Schema.optional),
subtask: Schema.Boolean.pipe(Schema.optional), subtask: Schema.Boolean.pipe(Schema.optional),
}) {} }) {}

View file

@ -0,0 +1,38 @@
export * as ConfigModel from "./model"
import { Schema, SchemaGetter } from "effect"
import { Model } from "@opencode-ai/schema/model"
import { Provider } from "@opencode-ai/schema/provider"
const ProviderID = Provider.ID.check(Schema.isPattern(/^[^/#]+$/))
const ModelID = Model.ID.check(Schema.isPattern(/^[^#]+$/))
const VariantID = Model.VariantID.check(Schema.isPattern(/^[^#]+$/))
const Explicit = Schema.Struct({
providerID: ProviderID,
model: ModelID,
variant: VariantID.pipe(Schema.optional),
})
const Short = Schema.String.check(Schema.isPattern(/^[^/#]+\/[^#]+(?:#[^#]+)?$/))
export interface Selection extends Schema.Schema.Type<typeof Explicit> {}
export const Selection = Schema.Union([Short, Explicit])
.pipe(
Schema.decodeTo(Explicit, {
decode: SchemaGetter.transform((input) => (typeof input === "string" ? parse(input) : input)),
encode: SchemaGetter.passthrough({ strict: false }),
}),
)
.annotate({ identifier: "Config.ModelSelection" })
function parse(input: string): Selection {
const providerEnd = input.indexOf("/")
const variantStart = input.lastIndexOf("#")
const hasVariant = variantStart > providerEnd
return {
providerID: Provider.ID.make(input.slice(0, providerEnd)),
model: Model.ID.make(input.slice(providerEnd + 1, hasVariant ? variantStart : undefined)),
...(hasVariant ? { variant: Model.VariantID.make(input.slice(variantStart + 1)) } : {}),
}
}

View file

@ -8,7 +8,6 @@ import { Config } from "../../config"
import { ConfigAgent } from "../agent" import { ConfigAgent } from "../agent"
import { ConfigMarkdown } from "../markdown" import { ConfigMarkdown } from "../markdown"
import { FSUtil } from "../../fs-util" import { FSUtil } from "../../fs-util"
import { ModelV2 } from "../../model"
import { ConfigAgentV1 } from "../../v1/config/agent" import { ConfigAgentV1 } from "../../v1/config/agent"
import { ConfigMigrateV1 } from "../../v1/config/migrate" import { ConfigMigrateV1 } from "../../v1/config/migrate"
@ -76,13 +75,12 @@ export const Plugin = define({
const exists = draft.get(agentID) !== undefined const exists = draft.get(agentID) !== undefined
draft.update(agentID, (agent) => { draft.update(agentID, (agent) => {
if (!exists) agent.permissions.push(...global) if (!exists) agent.permissions.push(...global)
if (item.model !== undefined) { if (item.model !== undefined)
const model = ModelV2.parse(item.model) agent.model = {
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant } id: item.model.model,
} providerID: item.model.providerID,
if (item.variant !== undefined && agent.model !== undefined) { ...(item.model.variant === undefined ? {} : { variant: item.model.variant }),
agent.model.variant = ModelV2.VariantID.make(item.variant) }
}
if (item.request !== undefined) { if (item.request !== undefined) {
Object.assign(agent.request.headers, item.request.headers ?? {}) Object.assign(agent.request.headers, item.request.headers ?? {})
Object.assign(agent.request.body, item.request.body ?? {}) Object.assign(agent.request.body, item.request.body ?? {})
@ -134,14 +132,16 @@ function decode(file: { directory: string; filepath: string; primary: boolean },
.replace(/\.md$/, "") .replace(/\.md$/, "")
const body = markdown.content.trim() const body = markdown.content.trim()
const legacy = Object.keys(markdown.data).some((key) => !agentKeys.has(key)) const legacy = Object.keys(markdown.data).some((key) => !agentKeys.has(key))
const agent = Option.getOrUndefined( const agent = legacy
legacy ? Option.getOrUndefined(
? Option.map( Option.map(
decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }), decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }),
ConfigMigrateV1.migrateAgent, ConfigMigrateV1.migrateAgent,
) ),
: decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }), )
) : Option.getOrUndefined(
decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }),
)
if (!agent) return if (!agent) return
const info = Option.getOrUndefined( const info = Option.getOrUndefined(
decodeConfig({ decodeConfig({

View file

@ -6,7 +6,6 @@ import { Effect, Option, Schema, Stream } from "effect"
import { CommandV2 } from "../../command" import { CommandV2 } from "../../command"
import { Config } from "../../config" import { Config } from "../../config"
import { FSUtil } from "../../fs-util" import { FSUtil } from "../../fs-util"
import { ModelV2 } from "../../model"
import { ConfigCommand } from "../command" import { ConfigCommand } from "../command"
import { ConfigMarkdown } from "../markdown" import { ConfigMarkdown } from "../markdown"
@ -35,13 +34,12 @@ export const Plugin = define({
item.template = command.template item.template = command.template
if (command.description !== undefined) item.description = command.description if (command.description !== undefined) item.description = command.description
if (command.agent !== undefined) item.agent = command.agent if (command.agent !== undefined) item.agent = command.agent
if (command.model !== undefined) { if (command.model !== undefined)
const model = ModelV2.parse(command.model) item.model = {
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant } id: command.model.model,
} providerID: command.model.providerID,
if (command.variant !== undefined && item.model !== undefined) { ...(command.model.variant === undefined ? {} : { variant: command.model.variant }),
item.model.variant = ModelV2.VariantID.make(command.variant) }
}
if (command.subtask !== undefined) item.subtask = command.subtask if (command.subtask !== undefined) item.subtask = command.subtask
}) })
} }

View file

@ -4,6 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
export const Plugin = define({ export const Plugin = define({
id: "opencode.config.provider", id: "opencode.config.provider",
@ -39,27 +40,29 @@ export const Plugin = define({
yield* ctx.catalog.transform((catalog) => { yield* ctx.catalog.transform((catalog) => {
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document") const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
const configuredDefault = Config.latest(loaded.entries, "model") const configuredDefault = Config.latest(loaded.entries, "model")
if (configuredDefault !== undefined) { if (configuredDefault !== undefined)
const model = ModelV2.parse(configuredDefault) catalog.model.default.set(configuredDefault.providerID, configuredDefault.model)
catalog.model.default.set(model.providerID, model.modelID)
}
for (const file of files) { for (const file of files) {
for (const [id, item] of Object.entries(file.info.providers ?? {})) { for (const [id, item] of Object.entries(file.info.providers ?? {})) {
const providerID = id const providerID = id
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name if (item.name !== undefined) provider.name = item.name
if (item.api !== undefined) provider.api = { ...item.api } if (item.package !== undefined) provider.package = item.package
if (item.request !== undefined) { if (item.settings !== undefined)
Object.assign(provider.request.settings, item.request.settings) provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings)
Object.assign(provider.request.headers, item.request.headers) if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers)
Object.assign(provider.request.body, item.request.body) if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body)
}
}) })
for (const [id, config] of Object.entries(item.models ?? {})) { for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => { catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api } if (config.modelID !== undefined) model.modelID = config.modelID
if (config.package !== undefined) model.package = config.package
if (config.settings !== undefined)
model.settings = ProviderV2.mergeOverlay(model.settings, config.settings)
if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers)
if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body)
if (config.capabilities !== undefined) { if (config.capabilities !== undefined) {
model.capabilities = { model.capabilities = {
tools: config.capabilities.tools, tools: config.capabilities.tools,
@ -67,27 +70,19 @@ export const Plugin = define({
output: [...config.capabilities.output], output: [...config.capabilities.output],
} }
} }
if (config.request !== undefined) {
Object.assign(model.request.settings, config.request.settings)
Object.assign(model.request.headers, config.request.headers)
Object.assign(model.request.body, config.request.body)
if (config.request.variant !== undefined) model.request.variant = config.request.variant
}
if (config.variants !== undefined) { if (config.variants !== undefined) {
model.variants ??= []
for (const variant of config.variants) { for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id) let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) { if (!existing) {
existing = { existing = { id: variant.id }
id: variant.id,
settings: {},
headers: {},
body: {},
}
model.variants.push(existing) model.variants.push(existing)
} }
Object.assign(existing.settings, variant.settings) if (variant.settings !== undefined)
Object.assign(existing.headers, variant.headers) existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings)
Object.assign(existing.body, variant.body) if (variant.headers !== undefined)
existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers)
if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body)
} }
} }
if (config.cost !== undefined) { if (config.cost !== undefined) {

View file

@ -1,13 +1,19 @@
export * as ConfigProvider from "./provider" export * as ConfigProvider from "./provider"
import { Schema } from "effect" import { Schema } from "effect"
import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model" import { ModelV2 } from "../model"
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({ const JsonRecord = Schema.Record(Schema.String, Schema.Json)
settings: ProviderV2.Settings.pipe(Schema.optional),
export const Overlays = {
settings: JsonRecord.pipe(Schema.optional),
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), body: JsonRecord.pipe(Schema.optional),
}
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
headers: Overlays.headers,
body: Overlays.body,
}) {} }) {}
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({ class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
@ -31,32 +37,16 @@ class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
output: Schema.Int.pipe(Schema.optional), output: Schema.Int.pipe(Schema.optional),
}) {} }) {}
const ModelApi = Schema.Union([
Schema.Struct({
id: ModelV2.ID.pipe(Schema.optional),
...ProviderV2.AISDK.fields,
}),
Schema.Struct({
id: ModelV2.ID.pipe(Schema.optional),
...ProviderV2.Native.fields,
}),
Schema.Struct({
id: ModelV2.ID,
}),
])
class Model extends Schema.Class<Model>("ConfigV2.Model")({ class Model extends Schema.Class<Model>("ConfigV2.Model")({
modelID: ModelV2.ID.pipe(Schema.optional),
family: ModelV2.Family.pipe(Schema.optional), family: ModelV2.Family.pipe(Schema.optional),
name: Schema.String.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional),
api: ModelApi.pipe(Schema.optional), package: Schema.String.pipe(Schema.optional),
...Overlays,
capabilities: ModelV2.Capabilities.pipe(Schema.optional), capabilities: ModelV2.Capabilities.pipe(Schema.optional),
request: Schema.Struct({
...Request.fields,
variant: Schema.String.pipe(Schema.optional),
}).pipe(Schema.optional),
variants: Schema.Struct({ variants: Schema.Struct({
id: ModelV2.VariantID, id: ModelV2.VariantID,
...Request.fields, ...Overlays,
}).pipe(Schema.Array, Schema.optional), }).pipe(Schema.Array, Schema.optional),
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional), cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional),
@ -66,7 +56,7 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({ export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
name: Schema.String.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional),
env: Schema.String.pipe(Schema.Array, Schema.optional), env: Schema.String.pipe(Schema.Array, Schema.optional),
api: ProviderV2.Api.pipe(Schema.optional), package: Schema.String.pipe(Schema.optional),
request: Request.pipe(Schema.optional), ...Overlays,
models: Schema.Record(Schema.String, Model).pipe(Schema.optional), models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
}) {} }) {}

View file

@ -1,6 +1,6 @@
import { Types } from "effect"
import { Model } from "@opencode-ai/schema/model" import { Model } from "@opencode-ai/schema/model"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
import type { DeepMutable } from "./schema"
export const ID = Model.ID export const ID = Model.ID
export type ID = typeof ID.Type export type ID = typeof ID.Type
@ -20,20 +20,10 @@ export const Cost = Model.Cost
export const Ref = Model.Ref export const Ref = Model.Ref
export type Ref = typeof Ref.Type export type Ref = typeof Ref.Type
export const Api = Model.Api
export type Api = Model.Api
export const Info = Model.Info export const Info = Model.Info
export type Info = Model.Info export type Info = Model.Info
export type MutableRequest = ProviderV2.MutableRequest & { variant?: string } export type MutableInfo = DeepMutable<Info>
export type MutableVariant = ProviderV2.MutableRequest & { id: VariantID }
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request" | "variants"> & {
api: ProviderV2.MutableApi<Api>
request: MutableRequest
variants: MutableVariant[]
}
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/") const [providerID, ...modelID] = input.split("/")

View file

@ -72,7 +72,7 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"]
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): ModelV2Info["variants"] { function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelV2Info["variants"]> {
const npm = model.provider?.npm ?? provider.npm const npm = model.provider?.npm ?? provider.npm
const options = model.reasoning_options ?? [] const options = model.reasoning_options ?? []
const effort = options.find((option) => option.type === "effort") const effort = options.find((option) => option.type === "effort")
@ -82,7 +82,7 @@ function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model)
const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
if (id === undefined) return [] if (id === undefined) return []
const settings = settingsForEffort(npm, id) const settings = settingsForEffort(npm, id)
return settings ? [{ id, settings, headers: {}, body: {} }] : [] return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
}) })
} }
@ -117,15 +117,18 @@ function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.
function budgetVariants( function budgetVariants(
npm: string | undefined, npm: string | undefined,
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>, option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
): ModelV2Info["variants"] { ): NonNullable<ModelV2Info["variants"]> {
const max = option.max const max = option.max
const high = option.max === undefined ? Math.max(option.min ?? 0, 16_000) : Math.min(Math.max(option.min ?? 0, 16_000), option.max) const high =
option.max === undefined
? Math.max(option.min ?? 0, 16_000)
: Math.min(Math.max(option.min ?? 0, 16_000), option.max)
return [ return [
{ id: "high", budget: high }, { id: "high", budget: high },
...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]), ...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
].flatMap((item) => { ].flatMap((item) => {
const settings = settingsForBudget(npm, item.budget) const settings = settingsForBudget(npm, item.budget)
return settings ? [{ id: item.id, settings, headers: {}, body: {} }] : [] return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : []
}) })
} }
@ -143,12 +146,13 @@ function modeName(model: ModelsDev.Model, mode: string) {
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}` return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
} }
function mergeVariants(model: ModelV2Info, next: ModelV2Info["variants"]) { function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["variants"]>) {
const existing = new Map(model.variants.map((variant) => [variant.id, variant])) const variants = model.variants ?? []
const existing = new Map(variants.map((variant) => [variant.id, variant]))
const nextIDs = new Set(next.map((variant) => variant.id)) const nextIDs = new Set(next.map((variant) => variant.id))
model.variants = [ model.variants = [
...next.map((variant) => existing.get(variant.id) ?? variant), ...next.map((variant) => existing.get(variant.id) ?? variant),
...model.variants.filter((variant) => !nextIDs.has(variant.id)), ...variants.filter((variant) => !nextIDs.has(variant.id)),
] ]
} }
@ -159,24 +163,14 @@ function applyModel(
readonly name?: string readonly name?: string
readonly cost?: ModelV2Info["cost"] readonly cost?: ModelV2Info["cost"]
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"] readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
readonly variants?: ModelV2Info["variants"] readonly variants?: NonNullable<ModelV2Info["variants"]>
} = {}, } = {},
) { ) {
draft.name = input.name ?? model.name draft.name = input.name ?? model.name
draft.modelID = model.id
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
draft.api = model.provider?.npm draft.package = model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined
? { draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
id: ModelV2.ID.make(model.id),
type: "aisdk",
package: model.provider.npm,
url: model.provider.api,
}
: {
id: ModelV2.ID.make(model.id),
type: "native",
url: model.provider?.api,
settings: {},
}
draft.capabilities = { draft.capabilities = {
tools: model.tool_call, tools: model.tool_call,
input: [...(model.modalities?.input ?? [])], input: [...(model.modalities?.input ?? [])],
@ -184,7 +178,11 @@ function applyModel(
} }
mergeVariants(draft, input.variants ?? []) mergeVariants(draft, input.variants ?? [])
draft.time.released = released(model.release_date) draft.time.released = released(model.release_date)
draft.cost = input.cost ?? cost(model.cost) draft.cost = (input.cost ?? cost(model.cost)).map((item) => ({
...item,
tier: item.tier && { ...item.tier },
cache: { ...item.cache },
}))
draft.status = model.status ?? "active" draft.status = model.status ?? "active"
draft.enabled = true draft.enabled = true
draft.limit = { draft.limit = {
@ -192,8 +190,8 @@ function applyModel(
input: model.limit.input, input: model.limit.input,
output: model.limit.output, output: model.limit.output,
} }
Object.assign(draft.request.headers, input.request?.headers ?? {}) draft.headers = { ...draft.headers, ...input.request?.headers }
Object.assign(draft.request.body, input.request?.body ?? {}) draft.body = { ...draft.body, ...input.request?.body }
} }
export const ModelsDevPlugin = define({ export const ModelsDevPlugin = define({
@ -222,25 +220,18 @@ export const ModelsDevPlugin = define({
const providerID = ProviderV2.ID.make(item.id) const providerID = ProviderV2.ID.make(item.id)
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.name = item.name provider.name = item.name
provider.api = item.npm provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
? { provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
type: "aisdk",
package: item.npm,
url: item.api,
}
: {
type: "native",
url: item.api,
settings: {},
}
}) })
for (const model of Object.values(item.models)) { for (const model of Object.values(item.models)) {
const baseCost = cost(model.cost) const baseCost = cost(model.cost)
const variants = reasoningVariants(item, model) const variants = reasoningVariants(item, model)
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants })) catalog.model.update(providerID, ModelV2.ID.make(model.id), (draft) =>
applyModel(draft, model, { cost: baseCost, variants }),
)
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) => catalog.model.update(providerID, ModelV2.ID.make(`${model.id}-${mode}`), (draft) =>
applyModel(draft, model, { applyModel(draft, model, {
name: modeName(model, mode), name: modeName(model, mode),
cost: mergeCost(baseCost, options.cost), cost: mergeCost(baseCost, options.cost),

View file

@ -64,15 +64,14 @@ export const AmazonBedrockPlugin = define({
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return if (typeof provider.settings?.endpoint !== "string") return
if (typeof provider.request.body.endpoint !== "string") return
// The AI SDK expects a base URL, but users configure Bedrock private/VPC // The AI SDK expects a base URL, but users configure Bedrock private/VPC
// endpoints as `endpoint`; move it into the catalog endpoint URL once. // endpoints as `endpoint`; move it into the catalog endpoint URL once.
provider.api.url = provider.request.body.endpoint provider.settings.baseURL = provider.settings.endpoint
delete provider.request.body.endpoint delete provider.settings.endpoint
}) })
} }
}) })
@ -112,12 +111,15 @@ export const AmazonBedrockPlugin = define({
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") { if (
evt.language = selectMantleModel(evt.sdk, evt.model.api.id) ProviderV2.isAISDK(evt.model.package) &&
ProviderV2.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
) {
evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
return return
} }
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region)) evt.language = evt.sdk.languageModel(resolveModelID(evt.model.modelID ?? evt.model.id, region))
}), }),
) )
}), }),

View file

@ -1,16 +1,19 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
export const AnthropicPlugin = define({ export const AnthropicPlugin = define({
id: "opencode.provider.anthropic", id: "opencode.provider.anthropic",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/anthropic") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["anthropic-beta"] = provider.headers = {
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" ...provider.headers,
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
}
}) })
} }
}) })

View file

@ -15,14 +15,14 @@ export const AzurePlugin = define({
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/azure") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue
const configured = item.provider.request.body.resourceName const configured = item.provider.settings?.resourceName
const resourceName = const resourceName =
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
if (!resourceName) continue if (!resourceName) continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.body.resourceName = resourceName provider.settings = { ...provider.settings, resourceName }
}) })
} }
}) })
@ -33,7 +33,7 @@ export const AzurePlugin = define({
if ( if (
!evt.options.resourceName && !evt.options.resourceName &&
!evt.options.baseURL && !evt.options.baseURL &&
(evt.model.api.type !== "aisdk" || !evt.model.api.url) (!ProviderV2.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string")
) { ) {
throw new Error( throw new Error(
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it", "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
@ -47,7 +47,11 @@ export const AzurePlugin = define({
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return if (evt.model.providerID !== ProviderV2.ID.azure) return
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) evt.language = selectLanguage(
evt.sdk,
evt.model.modelID ?? evt.model.id,
Boolean(evt.options.useCompletionUrls),
)
}), }),
) )
}), }),
@ -60,18 +64,25 @@ export const AzureCognitiveServicesPlugin = define({
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return if (!resourceName) return
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!item.provider.id.includes("azure-cognitive-services")) continue if (!item.provider.id.includes("azure-cognitive-services")) continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` provider.settings = {
...provider.settings,
baseURL: `https://${resourceName}.cognitiveservices.azure.com/openai`,
}
}) })
} }
}) })
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) evt.language = selectLanguage(
evt.sdk,
evt.model.modelID ?? evt.model.id,
Boolean(evt.options.useCompletionUrls),
)
}), }),
) )
}), }),

View file

@ -1,15 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
export const CerebrasPlugin = define({ export const CerebrasPlugin = define({
id: "opencode.provider.cerebras", id: "opencode.provider.cerebras",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/cerebras") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
}) })
} }
}) })

View file

@ -13,10 +13,10 @@ export const CloudflareWorkersAIPlugin = define({
const item = evt.provider.get(providerID) const item = evt.provider.get(providerID)
if (!item) return if (!item) return
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return if (!ProviderV2.isAISDK(provider.package)) return
if (provider.api.url) return if (typeof provider.settings?.baseURL === "string") return
const accountId = resolveAccountId(provider.request.body) const accountId = resolveAccountId(provider.settings ?? {})
if (accountId) provider.api.url = workersEndpoint(accountId) if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
}) })
}) })
yield* ctx.aisdk.sdk( yield* ctx.aisdk.sdk(
@ -25,7 +25,7 @@ export const CloudflareWorkersAIPlugin = define({
if (evt.package !== "@ai-sdk/openai-compatible") return if (evt.package !== "@ai-sdk/openai-compatible") return
const accountId = resolveAccountId(evt.options) const accountId = resolveAccountId(evt.options)
if (!hasWorkersEndpoint(evt.model.api) && !accountId) return if (!hasWorkersEndpoint(evt.model) && !accountId) return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
evt.sdk = mod.createOpenAICompatible( evt.sdk = mod.createOpenAICompatible(
sdkOptions({ sdkOptions({
@ -38,7 +38,7 @@ export const CloudflareWorkersAIPlugin = define({
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return if (evt.model.providerID !== providerID) return
evt.language = evt.sdk.languageModel(evt.model.api.id) evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
}), }),
) )
}), }),
@ -52,8 +52,11 @@ function workersEndpoint(accountId: string) {
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1` return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
} }
function hasWorkersEndpoint(api: ProviderV2.Api) { function hasWorkersEndpoint(model: {
return api.type === "aisdk" && Boolean(api.url) readonly package?: string
readonly settings?: Readonly<Record<string, unknown>>
}) {
return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
} }
function sdkOptions(options: Record<string, any>) { function sdkOptions(options: Record<string, any>) {

View file

@ -34,12 +34,11 @@ export const GithubCopilotPlugin = define({
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
evt.language = evt.sdk.languageModel(evt.model.api.id) evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
return return
} }
evt.language = shouldUseResponses(evt.model.api.id) const id = evt.model.modelID ?? evt.model.id
? evt.sdk.responses(evt.model.api.id) evt.language = shouldUseResponses(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
: evt.sdk.chat(evt.model.api.id)
}), }),
) )
}), }),

View file

@ -36,26 +36,24 @@ export const GitLabPlugin = define({
if (evt.model.providerID !== ProviderV2.ID.gitlab) return if (evt.model.providerID !== ProviderV2.ID.gitlab) return
const featureFlags = const featureFlags =
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {} typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
if (evt.model.api.id.startsWith("duo-workflow-")) { const id = evt.model.modelID ?? evt.model.id
if (id.startsWith("duo-workflow-")) {
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie) const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
const workflowRef = const workflowRef =
typeof evt.model.request.body.workflowRef === "string" ? evt.model.request.body.workflowRef : undefined typeof evt.model.settings?.workflowRef === "string" ? evt.model.settings.workflowRef : undefined
const workflowDefinition = const workflowDefinition =
typeof evt.model.request.body.workflowDefinition === "string" typeof evt.model.settings?.workflowDefinition === "string"
? evt.model.request.body.workflowDefinition ? evt.model.settings.workflowDefinition
: undefined : undefined
const language = evt.sdk.workflowChat( const language = evt.sdk.workflowChat(gitlab.isWorkflowModel(id) ? id : "duo-workflow", {
gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow", featureFlags,
{ workflowDefinition,
featureFlags, })
workflowDefinition,
},
)
if (workflowRef) language.selectedModelRef = workflowRef if (workflowRef) language.selectedModelRef = workflowRef
evt.language = language evt.language = language
return return
} }
evt.language = evt.sdk.agenticChat(evt.model.api.id, { evt.language = evt.sdk.agenticChat(id, {
aiGatewayHeaders: evt.options.aiGatewayHeaders, aiGatewayHeaders: evt.options.aiGatewayHeaders,
featureFlags, featureFlags,
}) })

View file

@ -59,25 +59,28 @@ export const GoogleVertexPlugin = define({
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if ( if (
item.provider.api.package !== "@ai-sdk/google-vertex" && ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
!( !(
item.provider.id === ProviderV2.ID.googleVertex && item.provider.id === ProviderV2.ID.googleVertex &&
item.provider.api.package.includes("@ai-sdk/openai-compatible") ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
) )
) )
continue continue
const project = resolveProject(item.provider.request.body) const project = resolveProject(item.provider.settings ?? {})
const location = String(resolveLocation(item.provider.request.body)) const location = String(resolveLocation(item.provider.settings ?? {}))
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
if (project) provider.request.body.project = project provider.settings = {
provider.request.body.location = location ...provider.settings,
if (provider.api.type === "aisdk" && provider.api.url) { ...(project ? { project } : {}),
provider.api.url = replaceVertexVars(provider.api.url, project, location) location,
} ...(typeof provider.settings?.baseURL === "string"
if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) { ? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
provider.request.body.fetch = authFetch(provider.request.body.fetch) : {}),
...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
? { fetch: authFetch(provider.settings?.fetch) }
: {}),
} }
}) })
} }
@ -104,7 +107,7 @@ export const GoogleVertexPlugin = define({
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
}), }),
) )
}), }),
@ -115,21 +118,20 @@ export const GoogleVertexAnthropicPlugin = define({
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
const project = const project =
item.provider.request.body.project ?? item.provider.settings?.project ??
process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GOOGLE_CLOUD_PROJECT ??
process.env.GCP_PROJECT ?? process.env.GCP_PROJECT ??
process.env.GCLOUD_PROJECT process.env.GCLOUD_PROJECT
const location = const location =
item.provider.request.body.location ?? item.provider.settings?.location ??
process.env.GOOGLE_CLOUD_LOCATION ?? process.env.GOOGLE_CLOUD_LOCATION ??
process.env.VERTEX_LOCATION ?? process.env.VERTEX_LOCATION ??
"global" "global"
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
if (project) provider.request.body.project = project provider.settings = { ...provider.settings, ...(project ? { project } : {}), location }
provider.request.body.location = location
}) })
} }
}) })
@ -162,7 +164,7 @@ export const GoogleVertexAnthropicPlugin = define({
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
}), }),
) )
}), }),

View file

@ -1,17 +1,17 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
export const KiloPlugin = define({ export const KiloPlugin = define({
id: "opencode.provider.kilo", id: "opencode.provider.kilo",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
provider.request.headers["X-Title"] = "opencode"
}) })
} }
}) })

View file

@ -1,6 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Integration } from "../../integration" import { Integration } from "../../integration"
import { ProviderV2 } from "../../provider"
export const LLMGatewayPlugin = define({ export const LLMGatewayPlugin = define({
id: "opencode.provider.llmgateway", id: "opencode.provider.llmgateway",
@ -10,14 +11,17 @@ export const LLMGatewayPlugin = define({
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.disabled) continue if (item.provider.disabled) continue
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
if (!configured.has(Integration.ID.make(item.provider.id))) continue if (!configured.has(Integration.ID.make(item.provider.id))) continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.headers = {
provider.request.headers["X-Title"] = "opencode" ...provider.headers,
provider.request.headers["X-Source"] = "opencode" "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-Source": "opencode",
}
}) })
} }
}) })

View file

@ -1,18 +1,22 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
export const NvidiaPlugin = define({ export const NvidiaPlugin = define({
id: "opencode.provider.nvidia", id: "opencode.provider.nvidia",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.headers = {
provider.request.headers["X-Title"] = "opencode" ...provider.headers,
provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode" "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": provider.headers?.["X-BILLING-INVOKE-ORIGIN"] ?? "OpenCode",
}
}) })
} }
}) })

View file

@ -178,8 +178,8 @@ export const OpenAIPlugin = define({
}) })
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/openai") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") continue
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
// OpenAIPlugin sends OpenAI models through Responses; this alias is a // OpenAIPlugin sends OpenAI models through Responses; this alias is a
@ -194,7 +194,7 @@ export const OpenAIPlugin = define({
// ChatGPT-plan tokens only authorize codex-eligible models, and the // ChatGPT-plan tokens only authorize codex-eligible models, and the
// subscription covers usage, so hide the rest and zero the cost. // subscription covers usage, so hide the rest and zero the cost.
evt.model.update(item.provider.id, model.id, (draft) => { evt.model.update(item.provider.id, model.id, (draft) => {
if (!OpenAICodex.eligible(draft.api.id)) { if (!OpenAICodex.eligible(draft.modelID ?? draft.id)) {
draft.enabled = false draft.enabled = false
return return
} }
@ -220,7 +220,7 @@ export const OpenAIPlugin = define({
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return if (evt.model.providerID !== ProviderV2.ID.openai) return
evt.language = evt.sdk.responses(evt.model.api.id) evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
}), }),
) )
}), }),

View file

@ -112,45 +112,43 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.integrationID = Integration.ID.make("opencode") provider.integrationID = Integration.ID.make("opencode")
if (item.name !== undefined) provider.name = item.name if (item.name !== undefined) provider.name = item.name
provider.api = item.npm provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
? { type: "aisdk", package: item.npm, url: item.api } provider.settings = {
: { type: "native", url: item.api, settings: {} } ...provider.settings,
Object.assign(provider.request.headers, item.options?.headers) ...withoutCredentials(item.options),
Object.assign(provider.request.body, withoutCredentials(item.options)) ...(item.api ? { baseURL: item.api } : {}),
}
provider.headers = { ...provider.headers, ...item.options?.headers }
}) })
for (const [modelID, config] of Object.entries(item.models ?? {})) { for (const [modelID, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, modelID, (model) => { catalog.model.update(providerID, modelID, (model) => {
if (config.family !== undefined) model.family = config.family if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name if (config.name !== undefined) model.name = config.name
if (config.id !== undefined) model.api.id = config.id if (config.id !== undefined) model.modelID = config.id
if (config.provider !== undefined) { if (config.provider !== undefined) {
model.api = config.provider.npm model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined
? { if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
id: model.api.id,
type: "aisdk",
package: config.provider.npm,
url: config.provider.api,
}
: { id: model.api.id, type: "native", url: config.provider.api, settings: {} }
} }
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input] if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output] if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
const packageName = config.provider?.npm ?? item.npm model.headers = { ...model.headers, ...config.headers }
const lowerer = ConfigProviderOptionsV1.get(packageName) model.settings = { ...model.settings, ...ConfigProviderOptionsV1.model(withoutCredentials(config.options)) }
Object.assign(model.request.headers, config.headers)
Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options)))
if (config.variants !== undefined) { if (config.variants !== undefined) {
model.variants ??= []
for (const [id, options] of Object.entries(config.variants)) { for (const [id, options] of Object.entries(config.variants)) {
const variantID = ModelV2.VariantID.make(id) const variantID = ModelV2.VariantID.make(id)
let existing = model.variants.find((item) => item.id === variantID) let existing = model.variants.find((item) => item.id === variantID)
if (!existing) { if (!existing) {
existing = { id: variantID, settings: {}, headers: {}, body: {} } existing = { id: variantID }
model.variants.push(existing) model.variants.push(existing)
} }
Object.assign(existing.headers, options.headers) existing.headers = { ...existing.headers, ...options.headers }
Object.assign(existing.body, lowerer.request(withoutCredentials(options))) existing.settings = {
...existing.settings,
...ConfigProviderOptionsV1.model(withoutCredentials(options)),
}
} }
} }
if (config.release_date !== undefined) { if (config.release_date !== undefined) {
@ -169,9 +167,9 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
const item = catalog.provider.get(ProviderV2.ID.opencode) const item = catalog.provider.get(ProviderV2.ID.opencode)
if (!item) return if (!item) return
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey) const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
catalog.provider.update(item.provider.id, (provider) => { catalog.provider.update(item.provider.id, (provider) => {
if (!hasKey) provider.request.body.apiKey = "public" if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
}) })
if (hasKey) return if (hasKey) return
for (const model of item.models.values()) { for (const model of item.models.values()) {

View file

@ -1,5 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const OpenRouterPlugin = define({ export const OpenRouterPlugin = define({
@ -7,11 +8,10 @@ export const OpenRouterPlugin = define({
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
provider.request.headers["X-Title"] = "opencode"
}) })
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) { for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
if (!item.models.has(modelID)) continue if (!item.models.has(modelID)) continue

View file

@ -40,7 +40,7 @@ export const SapAICorePlugin = define({
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.api.id) evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
}), }),
) )
}), }),

View file

@ -1,16 +1,16 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
export const VercelPlugin = define({ export const VercelPlugin = define({
id: "opencode.provider.vercel", id: "opencode.provider.vercel",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/vercel") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["http-referer"] = "https://opencode.ai/" provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
provider.request.headers["x-title"] = "opencode"
}) })
} }
}) })

View file

@ -15,7 +15,7 @@ export const XAIPlugin = define({
yield* ctx.aisdk.language( yield* ctx.aisdk.language(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
evt.language = evt.sdk.responses(evt.model.api.id) evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
}), }),
) )
}), }),

View file

@ -1,17 +1,21 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
export const ZenmuxPlugin = define({ export const ZenmuxPlugin = define({
id: "opencode.provider.zenmux", id: "opencode.provider.zenmux",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => { yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/" provider.headers = {
provider.request.headers["X-Title"] ??= "opencode" "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
...provider.headers,
}
}) })
} }
}) })

View file

@ -1,8 +1,9 @@
export * as VariantPlugin from "./variant" export * as VariantPlugin from "./variant"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin" import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ModelV2 } from "../model"
import { ProviderV2 } from "../provider"
export const Plugin = define({ export const Plugin = define({
id: "opencode.variant", id: "opencode.variant",
@ -11,14 +12,15 @@ export const Plugin = define({
for (const record of catalog.provider.list()) { for (const record of catalog.provider.list()) {
for (const model of record.models.values()) { for (const model of record.models.values()) {
catalog.model.update(model.providerID, model.id, (draft) => { catalog.model.update(model.providerID, model.id, (draft) => {
const generated = generate(draft) const generated = generate(draft, record.provider)
if (generated.length === 0) return if (generated.length === 0) return
const explicit = new Map(draft.variants.map((variant) => [variant.id, variant])) const variants = draft.variants ?? []
const generatedIDs = new Set(generated.map((variant) => variant.id)) const explicit = new Map(variants.map((variant) => [variant.id, variant]))
const generatedIDs = new Set<string>(generated.map((variant) => variant.id))
draft.variants = [ draft.variants = [
...generated.map((variant) => explicit.get(variant.id) ?? variant), ...generated.map((variant) => explicit.get(variant.id) ?? variant),
...draft.variants.filter((variant) => !generatedIDs.has(variant.id)), ...variants.filter((variant) => !generatedIDs.has(variant.id)),
] ]
}) })
} }
@ -27,14 +29,16 @@ export const Plugin = define({
}), }),
}) })
export function generate(model: ModelV2Info): ModelV2Info["variants"] { export function generate(
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return [] model: { readonly id: string; readonly modelID?: string; readonly package?: string },
const ids = `${model.id} ${model.api.id}`.toLowerCase() provider?: { readonly package: string },
): NonNullable<ModelV2.Info["variants"]> {
const packageName = model.package ?? provider?.package
if (!ProviderV2.isAISDK(packageName) || ProviderV2.packageName(packageName) !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase()
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return [] if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({ return ["high", "max"].map((id) => ({
id, id: ModelV2.VariantID.make(id),
settings: { reasoningEffort: id }, settings: { reasoningEffort: id },
headers: {},
body: {},
})) }))
} }

View file

@ -1,20 +1,106 @@
export * as ProviderV2 from "./provider" export * as ProviderV2 from "./provider"
import { Types } from "effect" import { Effect, Schema } from "effect"
import { pathToFileURL } from "url"
import { Provider } from "@opencode-ai/schema/provider" import { Provider } from "@opencode-ai/schema/provider"
import type { ProviderPackageDefinition } from "@opencode-ai/llm"
import { Npm } from "./npm"
import type { DeepMutable } from "./schema"
export const ID = Provider.ID export const ID = Provider.ID
export type ID = typeof ID.Type export type ID = typeof ID.Type
export const AISDK = Provider.AISDK export const AISDK_PREFIX = "aisdk:"
export const isAISDK = (value: string | undefined) => value?.startsWith(AISDK_PREFIX) ?? false
export const aisdk = (value: string) => (isAISDK(value) ? value : `${AISDK_PREFIX}${value}`)
export const packageName = (value: string | undefined) => {
if (value === undefined || !isAISDK(value)) return value
return value.slice(AISDK_PREFIX.length)
}
export const Native = Provider.Native type Json = Schema.Schema.Type<typeof Schema.Json>
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
const decodeJsonRecord = Schema.decodeUnknownSync(JsonRecord)
export const Api = Provider.Api export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.LoadError", {
export type Api = Provider.Api package: Schema.String,
export type MutableApi<T extends Api = Api> = T extends Api cause: Schema.Defect(),
? Omit<Types.DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any }) }) {}
: never export type ProviderPackage = ProviderPackageDefinition
const packages = new Map<string, Promise<unknown>>()
export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
const resolved = yield* Effect.sync(() => {
if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")) return specifier
try {
return import.meta.resolve(specifier)
} catch {
return undefined
}
})
if (resolved) return yield* importPackage(specifier, resolved)
if (!npm) {
return yield* new LoadError({
package: specifier,
cause: new Error(`Provider package ${specifier} is not installed`),
})
}
const parts = specifier.split("/")
const root = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? specifier)
const installed = yield* npm.add(root).pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
const entrypoint = yield* Effect.try({
try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
catch: (cause) => new LoadError({ package: specifier, cause }),
})
return yield* importPackage(specifier, entrypoint)
})
export function mergeOverlay(
base: Readonly<Record<string, unknown>> | undefined,
overlay: Readonly<Record<string, unknown>> | undefined,
): Record<string, Json> | undefined {
if (base === undefined) return overlay && decodeJsonRecord({ ...overlay })
if (overlay === undefined) return decodeJsonRecord({ ...base })
return decodeJsonRecord(
Object.fromEntries(
new Set([...Object.keys(base), ...Object.keys(overlay)]).values().map((key): [string, unknown] => {
const left = base[key]
const right = overlay[key]
if (right === undefined) return [key, left]
if (
typeof left === "object" &&
left !== null &&
!Array.isArray(left) &&
typeof right === "object" &&
right !== null &&
!Array.isArray(right)
)
return [
key,
mergeOverlay(left as Readonly<Record<string, unknown>>, right as Readonly<Record<string, unknown>>) ?? {},
]
return [key, right]
}),
),
)
}
export function mergeHeaders(
base: Readonly<Record<string, string>> | undefined,
overlay: Readonly<Record<string, string>> | undefined,
) {
if (base === undefined) return overlay && { ...overlay }
if (overlay === undefined) return { ...base }
return Object.fromEntries(
[...Object.entries(base), ...Object.entries(overlay)]
.reduce((result, entry) => {
result.set(entry[0].toLowerCase(), entry)
return result
}, new Map<string, [string, string]>())
.values(),
)
}
export const Request = Provider.Request export const Request = Provider.Request
export type Request = Provider.Request export type Request = Provider.Request
@ -25,9 +111,24 @@ export type Settings = Provider.Settings
export const Info = Provider.Info export const Info = Provider.Info
export type Info = Provider.Info export type Info = Provider.Info
export type MutableRequest = Types.DeepMutable<Request> export type MutableInfo = DeepMutable<Info>
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request"> & { const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) {
api: MutableApi const module = yield* Effect.tryPromise({
request: MutableRequest try: () => {
} const existing = packages.get(entrypoint)
if (existing) return existing
const loaded = import(entrypoint)
packages.set(entrypoint, loaded)
return loaded
},
catch: (cause) => new LoadError({ package: specifier, cause }),
})
if (typeof module !== "object" || module === null || typeof (module as { model?: unknown }).model !== "function") {
return yield* new LoadError({
package: specifier,
cause: new Error(`Provider package ${specifier} does not export model(modelID, settings)`),
})
}
return module as ProviderPackageDefinition
})

View file

@ -213,7 +213,10 @@ const layer = Layer.effect(
] ]
.filter((part): part is string => part !== undefined && part.length > 0) .filter((part): part is string => part !== undefined && part.length > 0)
.map(SystemPart.make), .map(SystemPart.make),
messages: [...toLLMMessages(context, model), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])], messages: [
...toLLMMessages(context, resolved.ref),
...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []),
],
tools: toolMaterialization?.definitions ?? [], tools: toolMaterialization?.definitions ?? [],
toolChoice: isLastStep ? "none" : undefined, toolChoice: isLastStep ? "none" : undefined,
}) })

View file

@ -1,7 +1,7 @@
export * as SessionRunnerModel from "./model" export * as SessionRunnerModel from "./model"
import { makeLocationNode } from "../../effect/app-node" import { makeLocationNode } from "../../effect/app-node"
import { type Model } from "@opencode-ai/llm" import { Model } from "@opencode-ai/llm"
// ast-grep-ignore: no-star-import // ast-grep-ignore: no-star-import
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages" import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
// ast-grep-ignore: no-star-import // ast-grep-ignore: no-star-import
@ -11,10 +11,12 @@ import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
import { Auth, type AnyRoute } from "@opencode-ai/llm/route" import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { produce } from "immer" import { produce } from "immer"
import { AISDK } from "../../aisdk"
import { Catalog } from "../../catalog" import { Catalog } from "../../catalog"
import { Credential } from "../../credential" import { Credential } from "../../credential"
import { Integration } from "../../integration" import { Integration } from "../../integration"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { Npm } from "../../npm"
import { OpenAICodex } from "../../plugin/provider/openai-codex" import { OpenAICodex } from "../../plugin/provider/openai-codex"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { SessionSchema } from "../schema" import { SessionSchema } from "../schema"
@ -55,16 +57,16 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
} }
} }
export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()( export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedApiError", "SessionRunnerModel.UnsupportedPackageError",
{ {
providerID: ProviderV2.ID, providerID: ProviderV2.ID,
modelID: ModelV2.ID, modelID: ModelV2.ID,
api: Schema.String, package: Schema.String,
}, },
) { ) {
override get message() { override get message() {
return `Unsupported API for ${this.providerID}/${this.modelID}: ${this.api}` return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
} }
} }
@ -72,7 +74,7 @@ export type Error =
| ModelNotSelectedError | ModelNotSelectedError
| ModelUnavailableError | ModelUnavailableError
| VariantUnavailableError | VariantUnavailableError
| UnsupportedApiError | UnsupportedPackageError
| Integration.AuthorizationError | Integration.AuthorizationError
export interface Resolved { export interface Resolved {
@ -104,41 +106,50 @@ export const resolved = (model: Model, variant?: ModelV2.VariantID): Resolved =>
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
if (credential?.type === "key") return Auth.value(credential.key) if (credential?.type === "key") return Auth.value(credential.key)
if (credential?.type === "oauth") return Auth.value(credential.access) if (credential?.type === "oauth") return Auth.value(credential.access)
const value = model.request.body.apiKey ?? model.api.settings?.apiKey const value = model.settings?.apiKey
if (typeof value === "string") return Auth.value(value) if (typeof value === "string") return Auth.value(value)
} }
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => { const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
const body = model.request.body route.with({
const httpBody = Object.hasOwn(body, "apiKey")
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
: body
return route.with({
provider: model.providerID, provider: model.providerID,
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url }, endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
headers: model.request.headers, headers: providerHeaders(model),
providerOptions: providerOptions(model), providerOptions: providerOptions(model),
http: { body: httpBody }, http: model.body === undefined ? undefined : { body: model.body },
limits: { context: model.limit.context, output: model.limit.output }, limits: { context: model.limit.context, output: model.limit.output },
}) })
const providerHeaders = (model: ModelV2.Info) => {
const packageName = ProviderV2.packageName(model.package)
const generated = new Map<string, string>()
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
generated.set("OpenAI-Organization", model.settings.organization)
if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string")
generated.set("OpenAI-Project", model.settings.project)
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
generated.set("Authorization", `Bearer ${model.settings.authToken}`)
return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
} }
const providerOptions = ( const providerOptions = (
model: ModelV2.Info, model: ModelV2.Info,
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => { ): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
if (Object.keys(model.request.settings).length === 0) return undefined if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined
if (model.api.type !== "aisdk") return undefined const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
if (model.api.package === "@ai-sdk/openai") return { openai: model.request.settings } if (Object.keys(settings).length === 0) return undefined
if (model.api.package === "@ai-sdk/anthropic") return { anthropic: model.request.settings } const packageName = ProviderV2.packageName(model.package)
if (model.api.package === "@ai-sdk/openai-compatible") return { openai: model.request.settings } if (packageName === "@ai-sdk/openai") return { openai: settings }
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
} }
export const withVariant = ( export const withVariant = (
model: ModelV2.Info, model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined, variantID: ModelV2.VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => { ): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
const id = variantID === "default" || variantID === undefined ? model.request.variant : variantID const id = variantID === "default" ? undefined : variantID
const variant = model.variants.find((item) => item.id === id) const variant = model.variants?.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default") if (!variant && variantID !== undefined && variantID !== "default")
return Effect.fail( return Effect.fail(
new VariantUnavailableError({ new VariantUnavailableError({
@ -150,81 +161,140 @@ export const withVariant = (
return Effect.succeed( return Effect.succeed(
variant variant
? produce(model, (draft) => { ? produce(model, (draft) => {
Object.assign(draft.request.settings, variant.settings) draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
Object.assign(draft.request.headers, variant.headers) draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
Object.assign(draft.request.body, variant.body) draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
}) })
: model, : model,
) )
} }
const apiName = (model: ModelV2.Info) => export interface Dependencies {
model.api.type === "aisdk" ? `${model.api.type}:${model.api.package}` : model.api.type readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
}
export const fromCatalogModel = ( export const fromCatalogModel = (
model: ModelV2.Info, model: ModelV2.Info,
credential?: Credential.Value, credential?: Credential.Value,
): Effect.Effect<Model, UnsupportedApiError> => { dependencies: Dependencies = {},
): Effect.Effect<Model, UnsupportedPackageError> => {
const resolved = const resolved =
credential?.type !== "key" || credential.metadata === undefined credential?.type !== "key" || credential.metadata === undefined
? model ? model
: produce(model, (draft) => { : produce(model, (draft) => {
Object.assign(draft.request.body, credential.metadata) draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata)
}) })
const packageName = ProviderV2.packageName(resolved.package)
const key = apiKey(resolved, credential) const key = apiKey(resolved, credential)
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
// ChatGPT-plan OAuth tokens are not API-key credentials: the public API rejects if (
// them, so requests must target the codex backend with the account header. OpenAICodex.isChatGPT(credential) &&
if (OpenAICodex.isChatGPT(credential)) { !ProviderV2.isAISDK(resolved.package) &&
const account = OpenAICodex.accountID(credential) isNativeOpenAI(resolved.package)
return Effect.succeed( ) {
withDefaults(resolved, OpenAIResponses.route) return Effect.succeed(codexModel(resolved, credential, key))
.with({ }
endpoint: { baseURL: OpenAICodex.baseURL },
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen( if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }), if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
),
})
.model({ id: resolved.api.id }),
)
}
return Effect.succeed( return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route) withDefaults(resolved, OpenAIResponses.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.api.id }), .model({ id: resolved.modelID ?? resolved.id }),
) )
} }
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/anthropic") { if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
return Effect.succeed( return Effect.succeed(
withDefaults(resolved, AnthropicMessages.route) withDefaults(resolved, AnthropicMessages.route)
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
.model({ id: resolved.api.id }), .model({ id: resolved.modelID ?? resolved.id }),
) )
} }
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai-compatible" && resolved.api.url) { if (
ProviderV2.isAISDK(resolved.package) &&
packageName === "@ai-sdk/openai-compatible" &&
typeof resolved.settings?.baseURL === "string"
) {
return Effect.succeed( return Effect.succeed(
withDefaults(resolved, OpenAICompatibleChat.route) withDefaults(resolved, OpenAICompatibleChat.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.api.id }), .model({ id: resolved.modelID ?? resolved.id }),
) )
} }
return Effect.fail( if (ProviderV2.isAISDK(resolved.package)) {
new UnsupportedApiError({ if (!dependencies.loadAISDK) return Effect.fail(unsupported(resolved))
providerID: resolved.providerID, const runtime = produce(resolved, (draft) => {
modelID: resolved.id, draft.settings = ProviderV2.mergeOverlay(draft.settings, {
api: apiName(resolved), ...(credential?.type === "key" ? { apiKey: credential.key } : {}),
}), ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
) ...credential?.metadata,
})
})
return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved)))
}
if (!resolved.package) return Effect.fail(unsupported(resolved))
const specifier = resolved.package
return Effect.gen(function* () {
const module = yield* (dependencies.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe(
Effect.mapError(() => unsupported(resolved)),
)
const settings = {
...resolved.settings,
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
...credential?.metadata,
headers: resolved.headers,
body: resolved.body,
limits: { context: resolved.limit.context, output: resolved.limit.output },
}
return yield* Effect.try({
try: () =>
Model.update(module.model(resolved.modelID ?? resolved.id, settings), { provider: resolved.providerID }),
catch: () => unsupported(resolved),
})
})
} }
export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) => const isNativeOpenAI = (packageName: string | undefined) =>
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential))) packageName === "@opencode-ai/llm/providers/openai" ||
packageName?.startsWith("@opencode-ai/llm/providers/openai/") === true
export const supported = (model: ModelV2.Info) => const codexModel = (
model.api.type === "aisdk" && model: ModelV2.Info,
(model.api.package === "@ai-sdk/openai" || credential: Credential.Value | undefined,
model.api.package === "@ai-sdk/anthropic" || key: ReturnType<typeof Auth.value> | undefined,
(model.api.package === "@ai-sdk/openai-compatible" && model.api.url !== undefined)) ) => {
const account = OpenAICodex.accountID(credential)
return withDefaults(model, OpenAIResponses.route)
.with({
endpoint: { baseURL: OpenAICodex.baseURL },
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen(
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }),
),
})
.model({ id: model.modelID ?? model.id })
}
const unsupported = (model: ModelV2.Info) =>
new UnsupportedPackageError({
providerID: model.providerID,
modelID: model.id,
package: model.package ?? "unknown",
})
export const resolve = (
session: SessionSchema.Info,
model: ModelV2.Info,
credential?: Credential.Value,
dependencies?: Dependencies,
) =>
withVariant(model, session.model?.variant).pipe(
Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)),
)
export const supported = (model: ModelV2.Info) => Boolean(model.package)
/** Resolves models from the catalog belonging to the current Location runtime. */ /** Resolves models from the catalog belonging to the current Location runtime. */
const layer = Layer.effect( const layer = Layer.effect(
@ -232,6 +302,8 @@ const layer = Layer.effect(
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const npm = yield* Npm.Service
const aisdk = yield* AISDK.Service
return Service.of({ return Service.of({
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) { resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
// Location plugins populate and filter the catalog asynchronously during layer startup. // Location plugins populate and filter the catalog asynchronously during layer startup.
@ -257,6 +329,10 @@ const layer = Layer.effect(
session, session,
selected, selected,
connection ? yield* integrations.connection.resolve(connection) : undefined, connection ? yield* integrations.connection.resolve(connection) : undefined,
{
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
},
) )
return { return {
model, model,
@ -271,4 +347,8 @@ const layer = Layer.effect(
}), }),
) )
export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, Integration.node] }) export const node = makeLocationNode({
service: Service,
layer,
deps: [Catalog.node, Integration.node, Npm.node, AISDK.node],
})

View file

@ -4,10 +4,10 @@ import {
ToolOutput, ToolOutput,
ToolResultPart, ToolResultPart,
type ContentPart, type ContentPart,
type Model,
type ProviderMetadata, type ProviderMetadata,
} from "@opencode-ai/llm" } from "@opencode-ai/llm"
import { Option, Schema } from "effect" import { Option, Schema } from "effect"
import type { ModelV2 } from "../../model"
import { SessionMessage } from "../message" import { SessionMessage } from "../message"
import type { FileAttachment } from "@opencode-ai/schema/prompt" import type { FileAttachment } from "@opencode-ai/schema/prompt"
@ -88,9 +88,9 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
} }
} }
const assistant = (message: SessionMessage.Assistant, model: Model) => { const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
const sameModel = const sameModel =
String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id) String(message.model.providerID) === String(model.providerID) && String(message.model.id) === String(model.id)
const reuseProviderMetadata = sameModel && message.error === undefined const reuseProviderMetadata = sameModel && message.error === undefined
const content = message.content.flatMap((item): ContentPart[] => { const content = message.content.flatMap((item): ContentPart[] => {
if (item.type === "text") return [{ type: "text", text: item.text }] if (item.type === "text") return [{ type: "text", text: item.text }]
@ -133,7 +133,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
] ]
} }
function toLLMMessage(message: SessionMessage.Message, model: Model): Message[] { function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Message[] {
switch (message.type) { switch (message.type) {
case "agent-switched": case "agent-switched":
case "model-switched": case "model-switched":
@ -197,5 +197,5 @@ ${message.recent}
} }
/** Translate projected V2 Session history into canonical @opencode-ai/llm context. */ /** Translate projected V2 Session history into canonical @opencode-ai/llm context. */
export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: Model) => export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: ModelV2.Ref) =>
messages.flatMap((message) => toLLMMessage(message, model)) messages.flatMap((message) => toLLMMessage(message, model))

View file

@ -2,10 +2,12 @@ export * as ConfigMigrateV1 from "./migrate"
import { ConfigV1 } from "./config" import { ConfigV1 } from "./config"
import { ConfigAgentV1 } from "./agent" import { ConfigAgentV1 } from "./agent"
import { ConfigCommandV1 } from "./command"
import { ConfigMCPV1 } from "./mcp" import { ConfigMCPV1 } from "./mcp"
import { ConfigPermissionV1 } from "./permission" import { ConfigPermissionV1 } from "./permission"
import { ConfigProviderV1 } from "./provider" import { ConfigProviderV1 } from "./provider"
import { ConfigProviderOptionsV1 } from "./provider-options" import { ConfigProviderOptionsV1 } from "./provider-options"
import { ProviderV2 } from "../../provider"
const keys = new Set([ const keys = new Set([
"logLevel", "logLevel",
@ -48,7 +50,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
return { return {
$schema: info.$schema, $schema: info.$schema,
shell: info.shell, shell: info.shell,
model: info.model, model: modelSelection(info.model),
default_agent: info.default_agent, default_agent: info.default_agent,
autoupdate: info.autoupdate, autoupdate: info.autoupdate,
share: info.share ?? (info.autoshare ? "auto" : undefined), share: info.share ?? (info.autoshare ? "auto" : undefined),
@ -72,7 +74,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
buffer: info.compaction.reserved, buffer: info.compaction.reserved,
}, },
skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])], skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])],
commands: info.command, commands: commands(info.command),
instructions: info.instructions, instructions: info.instructions,
references: info.references ?? info.reference, references: info.references ?? info.reference,
plugins: info.plugin?.map((plugin) => plugins: info.plugin?.map((plugin) =>
@ -126,8 +128,7 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
...(info.top_p === undefined ? {} : { top_p: info.top_p }), ...(info.top_p === undefined ? {} : { top_p: info.top_p }),
} }
return { return {
model: info.model, model: modelSelection(info.model, info.variant),
variant: info.variant,
request: Object.keys(body).length ? { body } : undefined, request: Object.keys(body).length ? { body } : undefined,
system: info.prompt, system: info.prompt,
description: info.description, description: info.description,
@ -140,6 +141,32 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
} }
} }
function commands(info?: Readonly<Record<string, ConfigCommandV1.Info>>) {
if (!info) return undefined
return Object.fromEntries(
Object.entries(info).map(([id, command]) => [
id,
{
template: command.template,
description: command.description,
agent: command.agent,
model: modelSelection(command.model, command.variant),
subtask: command.subtask,
},
]),
)
}
function modelSelection(input?: string, variant?: string) {
if (input === undefined || !/^[^/#]+\/[^#]+$/.test(input)) return undefined
const separator = input.indexOf("/")
return {
providerID: input.slice(0, separator),
model: input.slice(separator + 1),
...(variant === undefined || variant.length === 0 || variant.includes("#") ? {} : { variant }),
}
}
function mcp(info: typeof ConfigV1.Info.Type) { function mcp(info: typeof ConfigV1.Info.Type) {
const servers = Object.fromEntries( const servers = Object.fromEntries(
Object.entries(info.mcp ?? {}).flatMap(([name, server]) => Object.entries(info.mcp ?? {}).flatMap(([name, server]) =>
@ -184,31 +211,22 @@ function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
} }
function migrateProvider(info: ConfigProviderV1.Info) { function migrateProvider(info: ConfigProviderV1.Info) {
const lowerer = ConfigProviderOptionsV1.get(info.npm) const options = ConfigProviderOptionsV1.provider(info.options ?? {})
const options = lowerer.provider(info.options ?? {})
const url = info.api ?? options.url
return { return {
name: info.name, name: info.name,
env: info.env, env: info.env,
api: info.npm package: info.npm ? ProviderV2.aisdk(info.npm) : undefined,
? { settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings,
type: "aisdk" as const, headers: info.options && options.headers,
package: info.npm, body: info.options && options.body,
...(url === undefined ? {} : { url }),
settings: options.settings ?? {},
}
: undefined,
request: info.options && { headers: options.headers, body: options.body },
models: models:
info.models && info.models &&
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])), Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model)])),
} }
} }
function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) { function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
const packageID = info.provider?.npm ?? packageName const settings = info.options && ConfigProviderOptionsV1.model(info.options)
const lowerer = ConfigProviderOptionsV1.get(packageID)
const request = info.options && lowerer.request(info.options)
const costs = info.cost && [ const costs = info.cost && [
{ {
input: info.cost.input, input: info.cost.input,
@ -231,29 +249,18 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] } ? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
: undefined : undefined
return { return {
modelID: info.id,
family: info.family, family: info.family,
name: info.name, name: info.name,
api: info.provider?.npm package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined,
? { settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
...(info.id === undefined ? {} : { id: info.id }),
type: "aisdk" as const,
package: info.provider.npm,
...(info.provider.api === undefined ? {} : { url: info.provider.api }),
settings: {},
}
: info.id === undefined
? undefined
: { id: info.id },
capabilities, capabilities,
request: (info.headers || request) && { headers: info.headers,
headers: info.headers,
body: request,
},
variants: variants:
info.variants && info.variants &&
Object.entries(info.variants).map(([id, options]) => ({ Object.entries(info.variants).map(([id, options]) => ({
id, id,
body: lowerer.request(options), settings: ConfigProviderOptionsV1.model(options),
})), })),
cost: costs, cost: costs,
disabled: info.status === "deprecated" ? true : undefined, disabled: info.status === "deprecated" ? true : undefined,

View file

@ -3,225 +3,29 @@ export * as ConfigProviderOptionsV1 from "./provider-options"
type Options = Readonly<Record<string, unknown>> type Options = Readonly<Record<string, unknown>>
export interface ProviderResult { export interface ProviderResult {
readonly settings: Record<string, unknown>
readonly headers?: Record<string, string> readonly headers?: Record<string, string>
readonly body?: Record<string, unknown> readonly body?: Record<string, unknown>
readonly url?: string
readonly settings?: Record<string, unknown>
} }
export interface Lowerer { export function provider(options: Options): ProviderResult {
readonly provider: (options: Options) => ProviderResult const headers = options.headers
readonly request: (options: Options) => Record<string, unknown> const body = options.body
} const settings = Object.fromEntries(Object.entries(options).filter(([key]) => key !== "headers" && key !== "body"))
const headerOverlay =
export function get(packageName?: string): Lowerer { typeof headers === "object" && headers !== null && !Array.isArray(headers)
const key = packageName ?? "" ? Object.fromEntries(
return Object.hasOwn(lowerers, key) ? lowerers[key]! : raw Object.entries(headers).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
} )
: undefined
const raw: Lowerer = { const bodyOverlay = typeof body === "object" && body !== null && !Array.isArray(body) ? { ...body } : undefined
provider(options) {
return { body: clone(options) }
},
request: clone,
}
const openai: Lowerer = {
provider(options) {
return {
url: string(options.baseURL),
headers: compact({
Authorization: bearer(options.apiKey),
"OpenAI-Organization": string(options.organization),
"OpenAI-Project": string(options.project),
...headers(options.headers),
}),
body: body(options.body),
settings: omit(options, ["apiKey", "baseURL", "organization", "project", "headers", "body"]),
}
},
request(options) {
const result = snake(options)
if (options.reasoningEffort !== undefined || options.reasoningSummary !== undefined) {
result.reasoning = {
...(isRecord(result.reasoning) ? result.reasoning : {}),
...(options.reasoningEffort !== undefined ? { effort: options.reasoningEffort } : {}),
...(options.reasoningSummary !== undefined ? { summary: options.reasoningSummary } : {}),
}
delete result.reasoning_effort
delete result.reasoning_summary
}
if (options.textVerbosity !== undefined) {
result.text = { ...(isRecord(result.text) ? result.text : {}), verbosity: options.textVerbosity }
delete result.text_verbosity
}
return result
},
}
const anthropic: Lowerer = {
provider(options) {
return {
url: string(options.baseURL),
headers: compact({
"x-api-key": string(options.apiKey),
Authorization: options.authToken ? bearer(options.authToken) : undefined,
...headers(options.headers),
}),
body: body(options.body),
settings: omit(options, ["apiKey", "authToken", "baseURL", "headers", "body"]),
}
},
request(options) {
const result = snake(options)
if (options.effort !== undefined || options.taskBudget !== undefined) {
result.output_config = compactUnknown({ effort: options.effort, task_budget: options.taskBudget })
delete result.effort
delete result.task_budget
}
if (isRecord(options.metadata) && options.metadata.userId !== undefined) {
result.metadata = { ...(isRecord(result.metadata) ? result.metadata : {}), user_id: options.metadata.userId }
}
return result
},
}
const google: Lowerer = {
provider(options) {
return {
url: string(options.baseURL),
headers: compact({ "x-goog-api-key": string(options.apiKey), ...headers(options.headers) }),
body: body(options.body),
settings: omit(options, ["apiKey", "baseURL", "headers", "body"]),
}
},
request(options) {
const generationConfig = pick(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"])
return {
...omit(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"]),
...(Object.keys(generationConfig).length ? { generationConfig } : {}),
}
},
}
const azure: Lowerer = {
provider(options) {
return {
url: string(options.baseURL),
headers: compact({ "api-key": string(options.apiKey), ...headers(options.headers) }),
body: body(options.body),
settings: omit(options, ["apiKey", "baseURL", "headers", "body"]),
}
},
request: openai.request,
}
const bedrock: Lowerer = {
provider(options) {
return direct(options)
},
request(options) {
return { additionalModelRequestFields: clone(options) }
},
}
const openaiCompatible: Lowerer = {
provider(options) {
return { ...direct(options, ["baseURL"]), url: string(options.baseURL) }
},
request(options) {
const result = clone(options)
if (options.reasoningEffort !== undefined) {
result.reasoning_effort = options.reasoningEffort
delete result.reasoningEffort
}
return result
},
}
const lowerers: Readonly<Record<string, Lowerer>> = {
"@ai-sdk/openai": openai,
"@ai-sdk/anthropic": anthropic,
"@ai-sdk/google-vertex/anthropic": anthropic,
"@ai-sdk/google": google,
"@ai-sdk/google-vertex": google,
"@ai-sdk/azure": azure,
"@ai-sdk/amazon-bedrock": bedrock,
"@ai-sdk/openai-compatible": openaiCompatible,
"@ai-sdk/cerebras": openaiCompatible,
"@ai-sdk/deepinfra": openaiCompatible,
"@ai-sdk/groq": openaiCompatible,
"@ai-sdk/mistral": openaiCompatible,
"@ai-sdk/togetherai": openaiCompatible,
"@ai-sdk/xai": openaiCompatible,
"@openrouter/ai-sdk-provider": openaiCompatible,
"ai-gateway-provider": openaiCompatible,
"venice-ai-sdk-provider": openaiCompatible,
}
function direct(options: Options, extraKeys: ReadonlyArray<string> = []): ProviderResult {
return { return {
headers: headers(options.headers), settings,
body: body(options.body), headers: headerOverlay,
settings: omit(options, ["headers", "body", ...extraKeys]), body: bodyOverlay,
} }
} }
function body(input: unknown) { export function model(options: Options) {
if (!isRecord(input)) return undefined
return { ...input }
}
function snake(options: Options) {
return Object.fromEntries(Object.entries(options).map(([key, value]) => [snakeKey(key), snakeValue(value)]))
}
function snakeValue(value: unknown): unknown {
if (Array.isArray(value)) return value.map(snakeValue)
if (!isRecord(value)) return value
return Object.fromEntries(Object.entries(value).map(([key, value]) => [snakeKey(key), snakeValue(value)]))
}
function snakeKey(key: string) {
return key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase())
}
function clone(options: Options) {
return { ...options } return { ...options }
} }
function omit(options: Options, keys: ReadonlyArray<string>) {
return Object.fromEntries(Object.entries(options).filter(([key]) => !keys.includes(key)))
}
function pick(options: Options, keys: ReadonlyArray<string>) {
return Object.fromEntries(Object.entries(options).filter(([key]) => keys.includes(key)))
}
function headers(input: unknown) {
if (!isRecord(input)) return undefined
return Object.fromEntries(
Object.entries(input).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
)
}
function compact(input: Record<string, string | undefined>) {
const entries = Object.entries(input).filter((entry): entry is [string, string] => entry[1] !== undefined)
return entries.length ? Object.fromEntries(entries) : undefined
}
function compactUnknown(input: Record<string, unknown>) {
return Object.fromEntries(Object.entries(input).filter((entry) => entry[1] !== undefined))
}
function string(input: unknown) {
return typeof input === "string" && input ? input : undefined
}
function bearer(input: unknown) {
return typeof input === "string" && input ? `Bearer ${input}` : undefined
}
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input)
}

View file

@ -0,0 +1,85 @@
import type { LanguageModelV3CallOptions } from "@ai-sdk/provider"
import { AISDK } from "@opencode-ai/core/aisdk"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { LLM } from "@opencode-ai/llm"
import { LLMClient } from "@opencode-ai/llm/route"
import { expect } from "bun:test"
import { Effect } from "effect"
import { testEffect } from "./lib/effect"
const it = testEffect(AISDK.locationLayer)
type Fetch = (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response>
const model = (packageName: string, settings: Record<string, unknown> = {}) =>
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")),
modelID: ModelV2.ID.make("api-model"),
package: ProviderV2.aisdk(packageName),
settings,
limit: { context: 100, output: 20 },
})
it.effect("keys language models by package and flattened overlays", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
const loaded: string[] = []
yield* aisdk.hook.sdk((event) => {
loaded.push(event.package)
event.sdk = { languageModel: () => ({ package: event.package }) }
})
const first = yield* aisdk.language(model("first", { region: "us-east-1" }))
const second = yield* aisdk.language(model("second", { region: "us-east-1" }))
const third = yield* aisdk.language(
model("second", { region: "us-east-1", fetch: async () => new Response("ok") }),
)
expect(first).not.toBe(second)
expect(second).not.toBe(third)
expect(loaded).toEqual(["first", "second", "second"])
}),
)
it.effect("projects request settings, headers, and raw body overlays", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
let wrappedFetch: Fetch | undefined
let body: unknown
const customFetch: Fetch = async (_input, init) => {
body = init?.body
return new Response("ok")
}
yield* aisdk.hook.sdk((event) => {
wrappedFetch = event.options.fetch
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
})
const resolved = yield* aisdk.model(
ModelV2.Info.make({
...model("@ai-sdk/google", {
apiKey: "secret",
fetch: customFetch,
thinkingConfig: { thinkingBudget: 1024 },
}),
headers: { "x-test": "header" },
body: { safety_setting: "strict" },
}),
)
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
LLM.request({ model: resolved, prompt: "Hello" }),
)
expect(prepared.body.providerOptions).toEqual({
google: { thinkingConfig: { thinkingBudget: 1024 } },
})
expect(prepared.body.headers).toEqual({ "x-test": "header" })
expect(wrappedFetch).toBeFunction()
if (wrappedFetch === undefined) return yield* Effect.die("Expected wrapped fetch")
const fetchRequest = wrappedFetch
yield* Effect.promise(() =>
fetchRequest("https://provider.example", { method: "POST", body: JSON.stringify({ model: "api-model" }) }),
)
expect(JSON.parse(String(body))).toEqual({ model: "api-model", safety_setting: "strict" })
}),
)

View file

@ -61,14 +61,14 @@ describe("CatalogV2", () => {
}) })
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({}) expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
yield* credentials.create({ yield* credentials.create({
integrationID, integrationID,
label: "Second", label: "Second",
value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }), value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }),
}) })
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({}) expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
}).pipe(Effect.provide(localCatalogLayer)) }).pipe(Effect.provide(localCatalogLayer))
}) })
@ -130,111 +130,91 @@ describe("CatalogV2", () => {
), ),
) )
it.effect("normalizes provider baseURL into api url", () => it.effect("stores provider package settings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://override.example.com" }
package: "@ai-sdk/openai-compatible",
url: "https://default.example.com",
}
provider.request.body.baseURL = "https://override.example.com"
}), }),
) )
expect(required(yield* catalog.provider.get(providerID)).api).toEqual({ expect(required(yield* catalog.provider.get(providerID))).toMatchObject({
type: "aisdk", package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
package: "@ai-sdk/openai-compatible", settings: { baseURL: "https://override.example.com" },
url: "https://override.example.com",
}) })
}), }),
) )
it.effect("normalizes model baseURL into api url", () => it.effect("uses model package settings over provider settings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model") const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://provider.example.com" }
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
}) })
catalog.model.update(providerID, modelID, (model) => { catalog.model.update(providerID, modelID, (model) => {
model.api = { model.modelID = ModelV2.ID.make("upstream-model")
id: modelID, model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", model.settings = { baseURL: "https://override.example.com" }
package: "@ai-sdk/openai-compatible",
url: "https://model.example.com",
}
model.request.body.baseURL = "https://override.example.com"
}) })
}) })
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({ expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
id: modelID, modelID: ModelV2.ID.make("upstream-model"),
type: "aisdk", package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
package: "@ai-sdk/openai-compatible", settings: { baseURL: "https://override.example.com" },
url: "https://override.example.com",
settings: {},
}) })
}), }),
) )
it.effect("resolves default model api from provider api", () => it.effect("resolves default model package settings from the provider", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model") const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://provider.example.com" }
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
}) })
catalog.model.update(providerID, modelID, () => {}) catalog.model.update(providerID, modelID, () => {})
}) })
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({ expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
id: modelID, package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
type: "aisdk", settings: { baseURL: "https://provider.example.com" },
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}) })
}), }),
) )
it.effect("resolves provider and model request merges", () => it.effect("resolves provider and model overlay merges", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test") const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model") const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => { catalog.provider.update(providerID, (provider) => {
provider.request.headers.provider = "provider" provider.settings = { provider: true, shared: "provider" }
provider.request.headers.shared = "provider" provider.headers = { provider: "provider", shared: "provider" }
provider.request.body.provider = true provider.body = { provider: true, shared: "provider" }
}) })
catalog.model.update(providerID, modelID, (model) => { catalog.model.update(providerID, modelID, (model) => {
model.request.headers.model = "model" model.settings = { model: true, shared: "model" }
model.request.headers.shared = "model" model.headers = { model: "model", shared: "model" }
model.request.body.model = true model.body = { model: true, shared: "model" }
model.request.body.request = true
model.request.body.shared = "model"
}) })
}) })
const model = required(yield* catalog.model.get(providerID, modelID)) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) expect(model.settings).toEqual({ provider: true, shared: "model", model: true })
expect(model.request.body).toEqual({ provider: true, model: true, request: true, shared: "model" }) expect(model.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
expect(model.body).toEqual({ provider: true, shared: "model", model: true })
}), }),
) )

View file

@ -62,7 +62,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
info: decode({ info: decode({
permissions: [{ action: "read", resource: "*", effect: "allow" }], permissions: [{ action: "read", resource: "*", effect: "allow" }],
agents: { agents: {
reviewer: { variant: "high", hidden: true }, reviewer: { model: "openrouter/openai/gpt-5#high", hidden: true },
removed: { disabled: true }, removed: { disabled: true },
late: { late: {
permissions: [{ action: "edit", resource: "*", effect: "allow" }], permissions: [{ action: "edit", resource: "*", effect: "allow" }],
@ -170,7 +170,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
hidden: true, hidden: true,
color: "warning", color: "warning",
steps: 12, steps: 12,
model: { providerID: "anthropic", id: "claude-sonnet", variant: undefined }, model: { providerID: "anthropic", id: "claude-sonnet" },
}) })
expect(reviewer.request).toEqual({ expect(reviewer.request).toEqual({
settings: {}, settings: {},

View file

@ -44,8 +44,7 @@ describe("ConfigCommandPlugin.Plugin", () => {
`--- `---
description: File review description: File review
agent: reviewer agent: reviewer
model: anthropic/claude model: anthropic/claude#high
variant: high
subtask: true subtask: true
--- ---
Review files`, Review files`,

View file

@ -4,6 +4,7 @@ import { describe, expect } from "bun:test"
import { Effect, Fiber, Layer, PubSub, Schema, Stream } from "effect" import { Effect, Fiber, Layer, PubSub, Schema, Stream } from "effect"
import { FastCheck } from "effect/testing" import { FastCheck } from "effect/testing"
import { Config } from "@opencode-ai/core/config" import { Config } from "@opencode-ai/core/config"
import { ConfigModel } from "@opencode-ai/core/config/model"
import { Config as ConfigSchema } from "@opencode-ai/schema/config" import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { ConfigProvider } from "@opencode-ai/core/config/provider" import { ConfigProvider } from "@opencode-ai/core/config/provider"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@ -16,12 +17,14 @@ import { EventV2 } from "@opencode-ai/core/event"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { Project } from "@opencode-ai/core/project" import { Project } from "@opencode-ai/core/project"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location" import { location } from "../fixture/location"
import { tmpdir } from "../fixture/tmpdir" import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const it = testEffect(Layer.empty) const it = testEffect(Layer.empty)
const selection = Schema.decodeUnknownSync(ConfigModel.Selection)
function testLayer( function testLayer(
directory: string, directory: string,
@ -47,11 +50,10 @@ function testLayer(
} }
const provider = { const provider = {
api: { type: "native", settings: {} }, package: "native",
request: { settings: {},
headers: {}, headers: {},
body: {}, body: {},
},
models: {}, models: {},
} }
@ -105,13 +107,19 @@ describe("Config", () => {
it.effect("returns the latest defined scalar from priority-ordered documents", () => it.effect("returns the latest defined scalar from priority-ordered documents", () =>
Effect.sync(() => { Effect.sync(() => {
const entries = [ const entries = [
new Config.Document({ type: "document", info: new Config.Info({ model: "openrouter/openai/gpt-5" }) }), new Config.Document({
type: "document",
info: new Config.Info({ model: selection("openrouter/openai/gpt-5") }),
}),
new Config.Directory({ type: "directory", path: AbsolutePath.make("/skills") }), new Config.Directory({ type: "directory", path: AbsolutePath.make("/skills") }),
new Config.Document({ type: "document", info: new Config.Info({}) }), new Config.Document({ type: "document", info: new Config.Info({}) }),
new Config.Document({ type: "document", info: new Config.Info({ model: "openrouter/openai/gpt-5.5" }) }), new Config.Document({
type: "document",
info: new Config.Info({ model: selection("openrouter/openai/gpt-5.5") }),
}),
] ]
expect(Config.latest(entries, "model")).toBe("openrouter/openai/gpt-5.5") expect(Config.latest(entries, "model")).toEqual(selection("openrouter/openai/gpt-5.5"))
expect(Config.latest(entries, "default_agent")).toBeUndefined() expect(Config.latest(entries, "default_agent")).toBeUndefined()
}), }),
) )
@ -142,7 +150,12 @@ describe("Config", () => {
Effect.sync(() => { Effect.sync(() => {
FastCheck.assert( FastCheck.assert(
FastCheck.property(Schema.toArbitrary(ConfigV1.Info), (info) => { FastCheck.property(Schema.toArbitrary(ConfigV1.Info), (info) => {
Schema.decodeUnknownSync(Config.Info)(ConfigMigrateV1.migrate(info), { errors: "all" }) const parsed = Schema.decodeUnknownSync(ConfigV1.Info)(
Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(
Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(info),
),
)
Schema.decodeUnknownSync(Config.Info)(ConfigMigrateV1.migrate(parsed), { errors: "all" })
}), }),
{ numRuns: 100 }, { numRuns: 100 },
) )
@ -165,12 +178,9 @@ describe("Config", () => {
}, },
}) })
expect(migrated.providers?.bedrock?.api).toEqual({ expect(migrated.providers?.bedrock).toMatchObject({
type: "aisdk", package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"),
package: "@ai-sdk/amazon-bedrock",
settings: { region: "us-east-1", profile: "dev" }, settings: { region: "us-east-1", profile: "dev" },
})
expect(migrated.providers?.bedrock?.request).toEqual({
headers: { "x-test": "1" }, headers: { "x-test": "1" },
body: { trace: true }, body: { trace: true },
}) })
@ -197,8 +207,7 @@ describe("Config", () => {
template: "Review changes", template: "Review changes",
description: "Review code", description: "Review code",
agent: "reviewer", agent: "reviewer",
model: "anthropic/claude", model: { providerID: "anthropic", model: "claude", variant: "high" },
variant: "high",
subtask: true, subtask: true,
}, },
}) })
@ -431,8 +440,7 @@ describe("Config", () => {
], ],
agents: { agents: {
reviewer: { reviewer: {
model: "openrouter/openai/gpt-5", model: "openrouter/openai/gpt-5#high",
variant: "high",
request: { request: {
headers: { "x-agent": "reviewer" }, headers: { "x-agent": "reviewer" },
body: { reasoningEffort: "high" }, body: { reasoningEffort: "high" },
@ -505,7 +513,7 @@ describe("Config", () => {
expect(documents).toHaveLength(1) expect(documents).toHaveLength(1)
expect(documents[0]?.info.shell).toBe("/bin/bash") expect(documents[0]?.info.shell).toBe("/bin/bash")
expect(documents[0]?.info.model).toBe("anthropic/claude") expect(documents[0]?.info.model).toEqual(selection("anthropic/claude"))
expect(documents[0]?.info.default_agent).toBe("reviewer") expect(documents[0]?.info.default_agent).toBe("reviewer")
expect(documents[0]?.info.autoupdate).toBe("notify") expect(documents[0]?.info.autoupdate).toBe("notify")
expect(documents[0]?.info.share).toBe("disabled") expect(documents[0]?.info.share).toBe("disabled")
@ -516,8 +524,7 @@ describe("Config", () => {
{ action: "bash", resource: "git status", effect: "allow" }, { action: "bash", resource: "git status", effect: "allow" },
]) ])
const reviewer = documents[0]?.info.agents?.reviewer const reviewer = documents[0]?.info.agents?.reviewer
expect(reviewer?.model).toBe("openrouter/openai/gpt-5") expect(reviewer?.model).toEqual(selection("openrouter/openai/gpt-5#high"))
expect(reviewer?.variant).toBe("high")
expect(reviewer?.request).toEqual({ expect(reviewer?.request).toEqual({
headers: { "x-agent": "reviewer" }, headers: { "x-agent": "reviewer" },
body: { reasoningEffort: "high" }, body: { reasoningEffort: "high" },
@ -748,34 +755,32 @@ describe("Config", () => {
}) })
expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } }) expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
expect(documents[0]?.info.providers?.custom).toMatchObject({ expect(documents[0]?.info.providers?.custom).toMatchObject({
request: { body: { apiKey: "secret" } }, settings: { apiKey: "secret" },
models: { models: {
model: { model: {
request: { body: { reasoningEffort: "high" } }, settings: { reasoningEffort: "high" },
variants: [{ id: "fast", body: { temperature: 0.2 } }], variants: [{ id: "fast", settings: { temperature: 0.2 } }],
}, },
}, },
}) })
expect(documents[0]?.info.providers?.openai).toMatchObject({ expect(documents[0]?.info.providers?.openai).toMatchObject({
api: { settings: {} }, package: ProviderV2.aisdk("@ai-sdk/openai"),
request: { headers: { Authorization: "Bearer secret", "OpenAI-Organization": "org" } }, settings: { apiKey: "secret", organization: "org" },
models: { models: {
model: { model: {
request: { settings: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" },
body: { temperature: 0.3, reasoning: { effort: "high" }, service_tier: "priority" }, variants: [{ id: "high", settings: { reasoningEffort: "high", reasoningSummary: "auto" } }],
},
variants: [{ id: "high", body: { reasoning: { effort: "high", summary: "auto" } } }],
}, },
}, },
}) })
expect(documents[0]?.info.providers?.anthropic).toMatchObject({ expect(documents[0]?.info.providers?.anthropic).toMatchObject({
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
models: { models: {
model: { model: {
request: { settings: {
body: { effort: "high",
output_config: { effort: "high", task_budget: 4096 }, taskBudget: 4096,
metadata: { user_id: "user-1" }, metadata: { userId: "user-1" },
},
}, },
}, },
}, },

View file

@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test"
import { ConfigModel } from "@opencode-ai/core/config/model"
import { Model } from "@opencode-ai/schema/model"
import { Provider } from "@opencode-ai/schema/provider"
import { Schema } from "effect"
const decode = Schema.decodeUnknownSync(ConfigModel.Selection)
describe("ConfigModel.Selection", () => {
test("normalizes short and explicit model selections", () => {
expect(decode("openrouter/openai/gpt-5#high")).toEqual({
providerID: Provider.ID.make("openrouter"),
model: Model.ID.make("openai/gpt-5"),
variant: Model.VariantID.make("high"),
})
expect(decode({ providerID: "anthropic", model: "claude-sonnet", variant: "high" })).toEqual({
providerID: Provider.ID.make("anthropic"),
model: Model.ID.make("claude-sonnet"),
variant: Model.VariantID.make("high"),
})
})
test("rejects malformed selections and reserved fragments", () => {
expect(() => decode("gpt-5")).toThrow()
expect(() => decode("openai/gpt-5#")).toThrow()
expect(() => decode({ providerID: "openai", model: "gpt-5#high" })).toThrow()
})
})

View file

@ -2,223 +2,47 @@ import { describe, expect, test } from "bun:test"
import { ConfigProviderOptionsV1 } from "@opencode-ai/core/v1/config/provider-options" import { ConfigProviderOptionsV1 } from "@opencode-ai/core/v1/config/provider-options"
describe("ConfigProviderOptionsV1", () => { describe("ConfigProviderOptionsV1", () => {
test("keeps raw provider and request options unchanged", () => { test("splits provider overlays without changing package settings", () => {
const lowerer = ConfigProviderOptionsV1.get("custom-provider")
expect(lowerer.provider({ apiKey: "secret", headers: { "x-test": "1" }, nested: { camelCase: true } })).toEqual({
body: { apiKey: "secret", headers: { "x-test": "1" }, nested: { camelCase: true } },
})
expect(lowerer.request({ nested: { camelCase: true } })).toEqual({ nested: { camelCase: true } })
})
test("falls back to raw lowering for prototype property package names", () => {
expect(ConfigProviderOptionsV1.get("toString").provider({ enabled: true })).toEqual({ body: { enabled: true } })
})
test("lowers OpenAI provider and request options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/openai")
expect( expect(
lowerer.provider({ ConfigProviderOptionsV1.provider({
apiKey: "secret", apiKey: "secret",
baseURL: "https://openai.example/v1", baseURL: "https://openai.example/v1",
organization: "org", organization: "org",
project: "project", headers: { "x-test": "1", invalid: true },
headers: { "x-test": "1" },
body: { store: true }, body: { store: true },
timeout: 1000, nested: { camelCase: true },
}), }),
).toEqual({ ).toEqual({
url: "https://openai.example/v1", settings: {
headers: { apiKey: "secret",
Authorization: "Bearer secret", baseURL: "https://openai.example/v1",
"OpenAI-Organization": "org", organization: "org",
"OpenAI-Project": "project", nested: { camelCase: true },
"x-test": "1",
}, },
headers: { "x-test": "1" },
body: { store: true }, body: { store: true },
settings: { timeout: 1000 },
}) })
})
test("keeps model and variant options unchanged", () => {
expect( expect(
lowerer.request({ ConfigProviderOptionsV1.model({
reasoningEffort: "high", reasoningEffort: "high",
reasoningSummary: "auto",
reasoning: { encryptedContent: true },
textVerbosity: "low",
text: { outputFormat: "plain" },
nestedValue: { camelCase: true },
}),
).toEqual({
reasoning: { encrypted_content: true, effort: "high", summary: "auto" },
text: { output_format: "plain", verbosity: "low" },
nested_value: { camel_case: true },
})
})
test("lowers Anthropic provider and request options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/anthropic")
expect(
lowerer.provider({
apiKey: "secret",
authToken: "token",
baseURL: "https://anthropic.example",
headers: { "x-test": "1" },
body: { beta: true },
generateId: "custom",
}),
).toEqual({
url: "https://anthropic.example",
headers: { "x-api-key": "secret", Authorization: "Bearer token", "x-test": "1" },
body: { beta: true },
settings: { generateId: "custom" },
})
expect(
lowerer.request({
effort: "high",
taskBudget: 1024, taskBudget: 1024,
metadata: { userId: "user", traceId: "trace" }, metadata: { userId: "user" },
nestedValue: { camelCase: true },
}), }),
).toEqual({ ).toEqual({
output_config: { effort: "high", task_budget: 1024 }, reasoningEffort: "high",
metadata: { user_id: "user", trace_id: "trace" }, taskBudget: 1024,
nested_value: { camel_case: true }, metadata: { userId: "user" },
}) })
}) })
test("lowers Google provider and request options", () => { test("uses mechanical lowering for custom provider options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/google") expect(ConfigProviderOptionsV1.provider({ enabled: true })).toEqual({
settings: { enabled: true },
expect(
lowerer.provider({
apiKey: "secret",
baseURL: "https://google.example",
headers: { "x-test": "1" },
body: { trace: true },
project: "project",
}),
).toEqual({
url: "https://google.example",
headers: { "x-goog-api-key": "secret", "x-test": "1" },
body: { trace: true },
settings: { project: "project" },
})
expect(
lowerer.request({
thinkingConfig: { thinkingBudget: 1024 },
responseModalities: ["TEXT"],
mediaResolution: "high",
imageConfig: { aspectRatio: "16:9" },
safetySettings: ["safe"],
}),
).toEqual({
safetySettings: ["safe"],
generationConfig: {
thinkingConfig: { thinkingBudget: 1024 },
responseModalities: ["TEXT"],
mediaResolution: "high",
imageConfig: { aspectRatio: "16:9" },
},
})
})
test("lowers Azure provider options and uses OpenAI request lowering", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/azure")
expect(
lowerer.provider({
apiKey: "secret",
baseURL: "https://azure.example",
headers: { "x-test": "1" },
body: { trace: true },
resourceName: "resource",
}),
).toEqual({
url: "https://azure.example",
headers: { "api-key": "secret", "x-test": "1" },
body: { trace: true },
settings: { resourceName: "resource" },
})
expect(lowerer.request({ reasoningEffort: "high", reasoningSummary: "auto", textVerbosity: "low" })).toEqual({
reasoning: { effort: "high", summary: "auto" },
text: { verbosity: "low" },
})
})
test("lowers Amazon Bedrock provider and request options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/amazon-bedrock")
expect(
lowerer.provider({
headers: { "x-test": "1" },
body: { trace: true },
region: "us-east-1",
profile: "dev",
}),
).toEqual({
headers: { "x-test": "1" },
body: { trace: true },
settings: { region: "us-east-1", profile: "dev" },
})
expect(lowerer.request({ temperature: 0.2 })).toEqual({
additionalModelRequestFields: { temperature: 0.2 },
})
})
test("lowers OpenAI-compatible provider and request options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/openai-compatible")
expect(
lowerer.provider({
baseURL: "https://compatible.example/v1",
headers: { "x-test": "1" },
body: { trace: true },
apiKey: "secret",
}),
).toEqual({
url: "https://compatible.example/v1",
headers: { "x-test": "1" },
body: { trace: true },
settings: { apiKey: "secret" },
})
expect(lowerer.request({ reasoningEffort: "high", serviceTier: "priority" })).toEqual({
reasoning_effort: "high",
serviceTier: "priority",
})
})
test.each([
"@ai-sdk/cerebras",
"@ai-sdk/deepinfra",
"@ai-sdk/groq",
"@ai-sdk/mistral",
"@ai-sdk/togetherai",
"@ai-sdk/xai",
"@openrouter/ai-sdk-provider",
"ai-gateway-provider",
"venice-ai-sdk-provider",
])("uses OpenAI-compatible lowering for %s", (packageName) => {
const lowerer = ConfigProviderOptionsV1.get(packageName)
expect(lowerer.provider({ baseURL: "https://example.test", apiKey: "secret" })).toEqual({
url: "https://example.test",
headers: undefined, headers: undefined,
body: undefined, body: undefined,
settings: { apiKey: "secret" },
}) })
expect(lowerer.request({ reasoningEffort: "high" })).toEqual({ reasoning_effort: "high" })
}) })
test.each(["@ai-sdk/google-vertex", "@ai-sdk/google-vertex/anthropic"])(
"uses provider family lowering for %s",
(packageName) => {
const lowerer = ConfigProviderOptionsV1.get(packageName)
expect(lowerer.provider({ baseURL: "https://example.test", profile: "dev" })).toMatchObject({
url: "https://example.test",
settings: { profile: "dev" },
})
},
)
}) })

View file

@ -45,13 +45,6 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
) )
} }
function request(headers: Record<string, string>, variant?: string) {
return {
headers,
variant,
}
}
const decode = Schema.decodeUnknownSync(Config.Info) const decode = Schema.decodeUnknownSync(Config.Info)
describe("ConfigProviderPlugin.Plugin", () => { describe("ConfigProviderPlugin.Plugin", () => {
@ -68,7 +61,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({ info: decode({
providers: { providers: {
opencode: { opencode: {
api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" }, package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://opencode.test/v1" },
models: { models: {
"alpha-gpt-next": { "alpha-gpt-next": {
variants: [ variants: [
@ -119,7 +113,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({ info: decode({
providers: { providers: {
opencode: { opencode: {
api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" }, package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://opencode.test/v1" },
}, },
}, },
}), }),
@ -144,7 +139,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* addPlugin(config) yield* addPlugin(config)
const model = required(yield* catalog.model.get(providerID, modelID)) const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants[0]).toMatchObject({ expect(model.variants?.[0]).toMatchObject({
id: "high", id: "high",
body: { reasoningEffort: "high" }, body: { reasoningEffort: "high" },
}) })
@ -169,8 +164,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
custom: { custom: {
name: "Configured", name: "Configured",
env: ["CUSTOM_API_KEY"], env: ["CUSTOM_API_KEY"],
api: { type: "native", settings: {} }, package: "native",
request: request({ first: "first", shared: "first" }), headers: { first: "first", shared: "first" },
models: { models: {
chat: { chat: {
name: "First", name: "First",
@ -178,7 +173,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
disabled: true, disabled: true,
limit: { context: 100, output: 50 }, limit: { context: 100, output: 50 },
cost: { input: 1, output: 2 }, cost: { input: 1, output: 2 },
request: request({ first: "first", shared: "first" }, "retained"), settings: { retained: true },
headers: { first: "first", shared: "first" },
variants: [ variants: [
{ {
id: "fast", id: "fast",
@ -197,17 +193,18 @@ describe("ConfigProviderPlugin.Plugin", () => {
model: "custom/default", model: "custom/default",
providers: { providers: {
custom: { custom: {
api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" }, package: "aisdk:custom-sdk",
request: request({ last: "last", shared: "last" }), settings: { baseURL: "https://example.test" },
headers: { last: "last", shared: "last" },
models: { models: {
default: { default: {
name: "Default", name: "Default",
}, },
chat: { chat: {
api: { id: "api-chat" }, modelID: "api-chat",
name: "Last", name: "Last",
limit: { output: 75 }, limit: { output: 75 },
request: request({ last: "last", shared: "last" }), headers: { last: "last", shared: "last" },
variants: [ variants: [
{ {
id: "fast", id: "fast",
@ -247,22 +244,24 @@ describe("ConfigProviderPlugin.Plugin", () => {
}) })
expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed") expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
expect(provider.disabled).toBeUndefined() expect(provider.disabled).toBeUndefined()
expect(provider.api).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" }) expect(provider.package).toBe("aisdk:custom-sdk")
expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(provider.settings).toEqual({ baseURL: "https://example.test" })
expect(model.api.id).toBe(ModelV2.ID.make("api-chat")) expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.id).toBe(modelID)
expect(model.modelID).toBe(ModelV2.ID.make("api-chat"))
expect(model.name).toBe("Last") expect(model.name).toBe("Last")
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
expect(model.enabled).toBe(false) expect(model.enabled).toBe(false)
expect(model.limit).toEqual({ context: 100, output: 75 }) expect(model.limit).toEqual({ context: 100, output: 75 })
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }]) expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
expect(model.request.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.settings).toEqual({ baseURL: "https://example.test", retained: true })
expect(model.request.variant).toBe("retained") expect(model.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants.map((variant) => variant.id)).toEqual([ expect(model.variants?.map((variant) => variant.id)).toEqual([
ModelV2.VariantID.make("fast"), ModelV2.VariantID.make("fast"),
ModelV2.VariantID.make("slow"), ModelV2.VariantID.make("slow"),
]) ])
expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.variants?.[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants[1]?.headers).toEqual({ slow: "slow" }) expect(model.variants?.[1]?.headers).toEqual({ slow: "slow" })
}), }),
), ),
) )

View file

@ -271,7 +271,7 @@ describe("LocationServiceMap", () => {
providers: { providers: {
unavailable: { unavailable: {
name: "Unavailable", name: "Unavailable",
api: { type: "native", settings: {} }, package: "test-provider",
models: { chat: { disabled: true } }, models: { chat: { disabled: true } },
}, },
}, },
@ -306,7 +306,7 @@ describe("LocationServiceMap", () => {
), ),
) )
it.live("preserves the selected catalog identity when the api model id differs", () => it.live("preserves the selected catalog identity when the package model id differs", () =>
Effect.acquireRelease( Effect.acquireRelease(
Effect.promise(() => tmpdir()), Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()), (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
@ -318,12 +318,12 @@ describe("LocationServiceMap", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((editor) => { yield* catalog.transform((editor) => {
editor.provider.update(ProviderV2.ID.make("aliased"), (provider) => { editor.provider.update(ProviderV2.ID.make("aliased"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/openai", settings: {} } provider.package = ProviderV2.aisdk("@ai-sdk/openai")
}) })
editor.model.update(ProviderV2.ID.make("aliased"), ModelV2.ID.make("fast"), (model) => { editor.model.update(ProviderV2.ID.make("aliased"), ModelV2.ID.make("fast"), (model) => {
// Catalog id and provider API id intentionally differ, like gpt-5.5-fast -> gpt-5.5. // Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5.
model.api = { ...model.api, id: ModelV2.ID.make("base") } model.modelID = ModelV2.ID.make("base")
model.variants.push({ id: ModelV2.VariantID.make("high"), settings: {}, headers: {}, body: {} }) model.variants = [{ id: ModelV2.VariantID.make("high") }]
}) })
}) })
const models = yield* SessionRunnerModel.Service const models = yield* SessionRunnerModel.Service

View file

@ -1,4 +1,5 @@
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Effect } from "effect" import { Effect } from "effect"
export default define({ export default define({
@ -7,14 +8,11 @@ export default define({
ctx.catalog ctx.catalog
.transform((catalog) => { .transform((catalog) => {
catalog.provider.update("configured", (provider) => { catalog.provider.update("configured", (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" } provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
}) })
catalog.model.update("configured", "glm-5.2", (model) => { catalog.model.update("configured", "glm-5.2", (model) => {
model.api = { model.modelID = "glm-5.2"
id: "glm-5.2", model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk",
package: "@ai-sdk/openai-compatible",
}
model.variants = [ model.variants = [
{ {
id: "high", id: "high",

View file

@ -162,7 +162,7 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
id: ModelV2.ID.make(current.id), id: ModelV2.ID.make(current.id),
providerID: ProviderV2.ID.make(current.providerID), providerID: ProviderV2.ID.make(current.providerID),
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family), family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
variants: current.variants.map((variant) => ({ variants: current.variants?.map((variant) => ({
...variant, ...variant,
id: ModelV2.VariantID.make(variant.id), id: ModelV2.VariantID.make(variant.id),
})), })),
@ -327,35 +327,28 @@ function agentInfo(value: AgentV2.Info) {
function providerInfo(value: ProviderV2.MutableInfo) { function providerInfo(value: ProviderV2.MutableInfo) {
return { return {
...value, ...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } }, settings: value.settings && { ...value.settings },
request: { headers: value.headers && { ...value.headers },
settings: { ...value.request.settings }, body: value.body && { ...value.body },
headers: { ...value.request.headers },
body: { ...value.request.body },
},
} }
} }
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) { function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
return { return {
...value, ...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } }, settings: value.settings && { ...value.settings },
headers: value.headers && { ...value.headers },
body: value.body && { ...value.body },
capabilities: { capabilities: {
...value.capabilities, ...value.capabilities,
input: [...value.capabilities.input], input: [...value.capabilities.input],
output: [...value.capabilities.output], output: [...value.capabilities.output],
}, },
request: { variants: value.variants?.map((variant) => ({
...value.request,
settings: { ...value.request.settings },
headers: { ...value.request.headers },
body: { ...value.request.body },
},
variants: value.variants.map((variant) => ({
...variant, ...variant,
settings: { ...variant.settings }, settings: variant.settings && { ...variant.settings },
headers: { ...variant.headers }, headers: variant.headers && { ...variant.headers },
body: { ...variant.body }, body: variant.body && { ...variant.body },
})), })),
time: { ...value.time }, time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })), cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),

View file

@ -94,16 +94,16 @@ describe("ModelsDevPlugin", () => {
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast")) const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
expect(base?.variants).toEqual([]) expect(base?.variants).toEqual([])
expect(base?.request.body).toEqual({}) expect(base?.body).toEqual({})
expect(fast).toMatchObject({ expect(fast).toMatchObject({
id: "gpt-5.4-fast", id: "gpt-5.4-fast",
modelID: "gpt-5.4",
providerID: "acme", providerID: "acme",
name: "GPT-5.4 Fast", name: "GPT-5.4 Fast",
api: { id: "gpt-5.4" }, package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
request: { settings: { baseURL: "https://api.acme.test/v1" },
headers: { "x-mode": "fast" }, headers: { "x-mode": "fast" },
body: { service_tier: "priority" }, body: { service_tier: "priority" },
},
variants: [], variants: [],
}) })
expect(fast?.cost).toEqual([ expect(fast?.cost).toEqual([
@ -191,7 +191,7 @@ describe("ModelsDevPlugin", () => {
) )
const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning")) const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning"))
expect(model?.variants.map((variant) => variant.id)).toEqual([ expect(model?.variants?.map((variant) => variant.id)).toEqual([
ModelV2.VariantID.make("low"), ModelV2.VariantID.make("low"),
ModelV2.VariantID.make("high"), ModelV2.VariantID.make("high"),
]) ])
@ -202,8 +202,6 @@ describe("ModelsDevPlugin", () => {
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: ["reasoning.encrypted_content"],
}, },
headers: {},
body: {},
}) })
expect(model?.variants).toContainEqual({ expect(model?.variants).toContainEqual({
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
@ -212,20 +210,16 @@ describe("ModelsDevPlugin", () => {
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: ["reasoning.encrypted_content"],
}, },
headers: {},
body: {},
}) })
const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high")) const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high"))
expect(mode).toMatchObject({ expect(mode).toMatchObject({
id: "gpt-reasoning-high", id: "gpt-reasoning-high",
name: "GPT Reasoning High", name: "GPT Reasoning High",
request: { headers: { "x-mode": "high" },
headers: { "x-mode": "high" }, body: { service_tier: "priority" },
body: { service_tier: "priority" },
},
}) })
expect(mode?.variants.map((variant) => variant.id)).toEqual([ expect(mode?.variants?.map((variant) => variant.id)).toEqual([
ModelV2.VariantID.make("low"), ModelV2.VariantID.make("low"),
ModelV2.VariantID.make("high"), ModelV2.VariantID.make("high"),
]) ])
@ -234,22 +228,19 @@ describe("ModelsDevPlugin", () => {
expect(budgetModel?.variants).toContainEqual({ expect(budgetModel?.variants).toContainEqual({
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
settings: { thinking: { type: "enabled", budgetTokens: 16000 } }, settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
headers: {},
body: {},
}) })
expect(budgetModel?.variants).toContainEqual({ expect(budgetModel?.variants).toContainEqual({
id: ModelV2.VariantID.make("max"), id: ModelV2.VariantID.make("max"),
settings: { thinking: { type: "enabled", budgetTokens: 64000 } }, settings: { thinking: { type: "enabled", budgetTokens: 64000 } },
headers: {},
body: {},
}) })
const anthropicEffortModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-effort")) const anthropicEffortModel = yield* catalog.model.get(
ProviderV2.ID.anthropic,
ModelV2.ID.make("claude-effort"),
)
expect(anthropicEffortModel?.variants).toContainEqual({ expect(anthropicEffortModel?.variants).toContainEqual({
id: ModelV2.VariantID.make("low"), id: ModelV2.VariantID.make("low"),
settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
headers: {},
body: {},
}) })
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))), }).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
(previous) => (previous) =>

View file

@ -28,7 +28,8 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/alibaba", package: "@ai-sdk/alibaba",
options: { name: "alibaba" }, options: { name: "alibaba" },
@ -45,7 +46,8 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "alibaba" }, options: { name: "alibaba" },
@ -62,7 +64,8 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/alibaba", package: "@ai-sdk/alibaba",
options: { name: "custom-alibaba", apiKey: "test" }, options: { name: "custom-alibaba", apiKey: "test" },
@ -74,17 +77,18 @@ describe("AlibabaPlugin", () => {
}), }),
) )
it.effect("uses the old default languageModel(api.id) behavior", () => it.effect("uses the default languageModel(modelID) behavior", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const item = ModelV2.Info.make({ const item = ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("qwen-plus"),
package: "aisdk:test-provider",
}) })
const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} }) const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
const language = result.sdk?.languageModel(item.api.id) const language = result.sdk?.languageModel(item.modelID ?? item.id)
expect(language?.modelId).toBe("qwen-plus") expect(language?.modelId).toBe("qwen-plus")
expect(language?.provider).toBe("alibaba.chat") expect(language?.provider).toBe("alibaba.chat")
}), }),

View file

@ -79,31 +79,24 @@ function openAIUrl(language: unknown, path: string, modelId: string) {
} }
describe("AmazonBedrockPlugin", () => { describe("AmazonBedrockPlugin", () => {
it.effect("moves endpoint option to api URL", () => it.effect("moves endpoint setting to baseURL", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const bedrock = ProviderV2.Info.make({ const bedrock = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock), ...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" }, package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"),
request: { settings: { endpoint: "https://bedrock.example" },
headers: {},
body: { endpoint: "https://bedrock.example" },
},
}) })
catalog.provider.update(bedrock.id, (item) => { catalog.provider.update(bedrock.id, (item) => {
item.api = bedrock.api item.package = bedrock.package
item.request = { settings: {}, headers: {}, body: { endpoint: "https://bedrock.example" } } item.settings = { endpoint: "https://bedrock.example" }
}) })
}) })
yield* addPlugin() yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock)) const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
expect(result.api).toEqual({ expect(result.package).toBe(ProviderV2.aisdk("@ai-sdk/amazon-bedrock"))
type: "aisdk", expect(result.settings).toEqual({ baseURL: "https://bedrock.example" })
package: "@ai-sdk/amazon-bedrock",
url: "https://bedrock.example",
})
expect(result.request.body.endpoint).toBeUndefined()
}), }),
) )
@ -116,7 +109,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { options: {
@ -141,7 +135,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { options: {
@ -175,11 +170,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"),
type: "aisdk",
package: "test-provider",
},
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" }, options: { name: "amazon-bedrock" },
@ -199,7 +191,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock", region: "eu-west-1" }, options: { name: "amazon-bedrock", region: "eu-west-1" },
@ -218,7 +211,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" }, options: { name: "amazon-bedrock" },
@ -237,7 +231,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" }, options: { name: "amazon-bedrock" },
@ -257,7 +252,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { options: {
@ -286,7 +282,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { options: {
@ -314,11 +311,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
api: { modelID: ModelV2.ID.make("openai.gpt-5.5"),
id: ModelV2.ID.make("openai.gpt-5.5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
}), }),
package: "@ai-sdk/amazon-bedrock/mantle", package: "@ai-sdk/amazon-bedrock/mantle",
options: { options: {
@ -345,11 +339,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
api: { modelID: ModelV2.ID.make("openai.gpt-5.5"),
id: ModelV2.ID.make("openai.gpt-5.5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" }, options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
@ -357,11 +348,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
api: { modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: { region: "us-east-1" }, options: { region: "us-east-1" },
@ -378,11 +366,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/anthropic",
},
}), }),
package: "@ai-sdk/amazon-bedrock/anthropic", package: "@ai-sdk/amazon-bedrock/anthropic",
options: { name: "amazon-bedrock" }, options: { name: "amazon-bedrock" },
@ -409,11 +394,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"),
type: "aisdk",
package: "test-provider",
},
}), }),
package: "@ai-sdk/amazon-bedrock", package: "@ai-sdk/amazon-bedrock",
options: { options: {
@ -444,7 +426,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {}, options: {},
@ -452,7 +435,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" }, options: { region: "eu-west-1" },
@ -460,11 +444,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
api: { modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"),
type: "aisdk",
package: "test-provider",
},
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" }, options: { region: "eu-west-1" },
@ -472,7 +453,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-northeast-1" }, options: { region: "ap-northeast-1" },
@ -480,7 +462,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-southeast-2" }, options: { region: "ap-southeast-2" },
@ -505,7 +488,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {}, options: {},
@ -591,7 +575,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make(item.modelID),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: item.region }, options: { region: item.region },
@ -610,7 +595,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" }, options: { region: "eu-west-1" },

View file

@ -31,19 +31,19 @@ describe("AnthropicPlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({ const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.anthropic), ...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
api: { type: "aisdk", package: "@ai-sdk/anthropic" }, package: ProviderV2.aisdk("@ai-sdk/anthropic"),
request: { headers: { Existing: "1" }, body: {} }, headers: { Existing: "1" },
}) })
catalog.provider.update(item.id, (draft) => { catalog.provider.update(item.id, (draft) => {
draft.api = item.api draft.package = item.package
draft.request = { settings: {}, headers: { Existing: "1" }, body: {} } draft.headers = { Existing: "1" }
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe( expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.["anthropic-beta"]).toBe(
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
) )
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1") expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.Existing).toBe("1")
}), }),
) )
@ -52,9 +52,7 @@ describe("AnthropicPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {})) yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {}))
yield* addPlugin() yield* addPlugin()
expect( expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined()
required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
).toBeUndefined()
}), }),
) )
@ -66,7 +64,8 @@ describe("AnthropicPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" }, modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
}), }),
package: "@ai-sdk/anthropic", package: "@ai-sdk/anthropic",
options: { name: "custom-anthropic", apiKey: "test" }, options: { name: "custom-anthropic", apiKey: "test" },
@ -83,7 +82,8 @@ describe("AnthropicPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" }, modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
}), }),
package: "@ai-sdk/anthropic", package: "@ai-sdk/anthropic",
options: { name: "anthropic", apiKey: "test" }, options: { name: "anthropic", apiKey: "test" },

View file

@ -66,18 +66,16 @@ describe("AzureCognitiveServicesPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => { catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" } item.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
}) })
}) })
yield* addPlugin() yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))) const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
expect(result.api).toEqual({ expect(result).toMatchObject({
type: "aisdk", package: "aisdk:@ai-sdk/openai-compatible",
package: "@ai-sdk/openai-compatible", settings: { baseURL: "https://cognitive.cognitiveservices.azure.com/openai" },
url: "https://cognitive.cognitiveservices.azure.com/openai",
}) })
expect(result.request.body.baseURL).toBeUndefined() expect(result.settings?.resourceName).toBeUndefined()
expect(result.request.body.resourceName).toBeUndefined()
}), }),
), ),
) )
@ -89,26 +87,28 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({ const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")), ...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")),
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" }, package: "aisdk:@ai-sdk/openai-compatible",
}) })
const openai = ProviderV2.Info.make({ const openai = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai), ...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "test-provider" }, package: "aisdk:test-provider",
}) })
catalog.provider.update(azure.id, (item) => { catalog.provider.update(azure.id, (item) => {
item.api = azure.api item.package = azure.package
item.package = azure.package
}) })
catalog.provider.update(openai.id, (item) => { catalog.provider.update(openai.id, (item) => {
item.api = openai.api item.package = openai.package
item.package = openai.package
}) })
}) })
yield* addPlugin() yield* addPlugin()
const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))) const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai)) const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
expect(azure.request.body.baseURL).toBeUndefined() expect(azure.settings?.baseURL).toBeUndefined()
expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" }) expect(azure).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible" })
expect(openai.request.body.baseURL).toBeUndefined() expect(openai.settings?.baseURL).toBeUndefined()
expect(openai.api).toEqual({ type: "aisdk", package: "test-provider" }) expect(openai).toMatchObject({ package: "aisdk:test-provider" })
}), }),
), ),
) )
@ -122,7 +122,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true }, options: { useCompletionUrls: true },
@ -140,7 +141,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -148,7 +150,8 @@ describe("AzureCognitiveServicesPlugin", () => {
const ignored = yield* aisdk.runLanguage({ const ignored = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -168,7 +171,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("messages-deployment"),
package: "aisdk:test-provider",
}), }),
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel }, sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
options: {}, options: {},
@ -176,7 +180,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("chat-deployment"),
package: "aisdk:test-provider",
}), }),
sdk: { chat: sdk.chat, languageModel: sdk.languageModel }, sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
options: {}, options: {},
@ -184,7 +189,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")), ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("language-deployment"),
package: "aisdk:test-provider",
}), }),
sdk: { languageModel: sdk.languageModel }, sdk: { languageModel: sdk.languageModel },
options: {}, options: {},

View file

@ -66,11 +66,11 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => { catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/azure" } item.package = ProviderV2.aisdk("@ai-sdk/azure")
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
}), }),
), ),
) )
@ -82,18 +82,18 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({ const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure), ...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" }, package: ProviderV2.aisdk("@ai-sdk/azure"),
request: { headers: {}, body: { resourceName: "from-config" } }, settings: { resourceName: "from-config" },
}) })
catalog.provider.update(azure.id, (item) => { catalog.provider.update(azure.id, (item) => {
item.api = azure.api item.package = azure.package
item.request = { settings: {}, headers: {}, body: { resourceName: "from-config" } } item.settings = { resourceName: "from-config" }
}) })
catalog.provider.update(ProviderV2.ID.openai, () => {}) catalog.provider.update(ProviderV2.ID.openai, () => {})
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config") expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-config")
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined() expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.resourceName).toBeUndefined()
}), }),
), ),
) )
@ -105,16 +105,16 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({ const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure), ...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" }, package: ProviderV2.aisdk("@ai-sdk/azure"),
request: { headers: {}, body: { resourceName: "" } }, settings: { resourceName: "" },
}) })
catalog.provider.update(azure.id, (item) => { catalog.provider.update(azure.id, (item) => {
item.api = azure.api item.package = azure.package
item.request = { settings: {}, headers: {}, body: { resourceName: "" } } item.settings = { resourceName: "" }
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
}), }),
), ),
) )
@ -126,16 +126,16 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({ const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure), ...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" }, package: ProviderV2.aisdk("@ai-sdk/azure"),
request: { headers: {}, body: { resourceName: " " } }, settings: { resourceName: " " },
}) })
catalog.provider.update(azure.id, (item) => { catalog.provider.update(azure.id, (item) => {
item.api = azure.api item.package = azure.package
item.request = { settings: {}, headers: {}, body: { resourceName: " " } } item.settings = { resourceName: " " }
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env") expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
}), }),
), ),
) )
@ -149,7 +149,8 @@ describe("AzurePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/azure", package: "@ai-sdk/azure",
options: { name: "azure", baseURL: "https://proxy.example.com/openai" }, options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
@ -168,7 +169,8 @@ describe("AzurePlugin", () => {
.runSDK({ .runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/azure", package: "@ai-sdk/azure",
options: { name: "azure" }, options: { name: "azure" },
@ -188,7 +190,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true }, options: { useCompletionUrls: true },
@ -206,7 +209,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true }, options: { useCompletionUrls: true },
@ -224,8 +228,9 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
request: { headers: {}, body: { useCompletionUrls: true } }, package: ProviderV2.aisdk("test-provider"),
body: { useCompletionUrls: true },
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -243,7 +248,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -251,7 +257,8 @@ describe("AzurePlugin", () => {
const ignored = yield* aisdk.runLanguage({ const ignored = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("deployment"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -274,7 +281,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("messages-deployment"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") }, sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
options: {}, options: {},
@ -282,7 +290,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")), ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("language-deployment"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: { languageModel: make("languageModel") }, sdk: { languageModel: make("languageModel") },
options: {}, options: {},

View file

@ -36,12 +36,12 @@ describe("CerebrasPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => { catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" } item.package = ProviderV2.aisdk("@ai-sdk/cerebras")
item.request.headers.Existing = "1" item.headers = { ...item.headers, Existing: "1" }
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.headers).toEqual({
Existing: "1", Existing: "1",
"X-Cerebras-3rd-Party-Integration": "opencode", "X-Cerebras-3rd-Party-Integration": "opencode",
}) })
@ -53,7 +53,7 @@ describe("CerebrasPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {})) yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.request.headers).toEqual({}) expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.headers).toBeUndefined()
}), }),
) )
@ -69,11 +69,8 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"), ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
), ),
api: { modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider",
type: "aisdk",
package: "test-provider",
},
}), }),
package: "@ai-sdk/cerebras", package: "@ai-sdk/cerebras",
options: { name: "custom-cerebras", apiKey: "test" }, options: { name: "custom-cerebras", apiKey: "test" },
@ -95,11 +92,8 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"), ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
), ),
api: { modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider",
type: "aisdk",
package: "test-provider",
},
}), }),
package: "@ai-sdk/cerebras", package: "@ai-sdk/cerebras",
options: { name: "configured-cerebras", apiKey: "test" }, options: { name: "configured-cerebras", apiKey: "test" },
@ -120,11 +114,8 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"), ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
), ),
api: { modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider",
type: "aisdk",
package: "test-provider",
},
}), }),
package: "@ai-sdk/groq", package: "@ai-sdk/groq",
options: { name: "custom-cerebras", apiKey: "test" }, options: { name: "custom-cerebras", apiKey: "test" },

View file

@ -118,7 +118,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" }, options: { name: "cloudflare-ai-gateway" },
@ -139,7 +140,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { options: {
@ -183,7 +185,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { options: {
@ -212,7 +215,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { options: {
@ -249,7 +253,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { options: {
@ -280,7 +285,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" }, options: { name: "cloudflare-ai-gateway" },
@ -302,7 +308,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" }, options: { name: "cloudflare-ai-gateway" },
@ -325,7 +332,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" }, options: { name: "cloudflare-ai-gateway" },
@ -354,7 +362,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" }, options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
@ -380,11 +389,8 @@ describe("CloudflareAIGatewayPlugin", () => {
ProviderV2.ID.make("cloudflare-ai-gateway"), ProviderV2.ID.make("cloudflare-ai-gateway"),
ModelV2.ID.make("anthropic/claude-sonnet-4-5"), ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
), ),
api: { modelID: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"), package: "aisdk:test-provider",
type: "aisdk",
package: "test-provider",
},
}), }),
package: "ai-gateway-provider", package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" }, options: { name: "cloudflare-ai-gateway" },
@ -412,7 +418,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-ai-gateway" }, options: { name: "cloudflare-ai-gateway" },

View file

@ -87,7 +87,7 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider" } provider.package = ProviderV2.aisdk("test-provider")
}), }),
) )
yield* addPlugin() yield* addPlugin()
@ -95,15 +95,16 @@ describe("CloudflareWorkersAIPlugin", () => {
const sdk = yield* aisdk.runSDK({ const sdk = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: { id: ModelV2.ID.make("@cf/model"), ...provider.api }, modelID: ModelV2.ID.make("@cf/model"),
package: provider.package,
settings: provider.settings,
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } }, options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
}) })
expect(provider.api).toEqual({ expect(provider).toMatchObject({
type: "aisdk", package: "aisdk:test-provider",
package: "test-provider", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" },
url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1",
}) })
expect(sdk.sdk).toBeDefined() expect(sdk.sdk).toBeDefined()
}), }),
@ -116,14 +117,14 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" } provider.package = ProviderV2.aisdk("test-provider")
provider.settings = { ...provider.settings, baseURL: "https://proxy.example/v1" }
}), }),
) )
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({ expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
type: "aisdk", package: "aisdk:test-provider",
package: "test-provider", settings: { baseURL: "https://proxy.example/v1" },
url: "https://proxy.example/v1",
}) })
}), }),
), ),
@ -138,12 +139,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: { modelID: ModelV2.ID.make("@cf/model"),
id: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible",
type: "aisdk", settings: { baseURL: "https://proxy.example/v1" },
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" }, options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
@ -159,15 +157,14 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider" } provider.package = ProviderV2.aisdk("test-provider")
provider.request.body.accountId = "configured-acct" provider.settings = { ...provider.settings, accountId: "configured-acct" }
}), }),
) )
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({ expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
type: "aisdk", package: "aisdk:test-provider",
package: "test-provider", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" },
url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
}) })
}), }),
), ),
@ -182,12 +179,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: { modelID: ModelV2.ID.make("@cf/model"),
id: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible",
type: "aisdk", settings: { baseURL: "https://proxy.example/v1" },
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { options: {
@ -214,12 +208,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: { modelID: ModelV2.ID.make("@cf/model"),
id: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible",
type: "aisdk", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" },
package: "@ai-sdk/openai-compatible",
url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
},
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { options: {
@ -243,7 +234,8 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("@cf/api-model"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -262,12 +254,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: { modelID: ModelV2.ID.make("@cf/model"),
id: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/anthropic",
type: "aisdk", settings: { baseURL: "https://proxy.example/v1" },
package: "@ai-sdk/anthropic",
url: "https://proxy.example/v1",
},
}), }),
package: "@ai-sdk/anthropic", package: "@ai-sdk/anthropic",
options: { name: "cloudflare-workers-ai" }, options: { name: "cloudflare-workers-ai" },

View file

@ -56,7 +56,8 @@ describe("CoherePlugin", () => {
const ignored = yield* aisdk.runSDK({ const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("command"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "cohere" }, options: { name: "cohere" },
@ -66,7 +67,8 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("command"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/cohere", package: "@ai-sdk/cohere",
options: { name: "cohere" }, options: { name: "cohere" },
@ -83,7 +85,8 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("command-r-plus"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/cohere", package: "@ai-sdk/cohere",
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" }, options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
@ -108,7 +111,8 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("command-r-plus"),
package: "aisdk:test-provider",
}), }),
sdk, sdk,
options: {}, options: {},

View file

@ -48,7 +48,8 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}), }),
package: "@ai-sdk/deepinfra", package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" }, options: { name: "deepinfra" },
@ -66,7 +67,8 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}), }),
package: "@ai-sdk/deepinfra", package: "@ai-sdk/deepinfra",
options: { name: "custom-deepinfra", apiKey: "test" }, options: { name: "custom-deepinfra", apiKey: "test" },
@ -85,7 +87,8 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}), }),
package: "@ai-sdk/deepinfra", package: "@ai-sdk/deepinfra",
options: { name: "deepinfra", apiKey: "test" }, options: { name: "deepinfra", apiKey: "test" },
@ -111,7 +114,8 @@ describe("DeepInfraPlugin", () => {
const ignored = yield* aisdk.runSDK({ const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}), }),
package: item, package: item,
options: { name: "deepinfra" }, options: { name: "deepinfra" },
@ -122,7 +126,8 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
}), }),
package: "@ai-sdk/deepinfra", package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" }, options: { name: "deepinfra" },
@ -141,17 +146,14 @@ describe("DeepInfraPlugin", () => {
const sdkEvent = yield* aisdk.runSDK({ const sdkEvent = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")), ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")),
api: { modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), package: "aisdk:@ai-sdk/deepinfra",
type: "aisdk",
package: "@ai-sdk/deepinfra",
},
}), }),
package: "@ai-sdk/deepinfra", package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" }, options: { name: "deepinfra" },
}) })
const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options }) const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options })
const language = result.language ?? result.sdk.languageModel(result.model.api.id) const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
expect(language.provider).toBe("deepinfra.chat") expect(language.provider).toBe("deepinfra.chat")
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"]) expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
}), }),

View file

@ -54,7 +54,8 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, modelID: ModelV2.ID.make("test-model"),
package: ProviderV2.aisdk(fixtureProvider),
}), }),
package: fixtureProvider, package: fixtureProvider,
options: { name: "custom", marker: "dynamic" }, options: { name: "custom", marker: "dynamic" },
@ -72,7 +73,8 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, modelID: ModelV2.ID.make("test-model"),
package: ProviderV2.aisdk(fixtureProvider),
}), }),
package: fixtureProvider, package: fixtureProvider,
options: { name: "custom", marker: "dynamic" }, options: { name: "custom", marker: "dynamic" },
@ -89,7 +91,8 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, modelID: ModelV2.ID.make("test-model"),
package: ProviderV2.aisdk(fixtureProvider),
}), }),
package: fixtureProvider, package: fixtureProvider,
options: { name: "custom-provider", marker: "dynamic" }, options: { name: "custom-provider", marker: "dynamic" },
@ -105,7 +108,8 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), ...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" }, modelID: ModelV2.ID.make("test-model"),
package: "aisdk:fixture-provider",
}), }),
package: "fixture-provider", package: "fixture-provider",
options: { name: "npm-provider", marker: "npm" }, options: { name: "npm-provider", marker: "npm" },
@ -122,7 +126,8 @@ describe("DynamicProviderPlugin", () => {
.language( .language(
ModelV2.Info.make({ ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" }, modelID: ModelV2.ID.make("alias"),
package: "aisdk:fixture-provider",
}), }),
) )
.pipe(Effect.exit) .pipe(Effect.exit)
@ -139,7 +144,8 @@ describe("DynamicProviderPlugin", () => {
.language( .language(
ModelV2.Info.make({ ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "file:///missing/provider-factory.js" }, modelID: ModelV2.ID.make("alias"),
package: "aisdk:file:///missing/provider-factory.js",
}), }),
) )
.pipe(Effect.exit) .pipe(Effect.exit)
@ -158,7 +164,8 @@ describe("DynamicProviderPlugin", () => {
.language( .language(
ModelV2.Info.make({ ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" }, modelID: ModelV2.ID.make("alias"),
package: "aisdk:fixture-provider",
}), }),
) )
.pipe(Effect.exit) .pipe(Effect.exit)
@ -167,7 +174,7 @@ describe("DynamicProviderPlugin", () => {
}), }),
) )
itWithAISDK.effect("uses the model api.id for the default language model", () => itWithAISDK.effect("uses the model modelID for the default language model", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
@ -175,7 +182,8 @@ describe("DynamicProviderPlugin", () => {
const language = yield* aisdk.language( const language = yield* aisdk.language(
ModelV2.Info.make({ ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("test-model-api"), type: "aisdk", package: fixtureProvider }, modelID: ModelV2.ID.make("test-model-api"),
package: ProviderV2.aisdk(fixtureProvider),
}), }),
) )
expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } }) expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } })

View file

@ -45,7 +45,8 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/gateway", package: "@ai-sdk/gateway",
options: { name: "gateway" }, options: { name: "gateway" },
@ -65,11 +66,8 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")), ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")),
api: { modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"),
id: ModelV2.ID.make("anthropic/claude-sonnet-4"), package: "aisdk:test-provider",
type: "aisdk",
package: "test-provider",
},
}), }),
package: "@ai-sdk/gateway", package: "@ai-sdk/gateway",
options: { name: "vercel", apiKey: "test-key" }, options: { name: "vercel", apiKey: "test-key" },
@ -91,7 +89,8 @@ describe("GatewayPlugin", () => {
const ignored = yield* aisdk.runSDK({ const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make(modelID),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/vercel", package: "@ai-sdk/vercel",
options: { name: "vercel" }, options: { name: "vercel" },
@ -101,7 +100,8 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make(modelID),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/gateway", package: "@ai-sdk/gateway",
options: { name: "vercel" }, options: { name: "vercel" },

View file

@ -47,7 +47,8 @@ describe("GithubCopilotPlugin", () => {
const ignored = yield* aisdk.runSDK({ const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "github-copilot" }, options: { name: "github-copilot" },
@ -55,7 +56,8 @@ describe("GithubCopilotPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/github-copilot", package: "@ai-sdk/github-copilot",
options: { name: "github-copilot" }, options: { name: "github-copilot" },
@ -74,7 +76,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4"),
package: "aisdk:test-provider",
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {}, options: {},
@ -92,7 +95,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4"),
package: "aisdk:test-provider",
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {}, options: {},
@ -110,7 +114,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -118,7 +123,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5.1-codex"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -126,7 +132,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-4o"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -134,7 +141,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5-mini"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -142,7 +150,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -166,7 +175,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -174,7 +184,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5-mini"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -182,7 +193,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -230,7 +242,8 @@ describe("GithubCopilotPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},

View file

@ -70,7 +70,8 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
}), }),
package: "gitlab-ai-provider", package: "gitlab-ai-provider",
options: { name: "gitlab" }, options: { name: "gitlab" },
@ -107,7 +108,8 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
}), }),
package: "gitlab-ai-provider", package: "gitlab-ai-provider",
options: { name: "gitlab" }, options: { name: "gitlab" },
@ -132,7 +134,8 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
}), }),
package: "gitlab-ai-provider", package: "gitlab-ai-provider",
options: { options: {
@ -173,7 +176,8 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai", package: "@ai-sdk/openai",
options: { name: "gitlab" }, options: { name: "gitlab" },
@ -192,11 +196,10 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("duo-workflow-custom"),
request: { package: "aisdk:test-provider",
headers: {}, headers: {},
body: { workflowRef: "ref", workflowDefinition: "definition" }, settings: { workflowRef: "ref", workflowDefinition: "definition" },
},
}), }),
sdk: { sdk: {
workflowChat: (id: string, options: unknown) => { workflowChat: (id: string, options: unknown) => {
@ -227,7 +230,8 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("duo-workflow-exact"),
package: "aisdk:test-provider",
}), }),
sdk: { sdk: {
workflowChat: (id: string, options: unknown) => { workflowChat: (id: string, options: unknown) => {
@ -245,7 +249,7 @@ describe("GitLabPlugin", () => {
}), }),
) )
it.effect("uses provider feature flags instead of request feature flags", () => it.effect("uses provider feature flags instead of model settings feature flags", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
@ -254,11 +258,10 @@ describe("GitLabPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("duo-workflow-custom"),
request: { package: "aisdk:test-provider",
headers: {}, headers: {},
body: { featureFlags: { request_flag: true } }, settings: { featureFlags: { request_flag: true } },
},
}), }),
sdk: { sdk: {
workflowChat: (id: string, options: unknown) => { workflowChat: (id: string, options: unknown) => {
@ -282,8 +285,10 @@ describe("GitLabPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude"),
request: { headers: { h: "v" }, body: {} }, package: "aisdk:test-provider",
headers: { h: "v" },
settings: {},
}), }),
sdk: { sdk: {
workflowChat: () => undefined, workflowChat: () => undefined,

View file

@ -64,16 +64,16 @@ describe("GoogleVertexAnthropicPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" } provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
}), }),
) )
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
expect( expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project, "cloud-project",
).toBe("cloud-project") )
expect( expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location, "cloud-location",
).toBe("cloud-location") )
}), }),
), ),
) )
@ -84,18 +84,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" } provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
provider.request.body.project = "configured-project" provider.settings = { ...provider.settings, project: "configured-project", location: "configured-location" }
provider.request.body.location = "configured-location"
}), }),
) )
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project).toBe( expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
"configured-project", "configured-project",
) )
expect( expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location, "configured-location",
).toBe("configured-location") )
}), }),
), ),
) )
@ -121,7 +120,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
ProviderV2.ID.make("google-vertex-anthropic"), ProviderV2.ID.make("google-vertex-anthropic"),
ModelV2.ID.make("claude-sonnet-4-5"), ModelV2.ID.make("claude-sonnet-4-5"),
), ),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/google-vertex/anthropic", package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex-anthropic" }, options: { name: "google-vertex-anthropic" },
@ -147,7 +147,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
ProviderV2.ID.make("google-vertex-anthropic"), ProviderV2.ID.make("google-vertex-anthropic"),
ModelV2.ID.make("claude-sonnet-4-5"), ModelV2.ID.make("claude-sonnet-4-5"),
), ),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/google-vertex/anthropic", package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex-anthropic" }, options: { name: "google-vertex-anthropic" },
@ -167,7 +168,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/google-vertex/anthropic", package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "eu" }, options: { name: "google-vertex", project: "project", location: "eu" },
@ -186,7 +188,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/google-vertex/anthropic", package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" }, options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
@ -204,7 +207,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const sdkResult = yield* aisdk.runSDK({ const sdkResult = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/google-vertex/anthropic", package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "us" }, options: { name: "google-vertex", project: "project", location: "us" },
@ -212,7 +216,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const languageResult = yield* aisdk.runLanguage({ const languageResult = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
package: "aisdk:test-provider",
}), }),
sdk: sdkResult.sdk, sdk: sdkResult.sdk,
options: {}, options: {},
@ -234,7 +239,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
package: "aisdk:test-provider",
}), }),
sdk: { languageModel: selector(calls) }, sdk: { languageModel: selector(calls) },
options: {}, options: {},
@ -252,7 +258,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
}), }),
sdk: { languageModel: selector(calls) }, sdk: { languageModel: selector(calls) },
options: {}, options: {},

View file

@ -92,17 +92,14 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.opencode, (provider) => { catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" }
package: "@ai-sdk/openai-compatible",
url: "https://opencode.ai/zen/v1",
}
}), }),
) )
yield* addPlugin() yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode)) const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
expect(provider.request.body).toEqual({}) expect(provider.settings).toEqual({ baseURL: "https://opencode.ai/zen/v1" })
}), }),
) )
@ -121,21 +118,24 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = {
package: "@ai-sdk/openai-compatible", ...provider.settings,
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
} }
}), }),
) )
yield* addPlugin() yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("google-cloud-project") expect(provider.settings?.project).toBe("google-cloud-project")
expect(provider.request.body.location).toBe("google-vertex-location") expect(provider.settings?.location).toBe("google-vertex-location")
expect(provider.api).toEqual({ expect(provider).toMatchObject({
type: "aisdk", package: "aisdk:@ai-sdk/openai-compatible",
package: "@ai-sdk/openai-compatible", settings: {
url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location", baseURL:
"https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
},
}) })
}), }),
), ),
@ -160,10 +160,11 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = {
package: "@ai-sdk/openai-compatible", ...provider.settings,
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
} }
}), }),
) )
@ -172,21 +173,20 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: { modelID: ModelV2.ID.make("gemini"),
id: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex",
type: "aisdk",
package: "@ai-sdk/google-vertex",
},
}), }),
package: "@ai-sdk/google-vertex", package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" }, options: { name: "google-vertex" },
}) })
expect(provider.request.body.project).toBe("vertex-project") expect(provider.settings?.project).toBe("vertex-project")
expect(provider.api).toEqual({ expect(provider).toMatchObject({
type: "aisdk", package: "aisdk:@ai-sdk/openai-compatible",
package: "@ai-sdk/openai-compatible", settings: {
url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4", baseURL:
"https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
},
}) })
expect(vertexOptions[0].project).toBe("vertex-project") expect(vertexOptions[0].project).toBe("vertex-project")
expect(vertexOptions[0].location).toBe("europe-west4") expect(vertexOptions[0].location).toBe("europe-west4")
@ -209,23 +209,22 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = {
package: "@ai-sdk/openai-compatible", ...provider.settings,
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
} }
provider.request.body.project = "config-project" provider.settings = { ...provider.settings, project: "config-project", location: "global" }
provider.request.body.location = "global"
}), }),
) )
yield* addPlugin() yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("config-project") expect(provider.settings?.project).toBe("config-project")
expect(provider.request.body.location).toBe("global") expect(provider.settings?.location).toBe("global")
expect(provider.api).toEqual({ expect(provider).toMatchObject({
type: "aisdk", package: "aisdk:@ai-sdk/openai-compatible",
package: "@ai-sdk/openai-compatible", settings: { baseURL: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global" },
url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global",
}) })
}), }),
), ),
@ -236,21 +235,20 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = {
package: "@ai-sdk/openai-compatible", ...provider.settings,
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
} }
provider.request.body.project = "config-project" provider.settings = { ...provider.settings, project: "config-project", location: "eu" }
provider.request.body.location = "eu"
}), }),
) )
yield* addPlugin() yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.api).toEqual({ expect(provider).toMatchObject({
type: "aisdk", package: "aisdk:@ai-sdk/openai-compatible",
package: "@ai-sdk/openai-compatible", settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" },
url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu",
}) })
}), }),
) )
@ -270,14 +268,14 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" } provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex")
provider.request.body.project = "config-project" provider.settings = { ...provider.settings, project: "config-project" }
}), }),
) )
yield* addPlugin() yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("config-project") expect(provider.settings?.project).toBe("config-project")
expect(provider.request.body.location).toBe("us-central1") expect(provider.settings?.location).toBe("us-central1")
}), }),
), ),
) )
@ -297,11 +295,8 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: { modelID: ModelV2.ID.make("gemini"),
id: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex",
type: "aisdk",
package: "@ai-sdk/google-vertex",
},
}), }),
package: "@ai-sdk/google-vertex", package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" }, options: { name: "google-vertex" },
@ -345,11 +340,8 @@ describe("GoogleVertexPlugin", () => {
aisdk.runSDK({ aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: { modelID: ModelV2.ID.make("gemini"),
id: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/openai-compatible",
type: "aisdk",
package: "@ai-sdk/openai-compatible",
},
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "google-vertex" }, options: { name: "google-vertex" },
@ -359,11 +351,11 @@ describe("GoogleVertexPlugin", () => {
;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch ;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch
}), }),
) )
expect(fetchCalls).toHaveLength(1) const vertexCalls = fetchCalls.filter((call) => call.input === "https://vertex.example")
expect(vertexCalls).toHaveLength(1)
expect(googleAuthOptions).toEqual([{ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }]) expect(googleAuthOptions).toEqual([{ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }])
expect(fetchCalls[0].input).toBe("https://vertex.example") expect(new Headers(vertexCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token")
expect(new Headers(fetchCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token") expect(new Headers(vertexCalls[0].init?.headers).get("x-test")).toBe("1")
expect(new Headers(fetchCalls[0].init?.headers).get("x-test")).toBe("1")
}), }),
) )
@ -376,7 +368,8 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runLanguage({ yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make(" gemini-2.5-pro "),
package: "aisdk:test-provider",
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {}, options: {},

View file

@ -27,7 +27,8 @@ describe("GooglePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" }, modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/google",
}), }),
package: "@ai-sdk/google", package: "@ai-sdk/google",
options: { name: "custom-google", apiKey: "test" }, options: { name: "custom-google", apiKey: "test" },
@ -45,7 +46,8 @@ describe("GooglePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), ...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" }, modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/google",
}), }),
package: "@ai-sdk/google-vertex", package: "@ai-sdk/google-vertex",
options: { name: "google" }, options: { name: "google" },
@ -62,7 +64,8 @@ describe("GooglePlugin", () => {
const sdkEvent = yield* aisdk.runSDK({ const sdkEvent = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" }, modelID: ModelV2.ID.make("gemini-api"),
package: "aisdk:@ai-sdk/google",
}), }),
package: "@ai-sdk/google", package: "@ai-sdk/google",
options: { name: "custom-google", apiKey: "test" }, options: { name: "custom-google", apiKey: "test" },
@ -72,9 +75,29 @@ describe("GooglePlugin", () => {
sdk: sdkEvent.sdk, sdk: sdkEvent.sdk,
options: sdkEvent.options, options: sdkEvent.options,
}) })
const language = result.language ?? result.sdk.languageModel(result.model.api.id) const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
expect(language.modelId).toBe("gemini-api") expect(language.modelId).toBe("gemini-api")
expect(language.provider).toBe("custom-google") expect(language.provider).toBe("custom-google")
}), }),
) )
it.effect("wraps AI SDK language models for the native runner", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* addPlugin()
const resolved = yield* aisdk.model(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("gemini-api"),
package: "aisdk:@ai-sdk/google",
settings: { apiKey: "test" },
}),
)
expect(String(resolved.id)).toBe("gemini-api")
expect(String(resolved.provider)).toBe("custom-google")
expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/google")
}),
)
}) })

View file

@ -28,7 +28,8 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
}), }),
package: "@ai-sdk/groq", package: "@ai-sdk/groq",
options: { name: "groq" }, options: { name: "groq" },
@ -45,7 +46,8 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "groq" }, options: { name: "groq" },
@ -62,7 +64,8 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
}), }),
package: "@ai-sdk/groq/compat", package: "@ai-sdk/groq/compat",
options: { name: "groq" }, options: { name: "groq" },
@ -79,7 +82,8 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
}), }),
package: "@ai-sdk/groq", package: "@ai-sdk/groq",
options: { name: "custom-groq", apiKey: "test" }, options: { name: "custom-groq", apiKey: "test" },
@ -93,7 +97,7 @@ describe("GroqPlugin", () => {
}), }),
) )
it.effect("uses the default languageModel(api.id) behavior", () => it.effect("uses the default languageModel(modelID) behavior", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
@ -104,16 +108,13 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
api: { modelID: ModelV2.ID.make("llama-api"),
id: ModelV2.ID.make("llama-api"), package: "aisdk:@ai-sdk/groq",
type: "aisdk",
package: "@ai-sdk/groq",
},
}), }),
sdk, sdk,
options: { name: "groq", apiKey: "test" }, options: { name: "groq", apiKey: "test" },
}) })
const language = result.language ?? sdk.languageModel(result.model.api.id) const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
expect(language.modelId).toBe("llama-api") expect(language.modelId).toBe("llama-api")
expect(language.provider).toBe("groq.chat") expect(language.provider).toBe("groq.chat")
}), }),

View file

@ -22,27 +22,24 @@ describe("KiloPlugin", () => {
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.kilo")), Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.kilo")),
) )
it.effect("applies legacy referer headers only to kilo", () => it.effect("applies legacy referer headers only to Kilo endpoints", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
package: "@ai-sdk/openai-compatible", provider.headers = { Existing: "value" }
url: "https://api.kilo.ai/api/gateway",
}
provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
}) })
catalog.provider.update(ProviderV2.ID.openrouter, () => {}) catalog.provider.update(ProviderV2.ID.openrouter, () => {})
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
Existing: "value", Existing: "value",
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({}) expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
}), }),
) )
@ -51,49 +48,41 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty( expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("http-referer")
"http-referer", expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("x-title")
) expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("X-Source")
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("x-title")
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("X-Source")
}), }),
) )
it.effect("uses the legacy provider-id guard instead of endpoint package matching", () => it.effect("uses endpoint package matching instead of a provider ID guard", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("kilo")
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
}) })
catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => { catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
provider.api = { type: "aisdk", package: "kilo" } provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.request.headers).toEqual({})
}), }),
) )
}) })

View file

@ -34,23 +34,20 @@ describe("LLMGatewayPlugin", () => {
}) })
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => { catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
package: "@ai-sdk/openai-compatible", provider.headers = { Existing: "value" }
url: "https://api.llmgateway.io/v1",
}
provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
}) })
catalog.provider.update(ProviderV2.ID.openrouter, () => {}) catalog.provider.update(ProviderV2.ID.openrouter, () => {})
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toEqual({
Existing: "value", Existing: "value",
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
"X-Source": "opencode", "X-Source": "opencode",
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({}) expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
}), }),
) )
@ -64,17 +61,14 @@ describe("LLMGatewayPlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => { catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
provider.disabled = true provider.disabled = true
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
package: "@ai-sdk/openai-compatible",
url: "https://api.llmgateway.io/v1",
}
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true) expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true)
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({}) expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toBeUndefined()
}), }),
) )
}) })

View file

@ -28,7 +28,8 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/mistral", package: "@ai-sdk/mistral",
options: { name: "mistral" }, options: { name: "mistral" },
@ -45,7 +46,8 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "mistral" }, options: { name: "mistral" },
@ -68,7 +70,8 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/mistral", package: "@ai-sdk/mistral",
options: { name: "mistral" }, options: { name: "mistral" },
@ -92,7 +95,8 @@ describe("MistralPlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/mistral", package: "@ai-sdk/mistral",
options: { name: "custom-mistral" }, options: { name: "custom-mistral" },
@ -101,7 +105,7 @@ describe("MistralPlugin", () => {
}), }),
) )
it.effect("leaves Mistral language selection on the default sdk.languageModel(api.id) path", () => it.effect("leaves Mistral language selection on the default sdk.languageModel(modelID) path", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
@ -116,12 +120,13 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
}), }),
sdk, sdk,
options: {}, options: {},
}) })
const language = result.language ?? sdk.languageModel(result.model.api.id) const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
expect(calls).toEqual(["languageModel:mistral-large"]) expect(calls).toEqual(["languageModel:mistral-large"])
expect(language).toBeDefined() expect(language).toBeDefined()
}), }),

View file

@ -27,23 +27,20 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
package: "@ai-sdk/openai-compatible", provider.headers = { Existing: "value" }
url: "https://integrate.api.nvidia.com/v1",
}
provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
}) })
catalog.provider.update(ProviderV2.ID.openrouter, () => {}) catalog.provider.update(ProviderV2.ID.openrouter, () => {})
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
Existing: "value", Existing: "value",
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode", "X-BILLING-INVOKE-ORIGIN": "OpenCode",
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({}) expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
}), }),
) )
@ -52,16 +49,13 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode", "X-BILLING-INVOKE-ORIGIN": "OpenCode",
@ -74,21 +68,14 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
package: "@ai-sdk/openai-compatible", provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }
url: "https://integrate.api.nvidia.com/v1",
}
provider.request = {
settings: {},
headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
body: { baseURL: "https://integrate.api.nvidia.com/v1" },
}
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin", "X-BILLING-INVOKE-ORIGIN": "CustomOrigin",

View file

@ -27,7 +27,8 @@ describe("OpenAICompatiblePlugin", () => {
const defaulted = yield* aisdk.runSDK({ const defaulted = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "custom" }, options: { name: "custom" },
@ -35,7 +36,8 @@ describe("OpenAICompatiblePlugin", () => {
const disabled = yield* aisdk.runSDK({ const disabled = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "custom", includeUsage: false }, options: { name: "custom", includeUsage: false },
@ -53,7 +55,8 @@ describe("OpenAICompatiblePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "file:///tmp/@ai-sdk/openai-compatible-provider.js", package: "file:///tmp/@ai-sdk/openai-compatible-provider.js",
options: { name: "custom" }, options: { name: "custom" },
@ -76,7 +79,8 @@ describe("OpenAICompatiblePlugin", () => {
yield* aisdk.runSDK({ yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "custom-provider", baseURL: "https://example.com/v1" }, options: { name: "custom-provider", baseURL: "https://example.com/v1" },
@ -96,7 +100,8 @@ describe("OpenAICompatiblePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai" }, options: { name: "cloudflare-workers-ai" },

View file

@ -82,7 +82,8 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/openai", package: "@ai-sdk/openai",
options: { name: "custom-openai", apiKey: "test" }, options: { name: "custom-openai", apiKey: "test" },
@ -99,7 +100,8 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "openai" }, options: { name: "openai" },
@ -117,7 +119,8 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -136,7 +139,8 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -152,10 +156,10 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({ const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai), ...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "@ai-sdk/openai" }, package: ProviderV2.aisdk("@ai-sdk/openai"),
}) })
catalog.provider.update(item.id, (draft) => { catalog.provider.update(item.id, (draft) => {
draft.api = item.api draft.package = item.package
}) })
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {}) catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
@ -175,10 +179,10 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({ const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai), ...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "@ai-sdk/openai" }, package: ProviderV2.aisdk("@ai-sdk/openai"),
}) })
catalog.provider.update(item.id, (draft) => { catalog.provider.update(item.id, (draft) => {
draft.api = item.api draft.package = item.package
}) })
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => { catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => {
model.cost = [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }] model.cost = [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }]
@ -220,10 +224,10 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({ const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai), ...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "@ai-sdk/openai" }, package: ProviderV2.aisdk("@ai-sdk/openai"),
}) })
catalog.provider.update(item.id, (draft) => { catalog.provider.update(item.id, (draft) => {
draft.api = item.api draft.package = item.package
}) })
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), () => {}) catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {}) catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {})
@ -248,10 +252,10 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({ const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")), ...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
api: { type: "aisdk", package: "test-provider" }, package: ProviderV2.aisdk("test-provider"),
}) })
catalog.provider.update(item.id, (draft) => { catalog.provider.update(item.id, (draft) => {
draft.api = item.api draft.package = item.package
}) })
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
}) })

View file

@ -172,13 +172,10 @@ describe("OpencodePlugin", () => {
expect(provider).toMatchObject({ expect(provider).toMatchObject({
name: "Remote", name: "Remote",
integrationID: "opencode", integrationID: "opencode",
api: { package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
type: "aisdk", settings: { baseURL: `${server.url.origin}/v1`, custom: "value" },
package: "@ai-sdk/openai-compatible", headers: { "x-org-id": "org" },
url: `${server.url.origin}/v1`,
},
}) })
expect(provider.request).toEqual({ settings: {}, headers: { "x-org-id": "org" }, body: { custom: "value" } })
expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined() expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model"))) const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))
@ -188,8 +185,10 @@ describe("OpencodePlugin", () => {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }], cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 }, limit: { context: 1000, output: 100 },
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 },
headers: { "x-org-id": "org" },
}) })
expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 })
expect(model.variants).toEqual([ expect(model.variants).toEqual([
{ {
id: ModelV2.VariantID.make("custom"), id: ModelV2.VariantID.make("custom"),
@ -199,9 +198,8 @@ describe("OpencodePlugin", () => {
}, },
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
settings: {}, settings: { temperature: 0.2 },
headers: {}, headers: {},
body: { temperature: 0.2 },
}, },
]) ])
expect( expect(
@ -221,11 +219,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({ const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode), ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" }, package: ProviderV2.aisdk("test-provider"),
}) })
const model = ModelV2.Info.make({ const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("paid"),
package: ProviderV2.aisdk("test-provider"),
cost: cost(1), cost: cost(1),
}) })
catalog.provider.update(provider.id, () => {}) catalog.provider.update(provider.id, () => {})
@ -234,7 +233,7 @@ describe("OpencodePlugin", () => {
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public") expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false) expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
}), }),
), ),
@ -247,11 +246,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({ const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode), ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" }, package: ProviderV2.aisdk("test-provider"),
}) })
const model = ModelV2.Info.make({ const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")), ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
api: { id: ModelV2.ID.make("free"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("free"),
package: ProviderV2.aisdk("test-provider"),
cost: cost(0), cost: cost(0),
}) })
catalog.provider.update(provider.id, () => {}) catalog.provider.update(provider.id, () => {})
@ -260,7 +260,7 @@ describe("OpencodePlugin", () => {
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public") expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true) expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
}), }),
), ),
@ -273,11 +273,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({ const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode), ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" }, package: ProviderV2.aisdk("test-provider"),
}) })
const model = ModelV2.Info.make({ const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")), ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
api: { id: ModelV2.ID.make("output-only"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("output-only"),
package: ProviderV2.aisdk("test-provider"),
cost: cost(0, 1), cost: cost(0, 1),
}) })
catalog.provider.update(provider.id, () => {}) catalog.provider.update(provider.id, () => {})
@ -286,7 +287,7 @@ describe("OpencodePlugin", () => {
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public") expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe( expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(
true, true,
) )
@ -301,11 +302,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({ const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode), ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" }, package: ProviderV2.aisdk("test-provider"),
}) })
const model = ModelV2.Info.make({ const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("paid"),
package: ProviderV2.aisdk("test-provider"),
cost: cost(1), cost: cost(1),
}) })
catalog.provider.update(provider.id, () => {}) catalog.provider.update(provider.id, () => {})
@ -314,7 +316,7 @@ describe("OpencodePlugin", () => {
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined() expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}), }),
), ),
@ -334,11 +336,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({ const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode), ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" }, package: ProviderV2.aisdk("test-provider"),
}) })
const model = ModelV2.Info.make({ const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("paid"),
package: ProviderV2.aisdk("test-provider"),
cost: cost(1), cost: cost(1),
}) })
catalog.provider.update(provider.id, () => {}) catalog.provider.update(provider.id, () => {})
@ -347,7 +350,7 @@ describe("OpencodePlugin", () => {
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined() expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}), }),
), ),
@ -360,27 +363,25 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({ const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode), ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" }, package: ProviderV2.aisdk("test-provider"),
request: { settings: { apiKey: "configured" },
settings: {},
headers: {},
body: { apiKey: "configured" },
},
}) })
const model = ModelV2.Info.make({ const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("paid"),
package: ProviderV2.aisdk("test-provider"),
cost: cost(1), cost: cost(1),
}) })
catalog.provider.update(provider.id, (draft) => { catalog.provider.update(provider.id, (draft) => {
draft.request = { settings: {}, headers: {}, body: { apiKey: "configured" } } draft.package = provider.package
draft.settings = { apiKey: "configured" }
}) })
catalog.model.update(provider.id, model.id, (draft) => { catalog.model.update(provider.id, model.id, (draft) => {
draft.cost = [...model.cost] draft.cost = [...model.cost]
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured") expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("configured")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}), }),
), ),
@ -393,11 +394,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({ const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai), ...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "test-provider" }, package: ProviderV2.aisdk("test-provider"),
}) })
const model = ModelV2.Info.make({ const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("paid"),
package: ProviderV2.aisdk("test-provider"),
cost: cost(1), cost: cost(1),
}) })
catalog.provider.update(provider.id, () => {}) catalog.provider.update(provider.id, () => {})
@ -406,7 +408,7 @@ describe("OpencodePlugin", () => {
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined() expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true) expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
}), }),
), ),

View file

@ -30,19 +30,19 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" } provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
provider.request = { settings: {}, headers: { Existing: "value" }, body: {} } provider.headers = { Existing: "value" }
}) })
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {}) catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toEqual({
Existing: "value", Existing: "value",
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
}) })
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({}) expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toBeUndefined()
}), }),
) )
@ -55,7 +55,8 @@ describe("OpenRouterPlugin", () => {
const ignored = yield* aisdk.runSDK({ const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "openrouter" }, options: { name: "openrouter" },
@ -65,7 +66,8 @@ describe("OpenRouterPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("openai/gpt-5"),
package: ProviderV2.aisdk("test-provider"),
}), }),
package: "@openrouter/ai-sdk-provider", package: "@openrouter/ai-sdk-provider",
options: { name: "custom" }, options: { name: "custom" },
@ -79,7 +81,7 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" } provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
}) })
catalog.provider.update(ProviderV2.ID.openai, () => {}) catalog.provider.update(ProviderV2.ID.openai, () => {})
catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {}) catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {})

View file

@ -41,7 +41,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/perplexity", package: "@ai-sdk/perplexity",
options: { name: "perplexity" }, options: { name: "perplexity" },
@ -58,7 +59,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/perplexity-compatible", package: "@ai-sdk/perplexity-compatible",
options: { name: "perplexity" }, options: { name: "perplexity" },
@ -75,7 +77,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/perplexity", package: "@ai-sdk/perplexity",
options: { name: "perplexity" }, options: { name: "perplexity" },
@ -92,7 +95,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/perplexity", package: "@ai-sdk/perplexity",
options: { name: "custom-perplexity" }, options: { name: "custom-perplexity" },
@ -110,7 +114,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},

View file

@ -49,7 +49,8 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
function model(providerID: string) { function model(providerID: string) {
return ModelV2.Info.make({ return ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")), ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
api: { id: ModelV2.ID.make("sap-model"), type: "aisdk", package: fixtureProvider }, modelID: ModelV2.ID.make("sap-model"),
package: ProviderV2.aisdk(fixtureProvider),
}) })
} }

View file

@ -59,7 +59,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("gpt-4"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai", package: "@ai-sdk/openai",
options: { name: "openai" }, options: { name: "openai" },
@ -77,7 +78,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@ -96,7 +98,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { options: {
@ -119,7 +122,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@ -138,7 +142,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { options: {
@ -161,7 +166,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },

View file

@ -41,7 +41,8 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/togetherai", package: "@ai-sdk/togetherai",
options: { name: "togetherai" }, options: { name: "togetherai" },
@ -59,7 +60,8 @@ describe("TogetherAIPlugin", () => {
const ignored = yield* aisdk.runSDK({ const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "file:///tmp/@ai-sdk/togetherai-provider.js", package: "file:///tmp/@ai-sdk/togetherai-provider.js",
options: { name: "togetherai" }, options: { name: "togetherai" },
@ -69,7 +71,8 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/togetherai", package: "@ai-sdk/togetherai",
options: { name: "togetherai" }, options: { name: "togetherai" },
@ -87,7 +90,8 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/togetherai", package: "@ai-sdk/togetherai",
options: { name: "custom-togetherai" }, options: { name: "custom-togetherai" },
@ -110,11 +114,8 @@ describe("TogetherAIPlugin", () => {
ProviderV2.ID.make("togetherai"), ProviderV2.ID.make("togetherai"),
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
), ),
api: { modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), package: "aisdk:test-provider",
type: "aisdk",
package: "test-provider",
},
}), }),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {}, options: {},
@ -122,7 +123,9 @@ describe("TogetherAIPlugin", () => {
expect(result.language).toBeUndefined() expect(result.language).toBeUndefined()
expect(calls).toEqual([]) expect(calls).toEqual([])
expect(result.language ?? fakeSelectorSdk(calls).languageModel(result.model.api.id)).toBeDefined() expect(
result.language ?? fakeSelectorSdk(calls).languageModel(result.model.modelID ?? result.model.id),
).toBeDefined()
expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"]) expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"])
}), }),
) )

View file

@ -41,7 +41,8 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "venice-ai-sdk-provider", package: "venice-ai-sdk-provider",
options: { name: "venice" }, options: { name: "venice" },
@ -58,7 +59,8 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "venice-ai-sdk-provider", package: "venice-ai-sdk-provider",
options: { name: "custom-venice", apiKey: "test" }, options: { name: "custom-venice", apiKey: "test" },
@ -76,7 +78,8 @@ describe("VenicePlugin", () => {
const similar = yield* aisdk.runSDK({ const similar = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "file:///tmp/venice-ai-sdk-provider.js", package: "file:///tmp/venice-ai-sdk-provider.js",
options: { name: "venice" }, options: { name: "venice" },
@ -84,7 +87,8 @@ describe("VenicePlugin", () => {
const other = yield* aisdk.runSDK({ const other = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: { name: "venice" }, options: { name: "venice" },
@ -103,7 +107,8 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" }, modelID: ModelV2.ID.make("alias"),
package: "aisdk:test-provider",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},

View file

@ -25,12 +25,12 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => { catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" } provider.package = ProviderV2.aisdk("@ai-sdk/vercel")
provider.request.headers.Existing = "1" provider.headers = { ...provider.headers, Existing: "1" }
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).toEqual({ expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).toEqual({
Existing: "1", Existing: "1",
"http-referer": "https://opencode.ai/", "http-referer": "https://opencode.ai/",
"x-title": "opencode", "x-title": "opencode",
@ -43,14 +43,12 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => { catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" } provider.package = ProviderV2.aisdk("@ai-sdk/vercel")
}), }),
) )
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty( expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer")
"HTTP-Referer", expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title")
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty("X-Title")
}), }),
) )
@ -62,7 +60,8 @@ describe("VercelPlugin", () => {
const event = yield* aisdk.runSDK({ const event = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" }, modelID: ModelV2.ID.make("v0-1.0-md"),
package: "aisdk:@ai-sdk/vercel",
}), }),
package: "@ai-sdk/vercel", package: "@ai-sdk/vercel",
options: { name: "custom-vercel" }, options: { name: "custom-vercel" },
@ -77,7 +76,7 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {})) yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {}))
yield* addPlugin() yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.request.headers).toEqual({}) expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.headers).toBeUndefined()
}), }),
) )
}) })

View file

@ -42,7 +42,8 @@ describe("XAIPlugin", () => {
const ignored = yield* aisdk.runSDK({ const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
}), }),
package: "@ai-sdk/openai-compatible", package: "@ai-sdk/openai-compatible",
options: {}, options: {},
@ -51,7 +52,8 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
}), }),
package: "@ai-sdk/xai", package: "@ai-sdk/xai",
options: {}, options: {},
@ -71,7 +73,8 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runSDK({ const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), ...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
}), }),
package: "@ai-sdk/xai", package: "@ai-sdk/xai",
options: {}, options: {},
@ -81,7 +84,7 @@ describe("XAIPlugin", () => {
}), }),
) )
it.effect("uses responses with the model api.id for xAI language models", () => it.effect("uses responses with the model modelID for xAI language models", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
@ -91,7 +94,8 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},
@ -112,7 +116,8 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runLanguage({ const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({ model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
}), }),
sdk: fakeSelectorSdk(calls), sdk: fakeSelectorSdk(calls),
options: {}, options: {},

View file

@ -32,17 +32,14 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
}) })
}) })
yield* addPlugin() yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))) const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }) expect(result.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
expect(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"]) expect(Object.keys(required(result.headers)).sort()).toEqual(["HTTP-Referer", "X-Title"])
}), }),
) )
@ -51,17 +48,14 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
package: "@ai-sdk/openai-compatible", provider.headers = { ...provider.headers, Existing: "value" }
url: "https://zenmux.ai/api/v1",
}
provider.request.headers.Existing = "value"
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({ expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
Existing: "value", Existing: "value",
"HTTP-Referer": "https://opencode.ai/", "HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode", "X-Title": "opencode",
@ -74,17 +68,14 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = { provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk", provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
package: "@ai-sdk/openai-compatible", provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
url: "https://zenmux.ai/api/v1",
}
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({ expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
"HTTP-Referer": "https://example.com/", "HTTP-Referer": "https://example.com/",
"X-Title": "custom-title", "X-Title": "custom-title",
}) })
@ -96,12 +87,12 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => { yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" } provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
}) })
}) })
yield* addPlugin() yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({ expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).headers).toEqual({
"HTTP-Referer": "https://example.com/", "HTTP-Referer": "https://example.com/",
"X-Title": "custom-title", "X-Title": "custom-title",
}) })

View file

@ -24,14 +24,11 @@ describe("VariantPlugin", () => {
const service = yield* Catalog.Service const service = yield* Catalog.Service
yield* service.transform((catalog) => { yield* service.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.opencode, (provider) => { catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" } provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
}) })
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => { catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
model.api = { model.modelID = ModelV2.ID.make("glm-5.2")
id: ModelV2.ID.make("glm-5.2"), model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk",
package: "@ai-sdk/openai-compatible",
}
}) })
}) })
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
@ -48,11 +45,8 @@ describe("VariantPlugin", () => {
const service = yield* Catalog.Service const service = yield* Catalog.Service
yield* service.transform((catalog) => { yield* service.transform((catalog) => {
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => { catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
model.api = { model.modelID = ModelV2.ID.make("glm-5.2")
id: ModelV2.ID.make("glm-5.2"), model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
type: "aisdk",
package: "@ai-sdk/openai-compatible",
}
model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }] model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }]
}) })
}) })

View file

@ -2,8 +2,7 @@ import { describe, expect, test } from "bun:test"
import fs from "fs/promises" import fs from "fs/promises"
import path from "path" import path from "path"
import { DateTime, Effect, Layer } from "effect" import { DateTime, Effect, Layer } from "effect"
import { Message, Model } from "@opencode-ai/llm" import { Message } from "@opencode-ai/llm"
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
import { AgentV2 } from "@opencode-ai/core/agent" import { AgentV2 } from "@opencode-ai/core/agent"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@ -300,7 +299,7 @@ describe("SessionInstructions", () => {
test("toLLMMessages does not forward synthetic metadata to the provider", () => { test("toLLMMessages does not forward synthetic metadata to the provider", () => {
const created = DateTime.makeUnsafe(0) const created = DateTime.makeUnsafe(0)
const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route }) const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
const synthetic = SessionMessage.Synthetic.make({ const synthetic = SessionMessage.Synthetic.make({
id: SessionMessage.ID.make("msg_synthetic"), id: SessionMessage.ID.make("msg_synthetic"),
type: "synthetic", type: "synthetic",

View file

@ -1,6 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Message, Model } from "@opencode-ai/llm" import { Message } from "@opencode-ai/llm"
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionMessage } from "@opencode-ai/core/session/message"
@ -12,7 +11,7 @@ import { DateTime } from "effect"
const created = DateTime.makeUnsafe(0) const created = DateTime.makeUnsafe(0)
const id = (value: string) => SessionMessage.ID.make(`msg_${value}`) const id = (value: string) => SessionMessage.ID.make(`msg_${value}`)
const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route }) const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
describe("toLLMMessages", () => { describe("toLLMMessages", () => {
test("omits empty assistant turns", () => { test("omits empty assistant turns", () => {
@ -609,4 +608,35 @@ Recent work
}, },
]) ])
}) })
test("preserves provider metadata for a catalog alias with a different API model ID", () => {
const messages = toLLMMessages(
[
SessionMessage.Assistant.make({
id: id("assistant-alias"),
type: "assistant",
agent: "build",
model: { id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") },
content: [
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning-alias",
text: "Visible thought",
providerMetadata: { openai: { reasoningEncryptedContent: "encrypted" } },
}),
],
time: { created, completed: created },
}),
],
ModelV2.Ref.make({ id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") }),
)
expect(messages[0]?.content).toEqual([
{
type: "reasoning",
text: "Visible thought",
providerMetadata: { openai: { reasoningEncryptedContent: "encrypted" } },
},
])
})
}) })

View file

@ -1,5 +1,5 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { LLM } from "@opencode-ai/llm" import { LLM, Model } from "@opencode-ai/llm"
import { LLMClient } from "@opencode-ai/llm/route" import { LLMClient } from "@opencode-ai/llm/route"
import { DateTime, Effect } from "effect" import { DateTime, Effect } from "effect"
import { Headers } from "effect/unstable/http" import { Headers } from "effect/unstable/http"
@ -13,28 +13,26 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { it } from "./lib/effect" import { it } from "./lib/effect"
type Api = interface ModelOptions {
| { readonly modelID?: string
readonly type: "aisdk" readonly settings?: ModelV2.Info["settings"]
readonly package: string readonly headers?: ModelV2.Info["headers"]
readonly url?: string readonly body?: ModelV2.Info["body"]
readonly settings?: Record<string, unknown> readonly variants?: ModelV2.Info["variants"]
} }
| { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
const model = (api: Api, variants: ModelV2.Info["variants"] = []) => const model = (packageName: string | undefined, options: ModelOptions = {}) =>
ModelV2.Info.make({ ModelV2.Info.make({
id: ModelV2.ID.make("test-model"), id: ModelV2.ID.make("test-model"),
modelID: ModelV2.ID.make(options.modelID ?? "api-test-model"),
providerID: ProviderV2.ID.make("test-provider"), providerID: ProviderV2.ID.make("test-provider"),
name: "Test model", name: "Test model",
api: { id: ModelV2.ID.make("api-test-model"), ...api }, package: packageName,
settings: options.settings ?? {},
headers: options.headers ?? { "x-test": "header" },
body: options.body ?? { custom_extension: { enabled: true } },
capabilities: { tools: true, input: ["text"], output: ["text"] }, capabilities: { tools: true, input: ["text"], output: ["text"] },
request: { variants: options.variants ?? [],
settings: {},
headers: { "x-test": "header" },
body: { apiKey: "secret", custom_extension: { enabled: true } },
},
variants,
time: { released: 0 }, time: { released: 0 },
cost: [], cost: [],
status: "active", status: "active",
@ -43,12 +41,14 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
}) })
describe("SessionRunnerModel", () => { describe("SessionRunnerModel", () => {
it.effect("maps catalog OpenAI AI SDK models into native Responses routes", () => it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), settings: { baseURL: "https://openai.example/v1" },
) })
const resolved = yield* SessionRunnerModel.fromCatalogModel(catalog)
expect(catalog.id).toBe(ModelV2.ID.make("test-model"))
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" }) expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
expect(resolved.route).toMatchObject({ expect(resolved.route).toMatchObject({
id: "openai-responses", id: "openai-responses",
@ -65,7 +65,9 @@ describe("SessionRunnerModel", () => {
it.effect("keeps catalog apiKey credentials out of provider JSON", () => it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { apiKey: "secret", baseURL: "https://openai.example/v1" },
}),
) )
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" })) const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
@ -77,14 +79,14 @@ describe("SessionRunnerModel", () => {
it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () => it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
...model({ settings: {
type: "aisdk", apiKey: "settings-secret",
package: "@ai-sdk/openai-compatible", baseURL: "https://compatible.example/v1",
url: "https://compatible.example/v1", compatibility: "strict",
settings: { apiKey: "settings-secret", compatibility: "strict" }, },
}), headers: {},
request: { settings: {}, headers: {}, body: {} }, body: {},
}), }),
) )
const request = LLM.request({ model: resolved, prompt: "Hello" }) const request = LLM.request({ model: resolved, prompt: "Hello" })
@ -97,24 +99,29 @@ describe("SessionRunnerModel", () => {
}) })
expect(headers.authorization).toBe("Bearer settings-secret") expect(headers.authorization).toBe("Bearer settings-secret")
expect(resolved.route.id).toBe("openai-compatible-chat")
expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
expect(resolved.route.defaults.http?.body).toEqual({}) expect(resolved.route.defaults.http?.body).toEqual({})
}), }),
) )
it.effect("overlays selected OpenAI Session variant settings and bodies", () => it.effect("overlays selected OpenAI Session variant settings and bodies", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [ const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
{ settings: { baseURL: "https://openai.example/v1" },
id: ModelV2.VariantID.make("high"), variants: [
settings: { reasoningEffort: "high" }, {
headers: { "x-variant": "high" }, id: ModelV2.VariantID.make("high"),
body: { settings: { reasoningEffort: "high" },
store: false, headers: { "x-variant": "high" },
service_tier: "priority", body: {
temperature: 0.2, store: false,
service_tier: "priority",
temperature: 0.2,
},
}, },
}, ],
]) })
const session = SessionV2.Info.make({ const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant"), id: SessionV2.ID.make("ses_model_variant"),
projectID: ProjectV2.ID.global, projectID: ProjectV2.ID.global,
@ -147,9 +154,9 @@ describe("SessionRunnerModel", () => {
it.effect("overlays selected OpenAI-compatible Session variant bodies", () => it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = model( const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
{ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" }, settings: { baseURL: "https://compatible.example/v1" },
[ variants: [
{ {
id: ModelV2.VariantID.make("high"), id: ModelV2.VariantID.make("high"),
settings: {}, settings: {},
@ -157,7 +164,7 @@ describe("SessionRunnerModel", () => {
body: { store: false, reasoning_effort: "high" }, body: { store: false, reasoning_effort: "high" },
}, },
], ],
) })
const session = SessionV2.Info.make({ const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_compatible_variant"), id: SessionV2.ID.make("ses_compatible_variant"),
projectID: ProjectV2.ID.global, projectID: ProjectV2.ID.global,
@ -181,7 +188,9 @@ describe("SessionRunnerModel", () => {
it.effect("rejects an explicit unavailable Session variant during model resolution", () => it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }) const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
})
const session = SessionV2.Info.make({ const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant_unavailable"), id: SessionV2.ID.make("ses_model_variant_unavailable"),
projectID: ProjectV2.ID.global, projectID: ProjectV2.ID.global,
@ -211,14 +220,17 @@ describe("SessionRunnerModel", () => {
it.effect("overlays selected Anthropic Session variant settings", () => it.effect("overlays selected Anthropic Session variant settings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [ const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
{ settings: { baseURL: "https://anthropic.example/v1" },
id: ModelV2.VariantID.make("high"), variants: [
settings: { thinking: { type: "enabled", budgetTokens: 12000 } }, {
headers: {}, id: ModelV2.VariantID.make("high"),
body: {}, settings: { thinking: { type: "enabled", budgetTokens: 12000 } },
}, headers: {},
]) body: {},
},
],
})
const session = SessionV2.Info.make({ const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_anthropic_variant"), id: SessionV2.ID.make("ses_anthropic_variant"),
projectID: ProjectV2.ID.global, projectID: ProjectV2.ID.global,
@ -244,7 +256,9 @@ describe("SessionRunnerModel", () => {
it.effect("maps catalog Anthropic AI SDK models into native routes", () => it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }), model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
settings: { baseURL: "https://anthropic.example/v1" },
}),
) )
expect(resolved.route).toMatchObject({ expect(resolved.route).toMatchObject({
@ -257,9 +271,10 @@ describe("SessionRunnerModel", () => {
it.effect("uses resolved credentials for bearer auth", () => it.effect("uses resolved credentials for bearer auth", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ model(ProviderV2.aisdk("@ai-sdk/openai"), {
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), settings: { baseURL: "https://openai.example/v1" },
request: { settings: {}, headers: {}, body: {} }, headers: {},
body: {},
}), }),
Credential.Key.make({ type: "key", key: "secret" }), Credential.Key.make({ type: "key", key: "secret" }),
) )
@ -280,9 +295,10 @@ describe("SessionRunnerModel", () => {
Effect.gen(function* () { Effect.gen(function* () {
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } }) const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ model(ProviderV2.aisdk("@ai-sdk/openai"), {
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
request: { settings: {}, headers: {}, body: { apiKey: "configured-secret" } }, headers: {},
body: {},
}), }),
credential, credential,
) )
@ -302,9 +318,10 @@ describe("SessionRunnerModel", () => {
it.effect("does not project OAuth account metadata into the request body", () => it.effect("does not project OAuth account metadata into the request body", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ model(ProviderV2.aisdk("@ai-sdk/openai"), {
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), settings: { baseURL: "https://openai.example/v1" },
request: { settings: {}, headers: {}, body: {} }, headers: {},
body: {},
}), }),
Credential.OAuth.make({ Credential.OAuth.make({
type: "oauth", type: "oauth",
@ -323,9 +340,10 @@ describe("SessionRunnerModel", () => {
it.effect("routes ChatGPT OAuth credentials to the codex backend", () => it.effect("routes ChatGPT OAuth credentials to the codex backend", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ model(ProviderV2.aisdk("@ai-sdk/openai"), {
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), settings: { baseURL: "https://openai.example/v1" },
request: { headers: {}, body: {} }, headers: {},
body: {},
}), }),
Credential.OAuth.make({ Credential.OAuth.make({
type: "oauth", type: "oauth",
@ -354,12 +372,78 @@ describe("SessionRunnerModel", () => {
}), }),
) )
it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model("@opencode-ai/llm/providers/openai", {
settings: { baseURL: "https://openai.example/v1" },
}),
Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("chatgpt-browser"),
access: "chatgpt-token",
refresh: "refresh",
expires: Date.now() + 60_000,
metadata: { accountID: "acct_123" },
}),
)
const headers = yield* resolved.route.auth.apply({
request: LLM.request({ model: resolved, prompt: "Hello" }),
method: "POST",
url: "https://chatgpt.com/backend-api/codex/responses",
body: "{}",
headers: Headers.empty,
})
expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex")
expect(headers.authorization).toBe("Bearer chatgpt-token")
expect(headers["chatgpt-account-id"]).toBe("acct_123")
}),
)
it.effect("does not route native OpenAI-compatible packages to the codex backend", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model("@opencode-ai/llm/providers/openai-compatible", {
settings: { baseURL: "https://compatible.example/v1" },
}),
Credential.OAuth.make({
type: "oauth",
methodID: Integration.MethodID.make("chatgpt-browser"),
access: "chatgpt-token",
refresh: "refresh",
expires: Date.now() + 60_000,
metadata: { accountID: "acct_123" },
}),
)
expect(resolved.route.id).toBe("openai-compatible-chat")
expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
}),
)
it.effect("maps legacy OpenAI organization and project settings to headers", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { organization: "org_123", project: "proj_123" },
}),
)
expect(resolved.route.defaults.headers).toMatchObject({
"OpenAI-Organization": "org_123",
"OpenAI-Project": "proj_123",
})
}),
)
it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () => it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ model(ProviderV2.aisdk("@ai-sdk/openai"), {
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), settings: { baseURL: "https://openai.example/v1" },
request: { headers: {}, body: {} }, headers: {},
body: {},
}), }),
Credential.OAuth.make({ Credential.OAuth.make({
type: "oauth", type: "oauth",
@ -387,9 +471,10 @@ describe("SessionRunnerModel", () => {
it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () => it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () =>
Effect.gen(function* () { Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel( const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({ model(ProviderV2.aisdk("@ai-sdk/openai"), {
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), settings: { baseURL: "https://openai.example/v1" },
request: { headers: {}, body: {} }, headers: {},
body: {},
}), }),
Credential.OAuth.make({ Credential.OAuth.make({
type: "oauth", type: "oauth",
@ -415,35 +500,106 @@ describe("SessionRunnerModel", () => {
}), }),
) )
it.effect("rejects catalog APIs without a native route", () => it.effect("loads dynamic native provider packages through the injected package loader", () =>
Effect.gen(function* () {
const native = yield* SessionRunnerModel.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
}),
)
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model("@opencode-ai/llm/providers/custom", {
settings: { region: "test" },
headers: { "x-package": "header" },
body: { custom: true },
}),
undefined,
{
loadPackage: (specifier) => {
expect(specifier).toBe("@opencode-ai/llm/providers/custom")
return Effect.succeed({
model: (modelID, settings) => {
expect(modelID).toBe("api-test-model")
expect(settings).toEqual({
region: "test",
headers: { "x-package": "header" },
body: { custom: true },
limits: { context: 100, output: 20 },
})
return Model.make({ id: modelID, provider: "package-provider", route: native.route })
},
})
},
},
)
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
}),
)
it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () =>
Effect.gen(function* () {
const native = yield* SessionRunnerModel.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/openai"), {
settings: { baseURL: "https://openai.example/v1" },
}),
)
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model(ProviderV2.aisdk("@ai-sdk/google"), {
modelID: "gemini-api-model",
settings: { project: "test" },
headers: { "x-aisdk": "header" },
body: { custom: true },
}),
Credential.Key.make({ type: "key", key: "fallback-secret" }),
{
loadAISDK: (runtime) =>
Effect.sync(() => {
expect(runtime).toMatchObject({
id: "test-model",
modelID: "gemini-api-model",
providerID: "test-provider",
package: ProviderV2.aisdk("@ai-sdk/google"),
settings: { project: "test", apiKey: "fallback-secret" },
headers: { "x-aisdk": "header" },
body: { custom: true },
})
return Model.make({
id: runtime.modelID ?? runtime.id,
provider: runtime.providerID,
route: native.route,
})
}),
},
)
expect(resolved).toMatchObject({ id: "gemini-api-model", provider: "test-provider" })
}),
)
it.effect("rejects AISDK packages without an available loader", () =>
Effect.gen(function* () { Effect.gen(function* () {
const failure = yield* SessionRunnerModel.fromCatalogModel( const failure = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }), model(ProviderV2.aisdk("@ai-sdk/google"), {
settings: { baseURL: "https://google.example/v1" },
}),
).pipe(Effect.flip) ).pipe(Effect.flip)
expect(failure).toMatchObject({ expect(failure).toMatchObject({
_tag: "SessionRunnerModel.UnsupportedApiError", _tag: "SessionRunnerModel.UnsupportedPackageError",
providerID: "test-provider", providerID: "test-provider",
modelID: "test-model", modelID: "test-model",
api: "aisdk:@ai-sdk/google", package: "aisdk:@ai-sdk/google",
}) })
expect(failure.message).toBe("Unsupported API for test-provider/test-model: aisdk:@ai-sdk/google") expect(failure.message).toBe("Unsupported package for test-provider/test-model: aisdk:@ai-sdk/google")
}), }),
) )
it.effect("reports whether a catalog model has a supported native route", () => it.effect("reports whether a catalog model declares a provider package", () =>
Effect.sync(() => { Effect.sync(() => {
expect( expect(SessionRunnerModel.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true)
SessionRunnerModel.supported( expect(SessionRunnerModel.supported(model("@opencode-ai/llm/providers/custom"))).toBe(true)
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), expect(SessionRunnerModel.supported(model(undefined))).toBe(false)
),
).toBe(true)
expect(
SessionRunnerModel.supported(
model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
),
).toBe(false)
expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false)
}), }),
) )
}) })

View file

@ -112,12 +112,8 @@ test("Core reuses the canonical shared schemas", async () => {
[ModelV2.Family, Model.Family], [ModelV2.Family, Model.Family],
[ModelV2.Capabilities, Model.Capabilities], [ModelV2.Capabilities, Model.Capabilities],
[ModelV2.Cost, Model.Cost], [ModelV2.Cost, Model.Cost],
[ModelV2.Api, Model.Api],
[ModelV2.Info, Model.Info], [ModelV2.Info, Model.Info],
[ProviderV2.ID, Provider.ID], [ProviderV2.ID, Provider.ID],
[ProviderV2.AISDK, Provider.AISDK],
[ProviderV2.Native, Provider.Native],
[ProviderV2.Api, Provider.Api],
[ProviderV2.Request, Provider.Request], [ProviderV2.Request, Provider.Request],
[ProviderV2.Info, Provider.Info], [ProviderV2.Info, Provider.Info],
[corePermission.Effect, Permission.Effect], [corePermission.Effect, Permission.Effect],

View file

@ -15,6 +15,7 @@
"./route": "./src/route/index.ts", "./route": "./src/route/index.ts",
"./provider": "./src/provider.ts", "./provider": "./src/provider.ts",
"./providers": "./src/providers/index.ts", "./providers": "./src/providers/index.ts",
"./provider-package": "./src/provider-package.ts",
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts", "./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
"./providers/anthropic": "./src/providers/anthropic.ts", "./providers/anthropic": "./src/providers/anthropic.ts",
"./providers/azure": "./src/providers/azure.ts", "./providers/azure": "./src/providers/azure.ts",
@ -22,6 +23,8 @@
"./providers/github-copilot": "./src/providers/github-copilot.ts", "./providers/github-copilot": "./src/providers/github-copilot.ts",
"./providers/google": "./src/providers/google.ts", "./providers/google": "./src/providers/google.ts",
"./providers/openai": "./src/providers/openai.ts", "./providers/openai": "./src/providers/openai.ts",
"./providers/openai/responses": "./src/providers/openai/responses.ts",
"./providers/openai/chat": "./src/providers/openai/chat.ts",
"./providers/openai-compatible": "./src/providers/openai-compatible.ts", "./providers/openai-compatible": "./src/providers/openai-compatible.ts",
"./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts", "./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
"./providers/openrouter": "./src/providers/openrouter.ts", "./providers/openrouter": "./src/providers/openrouter.ts",

View file

@ -1,6 +1,7 @@
export { LLMClient } from "./route/client" export { LLMClient } from "./route/client"
export { Auth } from "./route/auth" export { Auth } from "./route/auth"
export { Provider } from "./provider" export { Provider } from "./provider"
export { ProviderPackage } from "./provider-package"
export { isContextOverflow, isContextOverflowFailure } from "./provider-error" export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
export type { export type {
RouteModelInput, RouteModelInput,
@ -31,3 +32,4 @@ export type {
ModelFactory as ProviderModelFactory, ModelFactory as ProviderModelFactory,
ModelOptions as ProviderModelOptions, ModelOptions as ProviderModelOptions,
} from "./provider" } from "./provider"
export type { Definition as ProviderPackageDefinition, Settings as ProviderPackageSettings } from "./provider-package"

View file

@ -0,0 +1,16 @@
import type { Model } from "./schema"
export interface Settings extends Readonly<Record<string, unknown>> {
readonly headers?: Readonly<Record<string, string>>
readonly body?: Readonly<Record<string, unknown>>
readonly limits?: {
readonly context: number
readonly output: number
}
}
export interface Definition<ProviderSettings extends Settings = Settings> {
readonly model: (modelID: string, settings: ProviderSettings) => Model
}
export * as ProviderPackage from "./provider-package"

View file

@ -1,5 +1,6 @@
import type { RouteDefaultsInput } from "../route/client" import type { RouteDefaultsInput } from "../route/client"
import { Auth } from "../route/auth" import { Auth } from "../route/auth"
import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema" import { ProviderID, type ModelID } from "../schema"
import * as BedrockConverse from "../protocols/bedrock-converse" import * as BedrockConverse from "../protocols/bedrock-converse"
import type { BedrockCredentials } from "../protocols/bedrock-converse" import type { BedrockCredentials } from "../protocols/bedrock-converse"
@ -15,6 +16,15 @@ export type Config = RouteDefaultsInput & {
/** Override the computed `https://bedrock-runtime.<region>.amazonaws.com` URL. */ /** Override the computed `https://bedrock-runtime.<region>.amazonaws.com` URL. */
readonly baseURL?: string readonly baseURL?: string
} }
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly auth?: "bearer" | "sigv4"
readonly baseURL?: string
readonly credentials?: BedrockCredentials
readonly region?: string
readonly topP?: number
}
export const routes = [BedrockConverse.route] export const routes = [BedrockConverse.route]
const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com` const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com`
@ -40,4 +50,19 @@ export const configure = (input: Config = {}) => {
} }
export const provider = configure() export const provider = configure()
export const model = provider.model export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
if (settings.auth === "bearer" && settings.apiKey === undefined)
throw new Error("Amazon Bedrock bearer auth requires apiKey")
if (settings.auth === "sigv4" && settings.apiKey !== undefined)
throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey")
return configure({
apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey,
baseURL: settings.baseURL,
credentials: settings.credentials,
generation: settings.topP === undefined ? undefined : { topP: settings.topP },
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
region: settings.region,
}).model(modelID)
}

View file

@ -1,6 +1,7 @@
import type { RouteDefaultsInput } from "../route/client" import type { RouteDefaultsInput } from "../route/client"
import { Auth } from "../route/auth" import { Auth } from "../route/auth"
import type { ProviderAuthOption } from "../route/auth-options" import type { ProviderAuthOption } from "../route/auth-options"
import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema" import { ProviderID, type ModelID } from "../schema"
import * as AnthropicMessages from "../protocols/anthropic-messages" import * as AnthropicMessages from "../protocols/anthropic-messages"
@ -10,6 +11,12 @@ export const routes = [AnthropicMessages.route]
export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string } export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly authToken?: string
readonly baseURL?: string
}
const auth = (options: ProviderAuthOption<"optional">) => { const auth = (options: ProviderAuthOption<"optional">) => {
if ("auth" in options && options.auth) return options.auth if ("auth" in options && options.auth) return options.auth
return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey") return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
@ -32,4 +39,11 @@ export const configure = (input: Config = {}) => {
} }
export const provider = configure() export const provider = configure()
export const model = provider.model export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
configure({
...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }),
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
}).model(modelID)

View file

@ -2,6 +2,7 @@ import { ProviderID, type ModelID } from "../schema"
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat" import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
import type { RouteDefaultsInput } from "../route/client" import type { RouteDefaultsInput } from "../route/client"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { ProviderPackage } from "../provider-package"
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile" import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
export const id = ProviderID.make("openai-compatible") export const id = ProviderID.make("openai-compatible")
@ -12,6 +13,12 @@ type GenericModelOptions = RouteDefaultsInput &
readonly baseURL: string readonly baseURL: string
} }
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
}
export type FamilyModelOptions = RouteDefaultsInput & export type FamilyModelOptions = RouteDefaultsInput &
ProviderAuthOption<"optional"> & { ProviderAuthOption<"optional"> & {
readonly baseURL?: string readonly baseURL?: string
@ -56,6 +63,16 @@ export const provider = {
configure, configure,
} }
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
configure({
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
provider: settings.provider,
}).model(modelID)
export const baseten = define(profiles.baseten) export const baseten = define(profiles.baseten)
export const cerebras = define(profiles.cerebras) export const cerebras = define(profiles.cerebras)
export const deepinfra = define(profiles.deepinfra) export const deepinfra = define(profiles.deepinfra)

View file

@ -1,5 +1,6 @@
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options" import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { Route, RouteDefaultsInput } from "../route/client" import type { Route, RouteDefaultsInput } from "../route/client"
import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema" import { ProviderID, type ModelID } from "../schema"
import * as OpenAIChat from "../protocols/openai-chat" import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses" import * as OpenAIResponses from "../protocols/openai-responses"
@ -21,6 +22,16 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: OpenAIProviderOptionsInput readonly providerOptions?: OpenAIProviderOptionsInput
} }
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly organization?: string
readonly project?: string
readonly queryParams?: Readonly<Record<string, string>>
readonly transport?: "http" | "websocket"
readonly providerOptions?: OpenAIProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY") const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
const defaults = (input: Config) => { const defaults = (input: Config) => {
@ -57,7 +68,32 @@ export const configure = (input: Config = {}) => {
export const provider = configure() export const provider = configure()
export const model = provider.model const config = (settings: Settings): Config => {
const headers = {
...(settings.organization === undefined ? {} : { "OpenAI-Organization": settings.organization }),
...(settings.project === undefined ? {} : { "OpenAI-Project": settings.project }),
...settings.headers,
}
return {
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: Object.keys(headers).length === 0 ? undefined : headers,
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
providerOptions: settings.providerOptions,
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
}
}
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
const configured = configure(config(settings))
if (settings.transport === undefined || settings.transport === "http") return configured.responses(modelID)
if (settings.transport === "websocket") return configured.responsesWebSocket(modelID)
throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`)
}
export const chatModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
configure(config(settings)).chat(modelID)
export const responses = provider.responses export const responses = provider.responses
export const responsesWebSocket = provider.responsesWebSocket export const responsesWebSocket = provider.responsesWebSocket
export const chat = provider.chat export const chat = provider.chat

View file

@ -0,0 +1,2 @@
export { chatModel as model } from "../openai"
export type { Settings } from "../openai"

View file

@ -0,0 +1,2 @@
export { model } from "../openai"
export type { Settings } from "../openai"

View file

@ -30,7 +30,6 @@ describe("public exports", () => {
test("provider barrels expose user-facing facades", () => { test("provider barrels expose user-facing facades", () => {
expect(OpenAI.model).toBeFunction() expect(OpenAI.model).toBeFunction()
expect(OpenAI.provider.model).toBe(OpenAI.model)
expect(OpenAI.provider.responses).toBe(OpenAI.responses) expect(OpenAI.provider.responses).toBe(OpenAI.responses)
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket) expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction() expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()

Some files were not shown because too many files have changed in this diff Show more