Compare commits

...

21 Commits

Author SHA1 Message Date
Shoubhit Dash 78880f3288 fix(core): preserve aisdk provider compatibility 2026-06-29 20:48:01 +05:30
Shoubhit Dash 864f16c703 refactor(core): consolidate provider helpers 2026-06-29 19:50:12 +05:30
Shoubhit Dash 5e12dbdbfb chore: sync native provider core stack 2026-06-25 20:37:23 +05:30
Shoubhit Dash 8efd8783d0 fix(schema): adapt legacy catalog responses 2026-06-25 20:36:02 +05:30
Shoubhit Dash 5436b55a7e chore: sync native provider schema stack 2026-06-25 20:32:36 +05:30
Shoubhit Dash 3a09383d33 refactor(llm): name provider model identity 2026-06-25 20:27:22 +05:30
Shoubhit Dash beefda1f8f refactor(llm): colocate provider package logic 2026-06-25 20:27:22 +05:30
Shoubhit Dash 2f85f4744d feat(llm): add native provider packages 2026-06-25 20:27:22 +05:30
Shoubhit Dash cd3885910d feat(core): load native provider packages 2026-06-25 20:24:53 +05:30
Dax Raad 248db536e2 refactor(core): migrate provider consumers 2026-06-25 19:56:14 +05:30
Dax Raad e40ce1d556 fix(core): validate provider body options 2026-06-25 19:55:54 +05:30
Dax Raad aca5adf39e refactor(core): flatten provider config 2026-06-25 19:55:54 +05:30
Shoubhit Dash 1a9bd8c1fd refactor(schema): flatten provider identity 2026-06-25 19:49:14 +05:30
Shoubhit Dash 315ba3720e chore: sync native provider stack 2026-06-25 00:24:57 +05:30
Shoubhit Dash d1446a9e72 refactor(llm): name provider model identity 2026-06-25 00:20:03 +05:30
Shoubhit Dash 928510608d refactor(llm): colocate provider package logic 2026-06-25 00:20:02 +05:30
Shoubhit Dash 2b0b573474 feat(llm): add native provider packages 2026-06-25 00:20:02 +05:30
Shoubhit Dash 62e44a80e7 feat(core): migrate native provider config 2026-06-25 00:17:32 +05:30
Shoubhit Dash 9848587052 refactor(llm): name provider model identity 2026-06-24 23:29:06 +05:30
Shoubhit Dash 547496656c refactor(llm): colocate provider package logic 2026-06-24 23:10:45 +05:30
Shoubhit Dash 733bb06134 feat(llm): add native provider packages 2026-06-24 23:05:02 +05:30
86 changed files with 1879 additions and 1618 deletions
+375 -12
View File
@@ -1,12 +1,46 @@
export * as AISDK from "./aisdk"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
import type {
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, Schema, Scope, Stream } from "effect"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { State } from "./state"
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 {
readonly model: ModelV2.Info
@@ -73,10 +107,10 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
function prepareOptions(model: ModelV2.Info, pkg: string) {
const options: Record<string, any> = {
name: model.providerID,
...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}),
...model.request.body,
...(model.settings ?? {}),
headers: model.headers,
body: model.body,
}
if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
const customFetch = options.fetch
const chunkTimeout = options.chunkTimeout
@@ -141,6 +175,7 @@ export interface Interface {
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
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") {}
@@ -195,24 +230,26 @@ export const locationLayer = Layer.effect(
runSDK: (event) => run(sdkHooks, event),
runLanguage: (event) => run(languageHooks, event),
language: Effect.fn("AISDK.language")(function* (model) {
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
const key = `${model.providerID}/${model.id}/${model.modelID ?? model.id}/${JSON.stringify(model.settings)}`
const existing = languages.get(key)
if (existing) return existing
if (model.api.type !== "aisdk")
if (!ProviderV2.isAISDK(model.package))
return yield* new InitError({
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 options = prepareOptions(model, packageName)
const sdkKey = JSON.stringify({
providerID: model.providerID,
api: model.api,
package: packageName,
settings: model.settings,
options,
})
const sdk =
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)
return yield* new InitError({
providerID: model.providerID,
@@ -220,15 +257,341 @@ export const locationLayer = Layer.effect(
})
sdks.set(sdkKey, sdk)
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),
)
languages.set(key, language)
return language
}),
model: Effect.fn("AISDK.model")(function* (model) {
return modelFromLanguage(model, yield* service.language(model))
}),
})
return service
}),
)
export const defaultLayer = locationLayer
function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
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 ? undefined : { body: { ...info.body } },
limits: { context: info.limit.context, output: info.limit.output },
},
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 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,
})
}
+5 -24
View File
@@ -69,38 +69,21 @@ export const layer = Layer.effect(
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
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
return provider.integrationID === undefined && !integration
}
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 = {
headers: { ...provider.request.headers, ...model.request.headers },
body: { ...provider.request.body, ...model.request.body },
variant: model.request.variant,
}
return ModelV2.Info.make({
...model,
api,
request,
package: model.package ?? provider.package,
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>({
initial: () => ({ providers: new Map() }),
draft: (draft) => {
@@ -118,7 +101,6 @@ export const layer = Layer.effect(
draft.providers.set(providerID, current)
}
fn(current.provider)
normalizeApi(current.provider)
},
remove: (providerID) => {
draft.providers.delete(providerID)
@@ -141,7 +123,6 @@ export const layer = Layer.effect(
fn(model)
model.id = modelID
model.providerID = providerID
normalizeApi(model)
},
remove: (providerID, modelID) => {
draft.providers.get(providerID)?.models.delete(modelID)
+17 -15
View File
@@ -52,17 +52,22 @@ export const Plugin = define({
const providerID = id
catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name
if (item.api !== undefined) provider.api = { ...item.api }
if (item.request !== undefined) {
Object.assign(provider.request.headers, item.request.headers)
Object.assign(provider.request.body, item.request.body)
}
if (item.package !== undefined) provider.package = item.package
if (item.settings !== undefined)
provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings)
if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers)
if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body)
})
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family
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) {
model.capabilities = {
tools: config.capabilities.tools,
@@ -70,24 +75,21 @@ export const Plugin = define({
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
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) {
model.variants ??= []
for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) {
existing = {
id: variant.id,
headers: {},
body: {},
}
model.variants.push(existing)
}
Object.assign(existing.headers, variant.headers)
Object.assign(existing.body, variant.body)
if (variant.settings !== undefined)
existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings)
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) {
+13 -24
View File
@@ -1,12 +1,17 @@
export * as ConfigProvider from "./provider"
import { Schema } from "effect"
import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model"
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
export const Overlays = {
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
body: Schema.Record(Schema.String, Schema.Unknown).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")({
@@ -30,32 +35,16 @@ class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
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")({
modelID: ModelV2.ID.pipe(Schema.optional),
family: ModelV2.Family.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),
request: Schema.Struct({
...Request.fields,
variant: Schema.String.pipe(Schema.optional),
}).pipe(Schema.optional),
variants: Schema.Struct({
id: ModelV2.VariantID,
...Request.fields,
...Overlays,
}).pipe(Schema.Array, Schema.optional),
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional),
@@ -65,7 +54,7 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
name: Schema.String.pipe(Schema.optional),
env: Schema.String.pipe(Schema.Array, Schema.optional),
api: ProviderV2.Api.pipe(Schema.optional),
request: Request.pipe(Schema.optional),
package: Schema.String.pipe(Schema.optional),
...Overlays,
models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
}) {}
+2 -7
View File
@@ -1,6 +1,6 @@
import { Types } from "effect"
import { Model } from "@opencode-ai/schema/model"
import { ProviderV2 } from "./provider"
import type { DeepMutable } from "./schema"
export const ID = Model.ID
export type ID = typeof ID.Type
@@ -20,15 +20,10 @@ export const Cost = Model.Cost
export const Ref = Model.Ref
export type Ref = typeof Ref.Type
export const Api = Model.Api
export type Api = Model.Api
export const Info = Model.Info
export type Info = Model.Info
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
api: ProviderV2.MutableApi<Api>
}
export type MutableInfo = DeepMutable<Info>
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/")
+4 -24
View File
@@ -75,17 +75,8 @@ export const ModelsDevPlugin = define({
const providerID = ProviderV2.ID.make(item.id)
catalog.provider.update(providerID, (provider) => {
provider.name = item.name
provider.api = item.npm
? {
type: "aisdk",
package: item.npm,
url: item.api,
}
: {
type: "native",
url: item.api,
settings: {},
}
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
})
for (const model of Object.values(item.models)) {
@@ -93,19 +84,8 @@ export const ModelsDevPlugin = define({
catalog.model.update(providerID, modelID, (draft) => {
draft.name = model.name
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
draft.api = model.provider?.npm
? {
id: draft.api.id,
type: "aisdk",
package: model.provider?.npm,
url: model.provider.api,
}
: {
id: draft.api.id,
type: "native",
url: model.provider?.api,
settings: {},
}
draft.package = model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined
draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
draft.capabilities = {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
@@ -65,15 +65,14 @@ export const AmazonBedrockPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return
if (typeof provider.request.body.endpoint !== "string") return
if (typeof provider.settings?.endpoint !== "string") return
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
provider.api.url = provider.request.body.endpoint
delete provider.request.body.endpoint
provider.settings.baseURL = provider.settings.endpoint
delete provider.settings.endpoint
})
}
}),
@@ -114,12 +113,15 @@ export const AmazonBedrockPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
if (
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
}
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))
}),
)
}),
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const AnthropicPlugin = define({
id: "anthropic",
@@ -7,11 +8,13 @@ export const AnthropicPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["anthropic-beta"] =
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
provider.headers = {
...provider.headers,
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
}
})
}
}),
+21 -10
View File
@@ -16,14 +16,14 @@ export const AzurePlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/azure") continue
const configured = item.provider.request.body.resourceName
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue
const configured = item.provider.settings?.resourceName
const resourceName =
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
if (!resourceName) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.body.resourceName = resourceName
provider.settings = { ...provider.settings, resourceName }
})
}
}),
@@ -35,7 +35,7 @@ export const AzurePlugin = define({
if (
!evt.options.resourceName &&
!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(
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
@@ -49,7 +49,11 @@ export const AzurePlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
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),
)
}),
)
}),
@@ -63,11 +67,14 @@ export const AzureCognitiveServicesPlugin = define({
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!item.provider.id.includes("azure-cognitive-services")) continue
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`,
}
})
}
}),
@@ -75,7 +82,11 @@ export const AzureCognitiveServicesPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
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),
)
}),
)
}),
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const CerebrasPlugin = define({
id: "cerebras",
@@ -7,10 +8,10 @@ export const CerebrasPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
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" }
})
}
}),
@@ -14,10 +14,10 @@ export const CloudflareWorkersAIPlugin = define({
const item = evt.provider.get(providerID)
if (!item) return
evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return
if (provider.api.url) return
const accountId = resolveAccountId(provider.request.body)
if (accountId) provider.api.url = workersEndpoint(accountId)
if (!ProviderV2.isAISDK(provider.package)) return
if (typeof provider.settings?.baseURL === "string") return
const accountId = resolveAccountId(provider.settings ?? {})
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
})
}),
)
@@ -27,7 +27,7 @@ export const CloudflareWorkersAIPlugin = define({
if (evt.package !== "@ai-sdk/openai-compatible") return
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"))
evt.sdk = mod.createOpenAICompatible(
sdkOptions({
@@ -40,7 +40,7 @@ export const CloudflareWorkersAIPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
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)
}),
)
}),
@@ -54,8 +54,11 @@ function workersEndpoint(accountId: string) {
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
}
function hasWorkersEndpoint(api: ProviderV2.Api) {
return api.type === "aisdk" && Boolean(api.url)
function hasWorkersEndpoint(model: {
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>) {
@@ -36,12 +36,11 @@ export const GithubCopilotPlugin = define({
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
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
}
evt.language = shouldUseResponses(evt.model.api.id)
? evt.sdk.responses(evt.model.api.id)
: evt.sdk.chat(evt.model.api.id)
const id = evt.model.modelID ?? evt.model.id
evt.language = shouldUseResponses(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
}),
)
}),
+10 -12
View File
@@ -36,26 +36,24 @@ export const GitLabPlugin = define({
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
const 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 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 =
typeof evt.model.request.body.workflowDefinition === "string"
? evt.model.request.body.workflowDefinition
typeof evt.model.settings?.workflowDefinition === "string"
? evt.model.settings.workflowDefinition
: undefined
const language = evt.sdk.workflowChat(
gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow",
{
featureFlags,
workflowDefinition,
},
)
const language = evt.sdk.workflowChat(gitlab.isWorkflowModel(id) ? id : "duo-workflow", {
featureFlags,
workflowDefinition,
})
if (workflowRef) language.selectedModelRef = workflowRef
evt.language = language
return
}
evt.language = evt.sdk.agenticChat(evt.model.api.id, {
evt.language = evt.sdk.agenticChat(id, {
aiGatewayHeaders: evt.options.aiGatewayHeaders,
featureFlags,
})
@@ -60,25 +60,28 @@ export const GoogleVertexPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
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" &&
ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
!(
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
const project = resolveProject(item.provider.request.body)
const location = String(resolveLocation(item.provider.request.body))
const project = resolveProject(item.provider.settings ?? {})
const location = String(resolveLocation(item.provider.settings ?? {}))
evt.provider.update(item.provider.id, (provider) => {
if (project) provider.request.body.project = project
provider.request.body.location = location
if (provider.api.type === "aisdk" && provider.api.url) {
provider.api.url = replaceVertexVars(provider.api.url, project, location)
}
if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) {
provider.request.body.fetch = authFetch(provider.request.body.fetch)
provider.settings = {
...provider.settings,
...(project ? { project } : {}),
location,
...(typeof provider.settings?.baseURL === "string"
? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
: {}),
...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
? { fetch: authFetch(provider.settings?.fetch) }
: {}),
}
})
}
@@ -106,7 +109,7 @@ export const GoogleVertexPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
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())
}),
)
}),
@@ -118,21 +121,20 @@ export const GoogleVertexAnthropicPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
const project =
item.provider.request.body.project ??
item.provider.settings?.project ??
process.env.GOOGLE_CLOUD_PROJECT ??
process.env.GCP_PROJECT ??
process.env.GCLOUD_PROJECT
const location =
item.provider.request.body.location ??
item.provider.settings?.location ??
process.env.GOOGLE_CLOUD_LOCATION ??
process.env.VERTEX_LOCATION ??
"global"
evt.provider.update(item.provider.id, (provider) => {
if (project) provider.request.body.project = project
provider.request.body.location = location
provider.settings = { ...provider.settings, ...(project ? { project } : {}), location }
})
}
}),
@@ -166,7 +168,7 @@ export const GoogleVertexAnthropicPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
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())
}),
)
}),
+5 -5
View File
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const KiloPlugin = define({
id: "kilo",
@@ -7,12 +8,11 @@ export const KiloPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
})
}
}),
@@ -1,6 +1,7 @@
import { Effect } from "effect"
import { define } from "../internal"
import { Integration } from "../../integration"
import { ProviderV2 } from "../../provider"
export const LLMGatewayPlugin = define({
id: "llmgateway",
@@ -10,14 +11,17 @@ export const LLMGatewayPlugin = define({
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.disabled) continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-Source"] = "opencode"
provider.headers = {
...provider.headers,
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-Source": "opencode",
}
})
}
}),
+10 -6
View File
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const NvidiaPlugin = define({
id: "nvidia",
@@ -7,13 +8,16 @@ export const NvidiaPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode"
provider.headers = {
...provider.headers,
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": provider.headers?.["X-BILLING-INVOKE-ORIGIN"] ?? "OpenCode",
}
})
}
}),
+3 -3
View File
@@ -156,8 +156,8 @@ export const OpenAIPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") 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) => {
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
@@ -177,7 +177,7 @@ export const OpenAIPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
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)
}),
)
}),
+15 -21
View File
@@ -111,40 +111,34 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
catalog.provider.update(providerID, (provider) => {
provider.integrationID = Integration.ID.make("opencode")
if (item.name !== undefined) provider.name = item.name
provider.api = item.npm
? { type: "aisdk", package: item.npm, url: item.api }
: { type: "native", url: item.api, settings: {} }
Object.assign(provider.request.headers, item.options?.headers)
Object.assign(provider.request.body, withoutCredentials(item.options))
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
provider.settings = {
...provider.settings,
...withoutCredentials(item.options),
...(item.api ? { baseURL: item.api } : {}),
}
provider.headers = { ...provider.headers, ...item.options?.headers }
})
for (const [modelID, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, modelID, (model) => {
if (config.family !== undefined) model.family = config.family
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) {
model.api = config.provider.npm
? {
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: {} }
model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
}
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?.output !== undefined) model.capabilities.output = [...config.modalities.output]
const packageName = config.provider?.npm ?? item.npm
const lowerer = ConfigProviderOptionsV1.get(packageName)
Object.assign(model.request.headers, config.headers)
Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options)))
model.headers = { ...model.headers, ...config.headers }
model.settings = { ...model.settings, ...ConfigProviderOptionsV1.model(withoutCredentials(config.options)) }
if (config.variants !== undefined) {
model.variants = Object.entries(config.variants).map(([id, options]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(options.headers ?? {}) },
body: lowerer.request(withoutCredentials(options)),
settings: ConfigProviderOptionsV1.model(withoutCredentials(options)),
}))
}
if (config.release_date !== undefined) {
@@ -163,9 +157,9 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
const item = catalog.provider.get(ProviderV2.ID.opencode)
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) => {
if (!hasKey) provider.request.body.apiKey = "public"
if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
})
if (hasKey) return
for (const model of item.models.values()) {
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { define } from "../internal"
export const OpenRouterPlugin = define({
@@ -8,11 +9,10 @@ export const OpenRouterPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
})
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
if (!item.models.has(modelID)) continue
@@ -39,7 +39,7 @@ export const SapAICorePlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
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)
}),
)
}),
+4 -4
View File
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const VercelPlugin = define({
id: "vercel",
@@ -7,11 +8,10 @@ export const VercelPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/vercel") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["http-referer"] = "https://opencode.ai/"
provider.request.headers["x-title"] = "opencode"
provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
})
}
}),
+1 -1
View File
@@ -15,7 +15,7 @@ export const XAIPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
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)
}),
)
}),
+9 -5
View File
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const ZenmuxPlugin = define({
id: "zenmux",
@@ -7,12 +8,15 @@ export const ZenmuxPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/"
provider.request.headers["X-Title"] ??= "opencode"
provider.headers = {
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
...provider.headers,
}
})
}
}),
+15 -11
View File
@@ -1,7 +1,8 @@
export * as VariantPlugin from "./variant"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect } from "effect"
import { ModelV2 } from "../model"
import { ProviderV2 } from "../provider"
import { define } from "./internal"
export const Plugin = define({
@@ -11,14 +12,14 @@ export const Plugin = define({
for (const record of catalog.provider.list()) {
for (const model of record.models.values()) {
catalog.model.update(model.providerID, model.id, (draft) => {
const generated = generate(draft)
const generated = generate(draft as ModelV2.Info, record.provider)
if (generated.length === 0) return
const explicit = new Map(draft.variants.map((variant) => [variant.id, variant]))
const generatedIDs = new Set(generated.map((variant) => variant.id))
const explicit = new Map((draft.variants ?? []).map((variant) => [variant.id, variant]))
const generatedIDs = new Set<string>(generated.map((variant) => variant.id))
draft.variants = [
...generated.map((variant) => explicit.get(variant.id) ?? variant),
...draft.variants.filter((variant) => !generatedIDs.has(variant.id)),
...(draft.variants ?? []).filter((variant) => !generatedIDs.has(variant.id)),
]
})
}
@@ -27,13 +28,16 @@ export const Plugin = define({
}),
})
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.api.id}`.toLowerCase()
export function generate(
model: ModelV2.Info,
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 []
return ["high", "max"].map((id) => ({
id,
headers: {},
body: { reasoning_effort: id },
id: ModelV2.VariantID.make(id),
settings: { reasoningEffort: id },
}))
}
+120 -9
View File
@@ -1,20 +1,91 @@
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 type { ProviderPackageDefinition } from "@opencode-ai/llm"
import { Npm } from "./npm"
import type { DeepMutable } from "./schema"
export const ID = Provider.ID
export type ID = typeof ID.Type
export const AISDK = Provider.AISDK
export const AISDK_PREFIX = "aisdk:"
export const isAISDK = (packageName: string | undefined) => packageName?.startsWith(AISDK_PREFIX) ?? false
export const aisdk = (packageName: string) => (isAISDK(packageName) ? packageName : `${AISDK_PREFIX}${packageName}`)
export const packageName = (packageName: string | undefined) =>
isAISDK(packageName) ? packageName!.slice(AISDK_PREFIX.length) : packageName
export const Native = Provider.Native
export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.LoadError", {
package: Schema.String,
cause: Schema.Defect(),
}) {}
export type ProviderPackage = ProviderPackageDefinition
export const Api = Provider.Api
export type Api = Provider.Api
export type MutableApi<T extends Api = Api> = T extends Api
? Omit<Types.DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any })
: never
const packages = new Map<string, Promise<unknown>>()
export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
const resolved = yield* resolvePackage(specifier)
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 installed = yield* npm
.add(rootPackage(specifier))
.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, unknown> | undefined {
if (base === undefined) return overlay && { ...overlay }
if (overlay === undefined) return { ...base }
return Object.fromEntries(
new Set([...Object.keys(base), ...Object.keys(overlay)]).values().map((key) => {
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 type Request = Provider.Request
@@ -22,4 +93,44 @@ export type Request = Provider.Request
export const Info = Provider.Info
export type Info = Provider.Info
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
export type MutableInfo = DeepMutable<Info>
const resolvePackage = (specifier: string) =>
Effect.sync(() => {
if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")) return specifier
try {
return import.meta.resolve(specifier)
} catch {
return undefined
}
})
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) {
const module = yield* Effect.tryPromise({
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 (!isProviderPackage(module)) {
return yield* new LoadError({
package: specifier,
cause: new Error(`Provider package ${specifier} does not export model(modelID, settings)`),
})
}
return module
})
function isProviderPackage(input: unknown): input is ProviderPackageDefinition {
return typeof input === "object" && input !== null && "model" in input && typeof input.model === "function"
}
function rootPackage(specifier: string) {
const parts = specifier.split("/")
if (specifier.startsWith("@")) return parts.slice(0, 2).join("/")
return parts[0]
}
+78 -73
View File
@@ -1,16 +1,14 @@
export * as SessionRunnerModel from "./model"
import { type Model } from "@opencode-ai/llm"
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
import { Model } from "@opencode-ai/llm"
import { Context, Effect, Layer, Schema } from "effect"
import { produce } from "immer"
import { AISDK } from "../../aisdk"
import { Catalog } from "../../catalog"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { Npm } from "../../npm"
import { ProviderV2 } from "../../provider"
import { SessionSchema } from "../schema"
@@ -50,16 +48,16 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
}
}
export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()(
"SessionRunnerModel.UnsupportedApiError",
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedPackageError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
api: Schema.String,
package: Schema.String,
},
) {
override get message() {
return `Unsupported API for ${this.providerID}/${this.modelID}: ${this.api}`
return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
}
}
@@ -67,7 +65,7 @@ export type Error =
| ModelNotSelectedError
| ModelUnavailableError
| VariantUnavailableError
| UnsupportedApiError
| UnsupportedPackageError
| Integration.AuthorizationError
export interface Interface {
@@ -79,33 +77,12 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
/** Test or embedding seam for supplying a model resolver directly. */
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
if (credential?.type === "key") return Auth.value(credential.key)
if (credential?.type === "oauth") return Auth.value(credential.access)
const value = model.request.body.apiKey ?? model.api.settings?.apiKey
if (typeof value === "string") return Auth.value(value)
}
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
const body = model.request.body
const httpBody = Object.hasOwn(body, "apiKey")
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
: body
return route.with({
provider: model.providerID,
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
headers: model.request.headers,
http: { body: httpBody },
limits: { context: model.limit.context, output: model.limit.output },
})
}
const withVariant = (
model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
const id = variantID === "default" || variantID === undefined ? model.request.variant : variantID
const variant = model.variants.find((item) => item.id === id)
const id = variantID === "default" ? undefined : variantID
const variant = model.variants?.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default")
return Effect.fail(
new VariantUnavailableError({
@@ -117,65 +94,87 @@ const withVariant = (
return Effect.succeed(
variant
? produce(model, (draft) => {
Object.assign(draft.request.headers, variant.headers)
Object.assign(draft.request.body, variant.body)
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
})
: model,
)
}
const apiName = (model: ModelV2.Info) =>
model.api.type === "aisdk" ? `${model.api.type}:${model.api.package}` : model.api.type
export interface Dependencies {
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
}
const unsupported = (model: ModelV2.Info, packageName = model.package ?? "unknown") =>
new UnsupportedPackageError({
providerID: model.providerID,
modelID: model.id,
package: packageName,
})
const credentialSettings = (credential: Credential.Value | undefined) => ({
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
...credential?.metadata,
})
export const fromCatalogModel = (
model: ModelV2.Info,
credential?: Credential.Value,
): Effect.Effect<Model, UnsupportedApiError> => {
dependencies: Dependencies = {},
): Effect.Effect<Model, UnsupportedPackageError> => {
const resolved =
credential?.metadata === undefined
? model
: produce(model, (draft) => {
Object.assign(draft.request.body, credential.metadata)
})
const key = apiKey(resolved, credential)
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
return Effect.succeed(
withDefaults(resolved, OpenAIResponses.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.api.id }),
draft.settings = ProviderV2.mergeOverlay(draft.settings, credential.metadata)
})
if (ProviderV2.isAISDK(resolved.package)) {
if (!dependencies.loadAISDK) {
return Effect.fail(unsupported(resolved))
}
const runtime = produce(resolved, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, credentialSettings(credential))
})
return dependencies.loadAISDK(runtime).pipe(
Effect.mapError(() => unsupported(resolved)),
)
}
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/anthropic") {
return Effect.succeed(
withDefaults(resolved, AnthropicMessages.route)
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
.model({ id: resolved.api.id }),
)
if (resolved.package) {
const specifier = resolved.package
return Effect.gen(function* () {
const module = yield* (dependencies.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe(
Effect.mapError(() => unsupported(resolved, specifier)),
)
const settings = {
...resolved.settings,
...credentialSettings(credential),
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, specifier),
})
})
}
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai-compatible" && resolved.api.url) {
return Effect.succeed(
withDefaults(resolved, OpenAICompatibleChat.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.api.id }),
)
}
return Effect.fail(
new UnsupportedApiError({
providerID: resolved.providerID,
modelID: resolved.id,
api: apiName(resolved),
}),
)
return Effect.fail(unsupported(resolved))
}
export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
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) =>
model.api.type === "aisdk" &&
(model.api.package === "@ai-sdk/openai" ||
model.api.package === "@ai-sdk/anthropic" ||
(model.api.package === "@ai-sdk/openai-compatible" && model.api.url !== undefined))
export const supported = (model: ModelV2.Info) => Boolean(model.package)
/** Resolves models from the catalog belonging to the current Location runtime. */
export const locationLayer = Layer.effect(
@@ -183,6 +182,8 @@ export const locationLayer = Layer.effect(
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const npm = yield* Npm.Service
const aisdk = yield* AISDK.Service
return Service.of({
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
// Location plugins populate and filter the catalog asynchronously during layer startup.
@@ -208,6 +209,10 @@ export const locationLayer = Layer.effect(
session,
selected,
connection ? yield* integrations.connection.resolve(connection) : undefined,
{
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
loadAISDK: (model) => aisdk.model(model),
},
)
}),
})
+14 -32
View File
@@ -6,6 +6,7 @@ import { ConfigMCPV1 } from "./mcp"
import { ConfigPermissionV1 } from "./permission"
import { ConfigProviderV1 } from "./provider"
import { ConfigProviderOptionsV1 } from "./provider-options"
import { ProviderV2 } from "../../provider"
const keys = new Set([
"logLevel",
@@ -168,30 +169,22 @@ function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
}
function migrateProvider(info: ConfigProviderV1.Info) {
const lowerer = ConfigProviderOptionsV1.get(info.npm)
const options = lowerer.provider(info.options ?? {})
const options = ConfigProviderOptionsV1.provider(info.options ?? {})
return {
name: info.name,
env: info.env,
api: info.npm
? {
type: "aisdk" as const,
package: info.npm,
url: info.api ?? options.url,
settings: options.settings ?? {},
}
: undefined,
request: info.options && { headers: options.headers, body: options.body },
package: info.npm ? ProviderV2.aisdk(info.npm) : undefined,
settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings,
headers: info.options && options.headers,
body: info.options && options.body,
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) {
const packageID = info.provider?.npm ?? packageName
const lowerer = ConfigProviderOptionsV1.get(packageID)
const request = info.options && lowerer.request(info.options)
function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
const settings = info.options && ConfigProviderOptionsV1.model(info.options)
const costs = info.cost && [
{
input: info.cost.input,
@@ -214,29 +207,18 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
: undefined
return {
modelID: info.id,
family: info.family,
name: info.name,
api: info.provider?.npm
? {
...(info.id === undefined ? {} : { id: info.id }),
type: "aisdk" as const,
package: info.provider.npm,
url: info.provider.api,
settings: {},
}
: info.id === undefined
? undefined
: { id: info.id },
package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined,
settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
capabilities,
request: (info.headers || request) && {
headers: info.headers,
body: request,
},
headers: info.headers,
variants:
info.variants &&
Object.entries(info.variants).map(([id, options]) => ({
id,
body: lowerer.request(options),
settings: ConfigProviderOptionsV1.model(options),
})),
cost: costs,
disabled: info.status === "deprecated" ? true : undefined,
+15 -203
View File
@@ -3,216 +3,28 @@ export * as ConfigProviderOptionsV1 from "./provider-options"
type Options = Readonly<Record<string, unknown>>
export interface ProviderResult {
readonly settings: Record<string, unknown>
readonly headers?: Record<string, string>
readonly body?: Record<string, unknown>
readonly url?: string
readonly settings?: Record<string, unknown>
}
export interface Lowerer {
readonly provider: (options: Options) => ProviderResult
readonly request: (options: Options) => Record<string, unknown>
}
export function get(packageName?: string): Lowerer {
const key = packageName ?? ""
return Object.hasOwn(lowerers, key) ? lowerers[key]! : raw
}
const raw: Lowerer = {
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.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 {
export function provider(options: Options): ProviderResult {
const headers = options.headers
const body = options.body
const entries = Object.entries(options)
const settings = Object.fromEntries(entries.filter(([key]) => key !== "headers" && key !== "body"))
const headerOverlay =
typeof headers === "object" && headers !== null && !Array.isArray(headers)
? Object.fromEntries(Object.entries(headers).filter((entry): entry is [string, string] => typeof entry[1] === "string"))
: undefined
const bodyOverlay = typeof body === "object" && body !== null && !Array.isArray(body) ? { ...body } : undefined
return {
headers: headers(options.headers),
body: body(options.body),
settings: omit(options, ["headers", "body", ...extraKeys]),
settings,
headers: headerOverlay,
body: bodyOverlay,
}
}
function body(input: unknown) {
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) {
export function model(options: 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)
}
+48 -52
View File
@@ -65,14 +65,14 @@ describe("CatalogV2", () => {
})
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({
integrationID,
label: "Second",
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(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(layer))
})
@@ -135,111 +135,107 @@ describe("CatalogV2", () => {
),
)
it.effect("normalizes provider baseURL into api url", () =>
it.effect("keeps provider API settings flat", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
yield* catalog.transform((catalog) =>
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://default.example.com",
}
provider.request.body.baseURL = "https://override.example.com"
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://override.example.com" }
}),
)
expect(required(yield* catalog.provider.get(providerID)).api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://override.example.com",
expect(required(yield* catalog.provider.get(providerID))).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://override.example.com" },
})
}),
)
it.effect("normalizes model baseURL into api url", () =>
it.effect("resolves model settings and upstream model ID", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://provider.example.com" }
})
catalog.model.update(providerID, modelID, (model) => {
model.api = {
id: modelID,
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://model.example.com",
}
model.request.body.baseURL = "https://override.example.com"
model.modelID = ModelV2.ID.make("upstream-model")
model.settings = { 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,
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://override.example.com",
settings: {},
modelID: ModelV2.ID.make("upstream-model"),
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://override.example.com" },
})
}),
)
it.effect("resolves default model api from provider api", () =>
it.effect("resolves default model transport from provider transport", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://provider.example.com" }
})
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,
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://provider.example.com" },
})
}),
)
it.effect("resolves provider and model request merges", () =>
it.effect("resolves provider and model overlay merges", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.request.headers.provider = "provider"
provider.request.headers.shared = "provider"
provider.request.body.provider = true
provider.headers = { provider: "provider", "X-Shared": "provider" }
provider.body = {
provider: true,
reasoning: { type: "enabled", budget: 8_000 },
stop: ["END"],
disabled: { type: "enabled" },
}
})
catalog.model.update(providerID, modelID, (model) => {
model.request.headers.model = "model"
model.request.headers.shared = "model"
model.request.body.model = true
model.request.body.request = true
model.request.body.shared = "model"
model.headers = { model: "model", "x-shared": "model" }
model.body = {
model: true,
request: true,
reasoning: { budget: 32_000 },
stop: ["STOP"],
disabled: null,
}
})
})
const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
expect(model.request.body).toEqual({ provider: true, model: true, request: true, shared: "model" })
expect(model.headers).toEqual({ provider: "provider", "x-shared": "model", model: "model" })
expect(model.body).toEqual({
provider: true,
model: true,
request: true,
reasoning: { type: "enabled", budget: 32_000 },
stop: ["STOP"],
disabled: null,
})
}),
)
+16 -26
View File
@@ -43,11 +43,7 @@ function testLayer(
}
const provider = {
api: { type: "native", settings: {} },
request: {
headers: {},
body: {},
},
package: "test-provider",
models: {},
}
@@ -93,6 +89,7 @@ describe("Config", () => {
provider: {
bedrock: {
npm: "@ai-sdk/amazon-bedrock",
api: "https://bedrock.example.com",
options: {
headers: { "x-test": "1" },
body: { trace: true },
@@ -103,13 +100,9 @@ describe("Config", () => {
},
})
expect(migrated.providers?.bedrock?.api).toEqual({
type: "aisdk",
package: "@ai-sdk/amazon-bedrock",
url: undefined,
settings: { region: "us-east-1", profile: "dev" },
})
expect(migrated.providers?.bedrock?.request).toEqual({
expect(migrated.providers?.bedrock).toMatchObject({
package: "aisdk:@ai-sdk/amazon-bedrock",
settings: { baseURL: "https://bedrock.example.com", region: "us-east-1", profile: "dev" },
headers: { "x-test": "1" },
body: { trace: true },
})
@@ -585,34 +578,31 @@ describe("Config", () => {
})
expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
expect(documents[0]?.info.providers?.custom).toMatchObject({
request: { body: { apiKey: "secret" } },
settings: { apiKey: "secret" },
models: {
model: {
request: { body: { reasoningEffort: "high" } },
variants: [{ id: "fast", body: { temperature: 0.2 } }],
settings: { reasoningEffort: "high" },
variants: [{ id: "fast", settings: { temperature: 0.2 } }],
},
},
})
expect(documents[0]?.info.providers?.openai).toMatchObject({
api: { settings: {} },
request: { headers: { Authorization: "Bearer secret", "OpenAI-Organization": "org" } },
package: "aisdk:@ai-sdk/openai",
settings: { apiKey: "secret", organization: "org" },
models: {
model: {
request: {
body: { temperature: 0.3, reasoning_effort: "high", service_tier: "priority" },
},
variants: [{ id: "high", body: { reasoning_effort: "high", reasoning_summary: "auto" } }],
settings: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" },
variants: [{ id: "high", settings: { reasoningEffort: "high", reasoningSummary: "auto" } }],
},
},
})
expect(documents[0]?.info.providers?.anthropic).toMatchObject({
models: {
model: {
request: {
body: {
output_config: { effort: "high", task_budget: 4096 },
metadata: { user_id: "user-1" },
},
settings: {
effort: "high",
taskBudget: 4096,
metadata: { userId: "user-1" },
},
},
},
@@ -2,221 +2,47 @@ import { describe, expect, test } from "bun:test"
import { ConfigProviderOptionsV1 } from "@opencode-ai/core/v1/config/provider-options"
describe("ConfigProviderOptionsV1", () => {
test("keeps raw provider and request options unchanged", () => {
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")
test("splits provider overlays without changing package settings", () => {
expect(
lowerer.provider({
ConfigProviderOptionsV1.provider({
apiKey: "secret",
baseURL: "https://openai.example/v1",
organization: "org",
project: "project",
headers: { "x-test": "1" },
headers: { "x-test": "1", invalid: true },
body: { store: true },
timeout: 1000,
nested: { camelCase: true },
}),
).toEqual({
url: "https://openai.example/v1",
headers: {
Authorization: "Bearer secret",
"OpenAI-Organization": "org",
"OpenAI-Project": "project",
"x-test": "1",
settings: {
apiKey: "secret",
baseURL: "https://openai.example/v1",
organization: "org",
nested: { camelCase: true },
},
headers: { "x-test": "1" },
body: { store: true },
settings: { timeout: 1000 },
})
})
test("keeps model and variant options unchanged", () => {
expect(
lowerer.request({
ConfigProviderOptionsV1.model({
reasoningEffort: "high",
textVerbosity: "low",
text: { outputFormat: "plain" },
nestedValue: { camelCase: true },
}),
).toEqual({
reasoning_effort: "high",
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,
metadata: { userId: "user", traceId: "trace" },
nestedValue: { camelCase: true },
metadata: { userId: "user" },
}),
).toEqual({
output_config: { effort: "high", task_budget: 1024 },
metadata: { user_id: "user", trace_id: "trace" },
nested_value: { camel_case: true },
reasoningEffort: "high",
taskBudget: 1024,
metadata: { userId: "user" },
})
})
test("lowers Google provider and request options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/google")
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", textVerbosity: "low" })).toEqual({
reasoning_effort: "high",
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",
test("uses mechanical lowering for custom provider options", () => {
expect(ConfigProviderOptionsV1.provider({ enabled: true })).toEqual({
settings: { enabled: true },
headers: 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" },
})
},
)
})
+24 -25
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)
describe("ConfigProviderPlugin.Plugin", () => {
@@ -68,7 +61,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
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: {
"alpha-gpt-next": {
variants: [
@@ -119,7 +113,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
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)
const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants[0]).toMatchObject({
expect(model.variants?.[0]).toMatchObject({
id: "high",
body: { reasoningEffort: "high" },
})
@@ -169,8 +164,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
custom: {
name: "Configured",
env: ["CUSTOM_API_KEY"],
api: { type: "native", settings: {} },
request: request({ first: "first", shared: "first" }),
package: "native",
headers: { first: "first", shared: "first" },
models: {
chat: {
name: "First",
@@ -178,7 +173,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
disabled: true,
limit: { context: 100, output: 50 },
cost: { input: 1, output: 2 },
request: request({ first: "first", shared: "first" }, "retained"),
settings: { retained: true },
headers: { first: "first", shared: "first" },
variants: [
{
id: "fast",
@@ -197,17 +193,18 @@ describe("ConfigProviderPlugin.Plugin", () => {
model: "custom/default",
providers: {
custom: {
api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" },
request: request({ last: "last", shared: "last" }),
package: "aisdk:custom-sdk",
settings: { baseURL: "https://example.test" },
headers: { last: "last", shared: "last" },
models: {
default: {
name: "Default",
},
chat: {
api: { id: "api-chat" },
modelID: "api-chat",
name: "Last",
limit: { output: 75 },
request: request({ last: "last", shared: "last" }),
headers: { last: "last", shared: "last" },
variants: [
{
id: "fast",
@@ -247,22 +244,24 @@ describe("ConfigProviderPlugin.Plugin", () => {
})
expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
expect(provider.disabled).toBeUndefined()
expect(provider.api).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" })
expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
expect(provider.package).toBe("aisdk:custom-sdk")
expect(provider.settings).toEqual({ baseURL: "https://example.test" })
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.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
expect(model.enabled).toBe(false)
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.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.request.variant).toBe("retained")
expect(model.variants.map((variant) => variant.id)).toEqual([
expect(model.settings).toEqual({ baseURL: "https://example.test", retained: true })
expect(model.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants?.map((variant) => variant.id)).toEqual([
ModelV2.VariantID.make("fast"),
ModelV2.VariantID.make("slow"),
])
expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants[1]?.headers).toEqual({ slow: "slow" })
expect(model.variants?.[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants?.[1]?.headers).toEqual({ slow: "slow" })
}),
),
)
+1 -1
View File
@@ -168,7 +168,7 @@ describe("LocationServiceMap", () => {
providers: {
unavailable: {
name: "Unavailable",
api: { type: "native", settings: {} },
package: "test-provider",
models: { chat: { disabled: true } },
},
},
+11 -12
View File
@@ -118,7 +118,7 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
id: ModelV2.ID.make(current.id),
providerID: ProviderV2.ID.make(current.providerID),
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
variants: current.variants.map((variant) => ({
variants: current.variants?.map((variant) => ({
...variant,
id: ModelV2.VariantID.make(variant.id),
})),
@@ -272,29 +272,28 @@ function agentInfo(value: AgentV2.Info) {
function providerInfo(value: ProviderV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
settings: value.settings && { ...value.settings },
headers: value.headers && { ...value.headers },
body: value.body && { ...value.body },
}
}
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
return {
...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: {
...value.capabilities,
input: [...value.capabilities.input],
output: [...value.capabilities.output],
},
request: {
...value.request,
headers: { ...value.request.headers },
body: { ...value.request.body },
},
variants: value.variants.map((variant) => ({
variants: value.variants?.map((variant) => ({
...variant,
headers: { ...variant.headers },
body: { ...variant.body },
settings: variant.settings && { ...variant.settings },
headers: variant.headers && { ...variant.headers },
body: variant.body && { ...variant.body },
})),
time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
@@ -28,7 +28,8 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "alibaba" },
@@ -45,7 +46,8 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "alibaba" },
@@ -62,7 +64,8 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
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* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const item = ModelV2.Info.make({
...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 language = result.sdk?.languageModel(item.api.id)
const language = result.sdk?.languageModel(item.modelID ?? item.id)
expect(language?.modelId).toBe("qwen-plus")
expect(language?.provider).toBe("alibaba.chat")
}),
@@ -79,31 +79,30 @@ function openAIUrl(language: unknown, path: string, modelId: string) {
}
describe("AmazonBedrockPlugin", () => {
it.effect("moves endpoint option to api URL", () =>
it.effect("moves endpoint option to baseURL", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
const bedrock = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
request: {
headers: {},
body: { endpoint: "https://bedrock.example" },
},
package: "aisdk:@ai-sdk/amazon-bedrock",
headers: {},
settings: { endpoint: "https://bedrock.example" },
})
catalog.provider.update(bedrock.id, (item) => {
item.api = bedrock.api
item.request = bedrock.request
item.package = bedrock.package
item.package = bedrock.package
item.headers = bedrock.headers
item.settings = { endpoint: "https://bedrock.example" }
})
})
yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
expect(result.api).toEqual({
type: "aisdk",
package: "@ai-sdk/amazon-bedrock",
url: "https://bedrock.example",
expect(result).toMatchObject({
package: "aisdk:@ai-sdk/amazon-bedrock",
settings: { baseURL: "https://bedrock.example" },
})
expect(result.request.body.endpoint).toBeUndefined()
expect(result.settings?.endpoint).toBeUndefined()
}),
)
@@ -116,7 +115,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -141,7 +141,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -175,11 +176,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -199,7 +197,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock", region: "eu-west-1" },
@@ -218,7 +217,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -237,7 +237,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -257,7 +258,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -286,7 +288,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -314,11 +317,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
api: {
id: ModelV2.ID.make("openai.gpt-5.5"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
modelID: ModelV2.ID.make("openai.gpt-5.5"),
package: "aisdk:@ai-sdk/amazon-bedrock/mantle",
}),
package: "@ai-sdk/amazon-bedrock/mantle",
options: {
@@ -345,11 +345,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
api: {
id: ModelV2.ID.make("openai.gpt-5.5"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
modelID: ModelV2.ID.make("openai.gpt-5.5"),
package: "aisdk:@ai-sdk/amazon-bedrock/mantle",
}),
sdk: fakeSelectorSdk(calls),
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
@@ -357,11 +354,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
api: {
id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
package: "aisdk:@ai-sdk/amazon-bedrock/mantle",
}),
sdk: fakeSelectorSdk(calls),
options: { region: "us-east-1" },
@@ -378,11 +372,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "@ai-sdk/amazon-bedrock/anthropic",
},
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:@ai-sdk/amazon-bedrock/anthropic",
}),
package: "@ai-sdk/amazon-bedrock/anthropic",
options: { name: "amazon-bedrock" },
@@ -409,11 +400,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -444,7 +432,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -452,7 +441,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -460,11 +450,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
api: {
id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -472,7 +459,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-northeast-1" },
@@ -480,7 +468,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-southeast-2" },
@@ -505,7 +494,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -591,7 +581,8 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: item.region },
@@ -610,7 +601,8 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -31,19 +31,21 @@ describe("AnthropicPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
api: { type: "aisdk", package: "@ai-sdk/anthropic" },
request: { headers: { Existing: "1" }, body: {} },
package: "aisdk:@ai-sdk/anthropic",
headers: { Existing: "1" },
settings: {},
})
catalog.provider.update(item.id, (draft) => {
draft.api = item.api
draft.request = item.request
draft.package = item.package
draft.package = item.package
draft.headers = item.headers
})
})
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",
)
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 +54,7 @@ describe("AnthropicPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {}))
yield* addPlugin()
expect(
required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined()
}),
)
@@ -66,7 +66,8 @@ describe("AnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:@ai-sdk/anthropic",
}),
package: "@ai-sdk/anthropic",
options: { name: "custom-anthropic", apiKey: "test" },
@@ -83,7 +84,8 @@ describe("AnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:@ai-sdk/anthropic",
}),
package: "@ai-sdk/anthropic",
options: { name: "anthropic", apiKey: "test" },
@@ -66,18 +66,16 @@ describe("AzureCognitiveServicesPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
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()
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
expect(result.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://cognitive.cognitiveservices.azure.com/openai",
expect(result).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://cognitive.cognitiveservices.azure.com/openai" },
})
expect(result.request.body.baseURL).toBeUndefined()
expect(result.request.body.resourceName).toBeUndefined()
expect(result.settings?.resourceName).toBeUndefined()
}),
),
)
@@ -89,26 +87,28 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...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({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "test-provider" },
package: "aisdk:test-provider",
})
catalog.provider.update(azure.id, (item) => {
item.api = azure.api
item.package = azure.package
item.package = azure.package
})
catalog.provider.update(openai.id, (item) => {
item.api = openai.api
item.package = openai.package
item.package = openai.package
})
})
yield* addPlugin()
const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
expect(azure.request.body.baseURL).toBeUndefined()
expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
expect(openai.request.body.baseURL).toBeUndefined()
expect(openai.api).toEqual({ type: "aisdk", package: "test-provider" })
expect(azure.settings?.baseURL).toBeUndefined()
expect(azure).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible" })
expect(openai.settings?.baseURL).toBeUndefined()
expect(openai).toMatchObject({ package: "aisdk:test-provider" })
}),
),
)
@@ -122,7 +122,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: { useCompletionUrls: true },
@@ -140,7 +141,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -148,7 +150,8 @@ describe("AzureCognitiveServicesPlugin", () => {
const ignored = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -168,7 +171,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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 },
options: {},
@@ -176,7 +180,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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 },
options: {},
@@ -184,7 +189,8 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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 },
options: {},
@@ -66,11 +66,11 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
item.package = ProviderV2.aisdk("@ai-sdk/azure")
})
})
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,21 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "from-config" } },
package: "aisdk:@ai-sdk/azure",
headers: {},
settings: { resourceName: "from-config" },
})
catalog.provider.update(azure.id, (item) => {
item.api = azure.api
item.request = azure.request
item.package = azure.package
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: "from-config" }
})
catalog.provider.update(ProviderV2.ID.openai, () => {})
})
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.openai)).request.body.resourceName).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-config")
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.resourceName).toBeUndefined()
}),
),
)
@@ -105,16 +108,19 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "" } },
package: "aisdk:@ai-sdk/azure",
headers: {},
settings: { resourceName: "" },
})
catalog.provider.update(azure.id, (item) => {
item.api = azure.api
item.request = azure.request
item.package = azure.package
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: "" }
})
})
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 +132,19 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: " " } },
package: "aisdk:@ai-sdk/azure",
headers: {},
settings: { resourceName: " " },
})
catalog.provider.update(azure.id, (item) => {
item.api = azure.api
item.request = azure.request
item.package = azure.package
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: " " }
})
})
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 +158,8 @@ describe("AzurePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/azure",
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
@@ -168,7 +178,8 @@ describe("AzurePlugin", () => {
.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/azure",
options: { name: "azure" },
@@ -188,7 +199,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
@@ -206,7 +218,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
@@ -224,8 +237,10 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
request: { headers: {}, body: { useCompletionUrls: true } },
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
headers: {},
settings: { useCompletionUrls: true },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -243,7 +258,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -251,7 +267,8 @@ describe("AzurePlugin", () => {
const ignored = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -274,7 +291,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
options: {},
@@ -282,7 +300,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: { languageModel: make("languageModel") },
options: {},
@@ -36,12 +36,12 @@ describe("CerebrasPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
item.request.headers.Existing = "1"
item.package = ProviderV2.aisdk("@ai-sdk/cerebras")
item.headers = { ...item.headers, Existing: "1" }
})
})
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",
"X-Cerebras-3rd-Party-Integration": "opencode",
})
@@ -53,7 +53,7 @@ describe("CerebrasPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
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"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/cerebras",
options: { name: "custom-cerebras", apiKey: "test" },
@@ -95,11 +92,8 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/cerebras",
options: { name: "configured-cerebras", apiKey: "test" },
@@ -120,11 +114,8 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/groq",
options: { name: "custom-cerebras", apiKey: "test" },
@@ -118,7 +118,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cloudflare-ai-gateway" },
@@ -139,7 +140,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {
@@ -183,7 +185,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {
@@ -212,7 +215,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {
@@ -249,7 +253,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {
@@ -280,7 +285,8 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cloudflare-ai-gateway" },
@@ -302,7 +308,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cloudflare-ai-gateway" },
@@ -325,7 +332,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cloudflare-ai-gateway" },
@@ -354,7 +362,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
@@ -380,11 +389,8 @@ describe("CloudflareAIGatewayPlugin", () => {
ProviderV2.ID.make("cloudflare-ai-gateway"),
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: "aisdk:test-provider",
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -412,7 +418,8 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cloudflare-ai-gateway" },
@@ -87,7 +87,7 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
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()
@@ -95,15 +95,16 @@ describe("CloudflareWorkersAIPlugin", () => {
const sdk = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
})
expect(provider.api).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1",
expect(provider).toMatchObject({
package: "aisdk:test-provider",
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" },
})
expect(sdk.sdk).toBeDefined()
}),
@@ -116,14 +117,14 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
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()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://proxy.example/v1",
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
package: "aisdk:test-provider",
settings: { baseURL: "https://proxy.example/v1" },
})
}),
),
@@ -138,12 +139,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
modelID: ModelV2.ID.make("@cf/model"),
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://proxy.example/v1" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
@@ -159,15 +157,14 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider" }
provider.request.body.accountId = "configured-acct"
provider.package = ProviderV2.aisdk("test-provider")
provider.settings = { ...provider.settings, accountId: "configured-acct" }
}),
)
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
package: "aisdk:test-provider",
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" },
})
}),
),
@@ -182,12 +179,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
modelID: ModelV2.ID.make("@cf/model"),
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://proxy.example/v1" },
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -214,12 +208,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
},
modelID: ModelV2.ID.make("@cf/model"),
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" },
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -243,7 +234,8 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -262,12 +254,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/anthropic",
url: "https://proxy.example/v1",
},
modelID: ModelV2.ID.make("@cf/model"),
package: "aisdk:@ai-sdk/anthropic",
settings: { baseURL: "https://proxy.example/v1" },
}),
package: "@ai-sdk/anthropic",
options: { name: "cloudflare-workers-ai" },
@@ -56,7 +56,8 @@ describe("CoherePlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cohere" },
@@ -66,7 +67,8 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cohere" },
@@ -83,7 +85,8 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
@@ -108,7 +111,8 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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,
options: {},
@@ -48,7 +48,8 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "deepinfra" },
@@ -66,7 +67,8 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-deepinfra", apiKey: "test" },
@@ -85,7 +87,8 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "deepinfra", apiKey: "test" },
@@ -111,7 +114,8 @@ describe("DeepInfraPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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,
options: { name: "deepinfra" },
@@ -122,7 +126,8 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "deepinfra" },
@@ -141,17 +146,14 @@ describe("DeepInfraPlugin", () => {
const sdkEvent = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")),
api: {
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
type: "aisdk",
package: "@ai-sdk/deepinfra",
},
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
package: "aisdk:@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
})
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(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
}),
@@ -53,7 +53,8 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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,
options: { name: "custom", marker: "dynamic" },
@@ -71,7 +72,8 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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,
options: { name: "custom", marker: "dynamic" },
@@ -88,7 +90,8 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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,
options: { name: "custom-provider", marker: "dynamic" },
@@ -104,7 +107,8 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "npm-provider", marker: "npm" },
@@ -121,7 +125,8 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...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)
@@ -138,7 +143,8 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...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)
@@ -157,7 +163,8 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...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)
@@ -166,7 +173,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* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -174,7 +181,8 @@ describe("DynamicProviderPlugin", () => {
const language = yield* aisdk.language(
ModelV2.Info.make({
...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" } })
@@ -45,7 +45,8 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "gateway" },
@@ -65,11 +66,8 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")),
api: {
id: ModelV2.ID.make("anthropic/claude-sonnet-4"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"),
package: "aisdk:test-provider",
}),
package: "@ai-sdk/gateway",
options: { name: "vercel", apiKey: "test-key" },
@@ -91,7 +89,8 @@ describe("GatewayPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "vercel" },
@@ -101,7 +100,8 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "vercel" },
@@ -47,7 +47,8 @@ describe("GithubCopilotPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "github-copilot" },
@@ -55,7 +56,8 @@ describe("GithubCopilotPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "github-copilot" },
@@ -74,7 +76,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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 },
options: {},
@@ -92,7 +95,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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 },
options: {},
@@ -110,7 +114,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -118,7 +123,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -126,7 +132,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -134,7 +141,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -142,7 +150,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -166,7 +175,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -174,7 +184,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -182,7 +193,8 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -230,7 +242,8 @@ describe("GithubCopilotPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -70,7 +70,8 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "gitlab" },
@@ -107,7 +108,8 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "gitlab" },
@@ -132,7 +134,8 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {
@@ -173,7 +176,8 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "gitlab" },
@@ -192,11 +196,10 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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" },
request: {
headers: {},
body: { workflowRef: "ref", workflowDefinition: "definition" },
},
modelID: ModelV2.ID.make("duo-workflow-custom"),
package: "aisdk:test-provider",
headers: {},
settings: { workflowRef: "ref", workflowDefinition: "definition" },
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
@@ -227,7 +230,8 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: {
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* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -254,11 +258,10 @@ describe("GitLabPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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" },
request: {
headers: {},
body: { featureFlags: { request_flag: true } },
},
modelID: ModelV2.ID.make("duo-workflow-custom"),
package: "aisdk:test-provider",
headers: {},
settings: { featureFlags: { request_flag: true } },
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
@@ -282,8 +285,10 @@ describe("GitLabPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
request: { headers: { h: "v" }, body: {} },
modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
headers: { h: "v" },
settings: {},
}),
sdk: {
workflowChat: () => undefined,
@@ -64,16 +64,16 @@ describe("GoogleVertexAnthropicPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
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)
expect(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project,
).toBe("cloud-project")
expect(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
).toBe("cloud-location")
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
"cloud-project",
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
"cloud-location",
)
}),
),
)
@@ -84,18 +84,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
provider.request.body.project = "configured-project"
provider.request.body.location = "configured-location"
provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
provider.settings = { ...provider.settings, project: "configured-project", location: "configured-location" }
}),
)
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",
)
expect(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
).toBe("configured-location")
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
"configured-location",
)
}),
),
)
@@ -121,7 +120,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
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",
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex-anthropic" },
@@ -147,7 +147,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
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",
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex-anthropic" },
@@ -167,7 +168,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "google-vertex", project: "project", location: "eu" },
@@ -186,7 +188,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
@@ -204,7 +207,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const sdkResult = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "google-vertex", project: "project", location: "us" },
@@ -212,7 +216,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const languageResult = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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,
options: {},
@@ -234,7 +239,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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) },
options: {},
@@ -252,7 +258,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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) },
options: {},
@@ -92,17 +92,14 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://opencode.ai/zen/v1",
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" }
}),
)
yield* addPlugin()
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
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
...provider.settings,
baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("google-cloud-project")
expect(provider.request.body.location).toBe("google-vertex-location")
expect(provider.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
expect(provider.settings?.project).toBe("google-cloud-project")
expect(provider.settings?.location).toBe("google-vertex-location")
expect(provider).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: {
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
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
...provider.settings,
baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
}),
)
@@ -172,21 +173,20 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/google-vertex",
},
modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/google-vertex",
}),
package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" },
})
expect(provider.request.body.project).toBe("vertex-project")
expect(provider.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
expect(provider.settings?.project).toBe("vertex-project")
expect(provider).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: {
baseURL:
"https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
},
})
expect(vertexOptions[0].project).toBe("vertex-project")
expect(vertexOptions[0].location).toBe("europe-west4")
@@ -209,23 +209,22 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
...provider.settings,
baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
provider.request.body.project = "config-project"
provider.request.body.location = "global"
provider.settings = { ...provider.settings, project: "config-project", location: "global" }
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("config-project")
expect(provider.request.body.location).toBe("global")
expect(provider.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global",
expect(provider.settings?.project).toBe("config-project")
expect(provider.settings?.location).toBe("global")
expect(provider).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global" },
})
}),
),
@@ -236,21 +235,20 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = {
...provider.settings,
baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
provider.request.body.project = "config-project"
provider.request.body.location = "eu"
provider.settings = { ...provider.settings, project: "config-project", location: "eu" }
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu",
expect(provider).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" },
})
}),
)
@@ -270,14 +268,14 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
provider.request.body.project = "config-project"
provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex")
provider.settings = { ...provider.settings, project: "config-project" }
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("config-project")
expect(provider.request.body.location).toBe("us-central1")
expect(provider.settings?.project).toBe("config-project")
expect(provider.settings?.location).toBe("us-central1")
}),
),
)
@@ -297,11 +295,8 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/google-vertex",
},
modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/google-vertex",
}),
package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" },
@@ -345,11 +340,8 @@ describe("GoogleVertexPlugin", () => {
aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
},
modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/openai-compatible",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "google-vertex" },
@@ -376,7 +368,8 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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 },
options: {},
@@ -27,7 +27,8 @@ describe("GooglePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-google", apiKey: "test" },
@@ -45,7 +46,8 @@ describe("GooglePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "google" },
@@ -62,7 +64,8 @@ describe("GooglePlugin", () => {
const sdkEvent = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-google", apiKey: "test" },
@@ -72,9 +75,29 @@ describe("GooglePlugin", () => {
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.modelId).toBe("gemini-api")
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")
}),
)
})
+12 -11
View File
@@ -28,7 +28,8 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "groq" },
@@ -45,7 +46,8 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "groq" },
@@ -62,7 +64,8 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "groq" },
@@ -79,7 +82,8 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
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* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -104,16 +108,13 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
api: {
id: ModelV2.ID.make("llama-api"),
type: "aisdk",
package: "@ai-sdk/groq",
},
modelID: ModelV2.ID.make("llama-api"),
package: "aisdk:@ai-sdk/groq",
}),
sdk,
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.provider).toBe("groq.chat")
}),
+16 -27
View File
@@ -27,22 +27,19 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
provider.request = { headers: { Existing: "value" }, body: {} }
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
provider.headers = { Existing: "value" }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
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",
"HTTP-Referer": "https://opencode.ai/",
"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,24 +48,19 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
})
})
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/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty(
"http-referer",
)
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")
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("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")
}),
)
@@ -77,23 +69,20 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
})
catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
provider.api = { type: "aisdk", package: "kilo" }
provider.package = ProviderV2.aisdk("kilo")
})
})
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/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.headers).toBeUndefined()
}),
)
})
@@ -34,23 +34,20 @@ describe("LLMGatewayPlugin", () => {
})
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.llmgateway.io/v1",
}
provider.request = { headers: { Existing: "value" }, body: {} }
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.llmgateway.io/v1" }
provider.headers = { Existing: "value" }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
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",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "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) => {
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
provider.disabled = true
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.llmgateway.io/v1",
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.llmgateway.io/v1" }
})
})
yield* addPlugin()
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()
}),
)
})
@@ -28,7 +28,8 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "mistral" },
@@ -45,7 +46,8 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "mistral" },
@@ -68,7 +70,8 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "mistral" },
@@ -92,7 +95,8 @@ describe("MistralPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
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* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -116,12 +120,13 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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,
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(language).toBeDefined()
}),
@@ -27,23 +27,20 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
provider.request = { headers: { Existing: "value" }, body: {} }
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://integrate.api.nvidia.com/v1" }
provider.headers = { Existing: "value" }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
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",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "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
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://integrate.api.nvidia.com/v1" }
})
})
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/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -74,20 +68,14 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
provider.request = {
headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
body: { baseURL: "https://integrate.api.nvidia.com/v1" },
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://integrate.api.nvidia.com/v1" }
provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }
})
})
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/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
@@ -27,7 +27,8 @@ describe("OpenAICompatiblePlugin", () => {
const defaulted = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom" },
@@ -35,7 +36,8 @@ describe("OpenAICompatiblePlugin", () => {
const disabled = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom", includeUsage: false },
@@ -53,7 +55,8 @@ describe("OpenAICompatiblePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom" },
@@ -76,7 +79,8 @@ describe("OpenAICompatiblePlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-provider", baseURL: "https://example.com/v1" },
@@ -96,7 +100,8 @@ describe("OpenAICompatiblePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "cloudflare-workers-ai" },
@@ -67,7 +67,8 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/openai",
options: { name: "custom-openai", apiKey: "test" },
@@ -84,7 +85,8 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "openai" },
@@ -102,7 +104,8 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -121,7 +124,8 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -137,10 +141,11 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "@ai-sdk/openai" },
package: "aisdk:@ai-sdk/openai",
})
catalog.provider.update(item.id, (draft) => {
draft.api = item.api
draft.package = item.package
draft.package = item.package
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
@@ -159,10 +164,11 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
api: { type: "aisdk", package: "test-provider" },
package: "aisdk:test-provider",
})
catalog.provider.update(item.id, (draft) => {
draft.api = item.api
draft.package = item.package
draft.package = item.package
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
@@ -133,13 +133,11 @@ describe("OpencodePlugin", () => {
expect(provider).toMatchObject({
name: "Remote",
integrationID: "opencode",
api: {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: `${server.url.origin}/v1`,
},
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: `${server.url.origin}/v1` },
})
expect(provider.request).toEqual({ headers: { "x-org-id": "org" }, body: { custom: "value" } })
expect(provider.headers).toEqual({ "x-org-id": "org" })
expect(provider.settings).toMatchObject({ custom: "value" })
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")))
@@ -150,12 +148,16 @@ describe("OpencodePlugin", () => {
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 },
})
expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 })
expect(model.settings).toEqual({
baseURL: `${server.url.origin}/v1`,
custom: "value",
temperature: 0.5,
})
expect(model.variants).toEqual([
{
id: ModelV2.VariantID.make("high"),
headers: {},
body: { temperature: 0.2 },
settings: { temperature: 0.2 },
},
])
expect(
@@ -175,11 +177,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
package: "aisdk:test-provider",
})
const model = ModelV2.Info.make({
...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: "aisdk:test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -188,7 +191,7 @@ describe("OpencodePlugin", () => {
})
})
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)
}),
),
@@ -201,11 +204,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
package: "aisdk:test-provider",
})
const model = ModelV2.Info.make({
...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: "aisdk:test-provider",
cost: cost(0),
})
catalog.provider.update(provider.id, () => {})
@@ -214,7 +218,7 @@ describe("OpencodePlugin", () => {
})
})
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)
}),
),
@@ -227,11 +231,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
package: "aisdk:test-provider",
})
const model = ModelV2.Info.make({
...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: "aisdk:test-provider",
cost: cost(0, 1),
})
catalog.provider.update(provider.id, () => {})
@@ -240,7 +245,7 @@ describe("OpencodePlugin", () => {
})
})
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(
true,
)
@@ -255,11 +260,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
package: "aisdk:test-provider",
})
const model = ModelV2.Info.make({
...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: "aisdk:test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -268,7 +274,7 @@ describe("OpencodePlugin", () => {
})
})
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)
}),
),
@@ -288,11 +294,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
package: "aisdk:test-provider",
})
const model = ModelV2.Info.make({
...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: "aisdk:test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -301,7 +308,7 @@ describe("OpencodePlugin", () => {
})
})
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)
}),
),
@@ -314,26 +321,26 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
request: {
headers: {},
body: { apiKey: "configured" },
},
package: "aisdk:test-provider",
headers: {},
settings: { apiKey: "configured" },
})
const model = ModelV2.Info.make({
...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: "aisdk:test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, (draft) => {
draft.request = provider.request
draft.headers = provider.headers
draft.settings = { apiKey: "configured" }
})
catalog.model.update(provider.id, model.id, (draft) => {
draft.cost = [...model.cost]
})
})
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)
}),
),
@@ -346,11 +353,12 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "test-provider" },
package: "aisdk:test-provider",
})
const model = ModelV2.Info.make({
...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: "aisdk:test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -359,7 +367,7 @@ describe("OpencodePlugin", () => {
})
})
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)
}),
),
@@ -30,19 +30,20 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
provider.request = { headers: { Existing: "value" }, body: {} }
provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
provider.headers = { Existing: "value" }
provider.settings = {}
})
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
})
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",
"HTTP-Referer": "https://opencode.ai/",
"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 +56,8 @@ describe("OpenRouterPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "openrouter" },
@@ -65,7 +67,8 @@ describe("OpenRouterPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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: "aisdk:test-provider",
}),
package: "@openrouter/ai-sdk-provider",
options: { name: "custom" },
@@ -79,7 +82,7 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
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.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {})
@@ -41,7 +41,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "perplexity" },
@@ -58,7 +59,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "perplexity" },
@@ -75,7 +77,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "perplexity" },
@@ -92,7 +95,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-perplexity" },
@@ -110,7 +114,8 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -49,7 +49,8 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
function model(providerID: string) {
return ModelV2.Info.make({
...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),
})
}
@@ -57,7 +57,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "openai" },
@@ -75,7 +76,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -94,7 +96,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {
@@ -117,7 +120,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -136,7 +140,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {
@@ -159,7 +164,8 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -41,7 +41,8 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "togetherai" },
@@ -59,7 +60,8 @@ describe("TogetherAIPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "togetherai" },
@@ -69,7 +71,8 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "togetherai" },
@@ -87,7 +90,8 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-togetherai" },
@@ -110,11 +114,8 @@ describe("TogetherAIPlugin", () => {
ProviderV2.ID.make("togetherai"),
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
),
api: {
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
package: "aisdk:test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -122,7 +123,9 @@ describe("TogetherAIPlugin", () => {
expect(result.language).toBeUndefined()
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"])
}),
)
@@ -41,7 +41,8 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "venice" },
@@ -58,7 +59,8 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-venice", apiKey: "test" },
@@ -76,7 +78,8 @@ describe("VenicePlugin", () => {
const similar = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "venice" },
@@ -84,7 +87,8 @@ describe("VenicePlugin", () => {
const other = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "venice" },
@@ -103,7 +107,8 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -25,12 +25,12 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
provider.request.headers.Existing = "1"
provider.package = ProviderV2.aisdk("@ai-sdk/vercel")
provider.headers = { ...provider.headers, Existing: "1" }
})
})
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",
"http-referer": "https://opencode.ai/",
"x-title": "opencode",
@@ -43,14 +43,12 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
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()
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty(
"HTTP-Referer",
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty("X-Title")
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer")
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title")
}),
)
@@ -62,7 +60,8 @@ describe("VercelPlugin", () => {
const event = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: { name: "custom-vercel" },
@@ -77,7 +76,7 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {}))
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()
}),
)
})
+11 -6
View File
@@ -42,7 +42,8 @@ describe("XAIPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {},
@@ -51,7 +52,8 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
options: {},
@@ -71,7 +73,8 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...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",
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* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -91,7 +94,8 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -112,7 +116,8 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...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),
options: {},
@@ -32,17 +32,14 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
})
})
yield* addPlugin()
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(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
expect(result.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
expect(Object.keys(required(result.headers)).sort()).toEqual(["HTTP-Referer", "X-Title"])
}),
)
@@ -51,17 +48,14 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
provider.request.headers.Existing = "value"
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
provider.headers = { ...provider.headers, Existing: "value" }
})
})
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",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@@ -74,17 +68,14 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
})
})
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/",
"X-Title": "custom-title",
})
@@ -96,12 +87,12 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
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()
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/",
"X-Title": "custom-title",
})
+9 -15
View File
@@ -36,21 +36,18 @@ describe("VariantPlugin", () => {
const service = yield* Catalog.Service
yield* service.transform((catalog) => {
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) => {
model.api = {
id: ModelV2.ID.make("glm-5.2"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
}
model.modelID = ModelV2.ID.make("glm-5.2")
model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
})
})
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([
expect.objectContaining({ id: "high", body: { reasoning_effort: "high" } }),
expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }),
expect.objectContaining({ id: "high", settings: { reasoningEffort: "high" } }),
expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
])
}),
)
@@ -60,19 +57,16 @@ describe("VariantPlugin", () => {
const service = yield* Catalog.Service
yield* service.transform((catalog) => {
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
model.api = {
id: ModelV2.ID.make("glm-5.2"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
}
model.variants = [{ id: ModelV2.VariantID.make("high"), headers: { custom: "true" }, body: {} }]
model.modelID = ModelV2.ID.make("glm-5.2")
model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
model.variants = [{ id: ModelV2.VariantID.make("high"), headers: { custom: "true" } }]
})
})
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([
expect.objectContaining({ id: "high", headers: { custom: "true" } }),
expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }),
expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
])
}),
)
+97 -58
View File
@@ -1,5 +1,6 @@
import { describe, expect } from "bun:test"
import { LLM } from "@opencode-ai/llm"
import { OpenAI } from "@opencode-ai/llm/providers"
import { LLMClient } from "@opencode-ai/llm/route"
import { DateTime, Effect } from "effect"
import { Headers } from "effect/unstable/http"
@@ -12,26 +13,22 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { it } from "./lib/effect"
type Api =
| {
readonly type: "aisdk"
readonly package: string
readonly url?: string
readonly settings?: Record<string, unknown>
}
| { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
type Transport = {
readonly package: string
readonly settings?: Record<string, unknown>
}
const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
const model = (transport: Transport, variants?: NonNullable<ModelV2.Info["variants"]>) =>
ModelV2.Info.make({
id: ModelV2.ID.make("test-model"),
modelID: ModelV2.ID.make("api-test-model"),
providerID: ProviderV2.ID.make("test-provider"),
name: "Test model",
api: { id: ModelV2.ID.make("api-test-model"), ...api },
...transport,
settings: { apiKey: "secret", ...transport.settings },
capabilities: { tools: true, input: ["text"], output: ["text"] },
request: {
headers: { "x-test": "header" },
body: { apiKey: "secret", custom_extension: { enabled: true } },
},
headers: { "x-test": "header" },
body: { custom_extension: { enabled: true } },
variants,
time: { released: 0 },
cost: [],
@@ -40,11 +37,49 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
limit: { context: 100, output: 20 },
})
const aisdkDependencies = {
loadAISDK: (input: ModelV2.Info) =>
Effect.succeed(
OpenAI.model(input.modelID ?? input.id, {
...input.settings,
headers: input.headers,
body: input.body,
limits: { context: input.limit.context, output: input.limit.output },
}),
),
}
describe("SessionRunnerModel", () => {
it.effect("maps catalog OpenAI AI SDK models into native Responses routes", () =>
it.effect("constructs native provider package models mechanically", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
model({
package: "@opencode-ai/llm/providers/openai",
settings: { baseURL: "https://openai.example/v1" },
}),
Credential.Key.make({ type: "key", key: "runtime-secret" }),
)
const headers = yield* resolved.route.auth.apply({
request: LLM.request({ model: resolved, prompt: "Hello" }),
method: "POST",
url: "https://openai.example/v1/responses",
body: "{}",
headers: Headers.empty,
})
expect(String(resolved.id)).toBe("api-test-model")
expect(resolved.route.id).toBe("openai-responses")
expect(resolved.route.defaults.headers).toEqual({ "x-test": "header" })
expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } })
expect(resolved.route.defaults.limits).toEqual({ context: 100, output: 20 })
expect(headers.authorization).toBe("Bearer runtime-secret")
}),
)
it.effect("maps catalog OpenAI native provider packages into Responses routes", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }),
)
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
@@ -63,7 +98,7 @@ describe("SessionRunnerModel", () => {
it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }),
)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
@@ -77,12 +112,15 @@ describe("SessionRunnerModel", () => {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://compatible.example/v1",
settings: { apiKey: "settings-secret", compatibility: "strict" },
package: "@opencode-ai/llm/providers/openai-compatible",
settings: {
apiKey: "settings-secret",
baseURL: "https://compatible.example/v1",
compatibility: "strict",
},
}),
request: { headers: {}, body: {} },
headers: {},
body: {},
}),
)
const request = LLM.request({ model: resolved, prompt: "Hello" })
@@ -101,7 +139,7 @@ describe("SessionRunnerModel", () => {
it.effect("overlays selected OpenAI Session variant bodies", () =>
Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [
const catalog = model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }, [
{
id: ModelV2.VariantID.make("high"),
headers: { "x-variant": "high" },
@@ -144,7 +182,10 @@ describe("SessionRunnerModel", () => {
it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
Effect.gen(function* () {
const catalog = model(
{ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" },
{
package: "@opencode-ai/llm/providers/openai-compatible",
settings: { baseURL: "https://compatible.example/v1" },
},
[
{
id: ModelV2.VariantID.make("high"),
@@ -176,7 +217,7 @@ describe("SessionRunnerModel", () => {
it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" })
const catalog = model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } })
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant_unavailable"),
projectID: ProjectV2.ID.global,
@@ -206,13 +247,16 @@ describe("SessionRunnerModel", () => {
it.effect("overlays selected Anthropic Session variant bodies", () =>
Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [
{
id: ModelV2.VariantID.make("high"),
headers: {},
body: { thinking: { type: "enabled", budget_tokens: 12000 } },
},
])
const catalog = model(
{ package: "@opencode-ai/llm/providers/anthropic", settings: { baseURL: "https://anthropic.example/v1" } },
[
{
id: ModelV2.VariantID.make("high"),
headers: {},
body: { thinking: { type: "enabled", budget_tokens: 12000 } },
},
],
)
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_anthropic_variant"),
projectID: ProjectV2.ID.global,
@@ -233,10 +277,10 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
it.effect("maps catalog Anthropic native provider packages into native routes", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }),
model({ package: "@opencode-ai/llm/providers/anthropic", settings: { baseURL: "https://anthropic.example/v1" } }),
)
expect(resolved.route).toMatchObject({
@@ -250,8 +294,9 @@ describe("SessionRunnerModel", () => {
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: {} },
...model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }),
headers: {},
body: {},
}),
Credential.Key.make({ type: "key", key: "secret" }),
)
@@ -273,8 +318,10 @@ describe("SessionRunnerModel", () => {
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: { apiKey: "configured-secret" } },
...model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }),
settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
headers: {},
body: {},
}),
credential,
)
@@ -287,39 +334,31 @@ describe("SessionRunnerModel", () => {
})
expect(headers.authorization).toBe("Bearer stored-secret")
expect(resolved.route.defaults.http?.body).toEqual({ tenant: "work" })
expect(resolved.route.defaults.http?.body).toEqual({})
}),
)
it.effect("rejects catalog APIs without a native route", () =>
it.effect("delegates aisdk-prefixed packages to the compatibility resolver", () =>
Effect.gen(function* () {
const failure = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
).pipe(Effect.flip)
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ package: "aisdk:@ai-sdk/google", settings: { baseURL: "https://google.example/v1" } }),
undefined,
aisdkDependencies,
)
expect(failure).toMatchObject({
_tag: "SessionRunnerModel.UnsupportedApiError",
providerID: "test-provider",
modelID: "test-model",
api: "aisdk:@ai-sdk/google",
})
expect(failure.message).toBe("Unsupported API for test-provider/test-model: aisdk:@ai-sdk/google")
expect(resolved.route.id).toBe("openai-responses")
expect(resolved.route.endpoint).toMatchObject({ baseURL: "https://google.example/v1" })
}),
)
it.effect("reports whether a catalog model has a supported native route", () =>
it.effect("reports whether a catalog model has a supported package", () =>
Effect.sync(() => {
expect(
SessionRunnerModel.supported(
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
model({ package: "aisdk:@ai-sdk/google", settings: { baseURL: "https://google.example/v1" } }),
),
).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)
expect(SessionRunnerModel.supported(model({ package: "native-provider-package" }))).toBe(true)
}),
)
})
-4
View File
@@ -100,12 +100,8 @@ test("Core reuses the canonical shared schemas", async () => {
[ModelV2.Family, Model.Family],
[ModelV2.Capabilities, Model.Capabilities],
[ModelV2.Cost, Model.Cost],
[ModelV2.Api, Model.Api],
[ModelV2.Info, Model.Info],
[ProviderV2.ID, Provider.ID],
[ProviderV2.AISDK, Provider.AISDK],
[ProviderV2.Native, Provider.Native],
[ProviderV2.Api, Provider.Api],
[ProviderV2.Request, Provider.Request],
[ProviderV2.Info, Provider.Info],
[corePermission.Effect, Permission.Effect],
+3
View File
@@ -15,6 +15,7 @@
"./route": "./src/route/index.ts",
"./provider": "./src/provider.ts",
"./providers": "./src/providers/index.ts",
"./provider-package": "./src/provider-package.ts",
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
"./providers/anthropic": "./src/providers/anthropic.ts",
"./providers/azure": "./src/providers/azure.ts",
@@ -22,6 +23,8 @@
"./providers/github-copilot": "./src/providers/github-copilot.ts",
"./providers/google": "./src/providers/google.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-profile": "./src/providers/openai-compatible-profile.ts",
"./providers/openrouter": "./src/providers/openrouter.ts",
+5
View File
@@ -1,6 +1,7 @@
export { LLMClient } from "./route/client"
export { Auth } from "./route/auth"
export { Provider } from "./provider"
export { ProviderPackage } from "./provider-package"
export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
export type {
RouteModelInput,
@@ -31,3 +32,7 @@ export type {
ModelFactory as ProviderModelFactory,
ModelOptions as ProviderModelOptions,
} from "./provider"
export type {
Definition as ProviderPackageDefinition,
Settings as ProviderPackageSettings,
} from "./provider-package"
+16
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"
+26 -1
View File
@@ -1,5 +1,6 @@
import type { RouteDefaultsInput } from "../route/client"
import { Auth } from "../route/auth"
import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema"
import * as BedrockConverse 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. */
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]
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 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)
}
+14 -1
View File
@@ -1,6 +1,7 @@
import type { RouteDefaultsInput } from "../route/client"
import { Auth } from "../route/auth"
import type { ProviderAuthOption } from "../route/auth-options"
import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema"
import * as AnthropicMessages from "../protocols/anthropic-messages"
@@ -10,6 +11,11 @@ export const routes = [AnthropicMessages.route]
export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
}
const auth = (options: ProviderAuthOption<"optional">) => {
if ("auth" in options && options.auth) return options.auth
return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
@@ -32,4 +38,11 @@ export const configure = (input: Config = {}) => {
}
export const provider = configure()
export const model = provider.model
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,
}).model(modelID)
@@ -2,6 +2,7 @@ import { ProviderID, type ModelID } from "../schema"
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
import type { RouteDefaultsInput } from "../route/client"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { ProviderPackage } from "../provider-package"
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
export const id = ProviderID.make("openai-compatible")
@@ -12,6 +13,12 @@ type GenericModelOptions = RouteDefaultsInput &
readonly baseURL: string
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL: string
readonly provider?: string
}
export type FamilyModelOptions = RouteDefaultsInput &
ProviderAuthOption<"optional"> & {
readonly baseURL?: string
@@ -56,6 +63,16 @@ export const provider = {
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 cerebras = define(profiles.cerebras)
export const deepinfra = define(profiles.deepinfra)
+28 -1
View File
@@ -1,5 +1,6 @@
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { Route, RouteDefaultsInput } from "../route/client"
import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema"
import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses"
@@ -21,6 +22,14 @@ export type Config = RouteDefaultsInput &
readonly providerOptions?: OpenAIProviderOptionsInput
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: 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 defaults = (input: Config) => {
@@ -57,7 +66,25 @@ export const configure = (input: Config = {}) => {
export const provider = configure()
export const model = provider.model
const config = (settings: Settings): Config => ({
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.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 responsesWebSocket = provider.responsesWebSocket
export const chat = provider.chat
@@ -0,0 +1,2 @@
export { chatModel as model } from "../openai"
export type { Settings } from "../openai"
@@ -0,0 +1,2 @@
export { model } from "../openai"
export type { Settings } from "../openai"
-1
View File
@@ -30,7 +30,6 @@ describe("public exports", () => {
test("provider barrels expose user-facing facades", () => {
expect(OpenAI.model).toBeFunction()
expect(OpenAI.provider.model).toBe(OpenAI.model)
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test"
import { model } from "@opencode-ai/llm/providers/openai"
describe("provider package entrypoints", () => {
test("semantic API aliases expose the same contract", async () => {
const modules = await Promise.all([
import("@opencode-ai/llm/providers/openai"),
import("@opencode-ai/llm/providers/openai/responses"),
import("@opencode-ai/llm/providers/openai/chat"),
import("@opencode-ai/llm/providers/anthropic"),
import("@opencode-ai/llm/providers/openai-compatible"),
import("@opencode-ai/llm/providers/amazon-bedrock"),
])
for (const module of modules) expect(module.model).toBeFunction()
expect(modules[0].model).toBe(modules[1].model)
})
test("maps package settings onto the executable model", () => {
const selected = model("gpt-5", {
apiKey: "fixture",
baseURL: "https://api.openai.test/v1",
headers: { "x-application": "opencode" },
body: { service_tier: "priority" },
limits: { context: 200_000, output: 64_000 },
unrelatedInheritedSetting: true,
})
expect(selected.route.id).toBe("openai-responses")
expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" })
expect(selected.route.defaults.http?.body).toEqual({ service_tier: "priority" })
expect(selected.route.defaults.limits).toEqual({ context: 200_000, output: 64_000 })
})
test("selects transport without changing the semantic API", () => {
expect(model("gpt-5", { apiKey: "fixture" }).route.id).toBe("openai-responses")
expect(model("gpt-5", { apiKey: "fixture", transport: "websocket" }).route.id).toBe(
"openai-responses-websocket",
)
})
})
+12 -26
View File
@@ -41,34 +41,23 @@ export const Cost = Schema.Struct({
}),
})
export const Api = Schema.Union([
Schema.Struct({
id: ID,
...Provider.AISDK.fields,
}),
Schema.Struct({
id: ID,
...Provider.Native.fields,
}),
]).pipe(Schema.toTaggedUnion("type"))
export type Api = typeof Api.Type
export interface Variant extends Schema.Schema.Type<typeof Variant> {}
export const Variant = Schema.Struct({
id: VariantID,
...Provider.Overlays,
})
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
id: ID,
modelID: ID.pipe(Schema.optional),
providerID: Provider.ID,
family: Family.pipe(Schema.optional),
name: Schema.String,
api: Api,
package: Provider.Package.pipe(Schema.optional),
...Provider.Overlays,
capabilities: Capabilities,
request: Schema.Struct({
...Provider.Request.fields,
variant: Schema.String.pipe(Schema.optional),
}),
variants: Schema.Struct({
id: VariantID,
...Provider.Request.fields,
}).pipe(Schema.Array, Schema.mutable),
variants: Variant.pipe(Schema.Array, Schema.mutable, Schema.optional),
time: Schema.Struct({
released: Schema.Finite,
}),
@@ -84,15 +73,12 @@ export const Info = Schema.Struct({
.annotate({ identifier: "ModelV2.Info" })
.pipe(
withStatics((schema) => ({
empty: (providerID: Provider.ID, modelID: ID) =>
empty: (providerID: Provider.ID, id: ID) =>
schema.make({
id: modelID,
id,
providerID,
name: modelID,
api: { id: modelID, type: "native", settings: {} },
name: id,
capabilities: { tools: false, input: [], output: [] },
request: { headers: {}, body: {} },
variants: [],
time: { released: 0 },
cost: [],
status: "active",
+10 -25
View File
@@ -22,23 +22,14 @@ export const ID = Schema.String.pipe(
)
export type ID = typeof ID.Type
export interface AISDK extends Schema.Schema.Type<typeof AISDK> {}
export const AISDK = Schema.Struct({
type: Schema.Literal("aisdk"),
package: Schema.String,
url: Schema.String.pipe(Schema.optional),
export const Package = Schema.String
export type Package = typeof Package.Type
export const Overlays = {
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
})
export interface Native extends Schema.Schema.Type<typeof Native> {}
export const Native = Schema.Struct({
type: Schema.Literal("native"),
url: Schema.String.pipe(Schema.optional),
settings: Schema.Record(Schema.String, Schema.Unknown),
})
export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
export type Api = typeof Api.Type
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}
export interface Request extends Schema.Schema.Type<typeof Request> {}
export const Request = Schema.Struct({
@@ -52,18 +43,12 @@ export const Info = Schema.Struct({
integrationID: Integration.ID.pipe(Schema.optional),
name: Schema.String,
disabled: Schema.Boolean.pipe(Schema.optional),
api: Api,
request: Request,
package: Package,
...Overlays,
})
.annotate({ identifier: "ProviderV2.Info" })
.pipe(
withStatics((schema) => ({
empty: (id: ID) =>
schema.make({
id,
name: id,
api: { type: "native", settings: {} },
request: { headers: {}, body: {} },
}),
empty: (id: ID) => schema.make({ id, name: id, package: "" }),
})),
)
+25 -52
View File
@@ -4105,47 +4105,34 @@ export type SessionMessage =
export type ModelV2Info = {
id: string
modelID?: string
providerID: string
family?: string
name: string
api:
| {
id: string
type: "aisdk"
package: string
url?: string
settings?: {
[key: string]: unknown
}
}
| {
id: string
type: "native"
url?: string
settings: {
[key: string]: unknown
}
}
package?: string
settings?: {
[key: string]: unknown
}
headers?: {
[key: string]: string
}
body?: {
[key: string]: unknown
}
capabilities: {
tools: boolean
input: Array<string>
output: Array<string>
}
request: {
headers: {
[key: string]: string
}
body: {
variants?: Array<{
id: string
settings?: {
[key: string]: unknown
}
variant?: string
}
variants: Array<{
id: string
headers: {
headers?: {
[key: string]: string
}
body: {
body?: {
[key: string]: unknown
}
}>
@@ -4178,29 +4165,15 @@ export type ProviderV2Info = {
integrationID?: string
name: string
disabled?: boolean
api:
| {
type: "aisdk"
package: string
url?: string
settings?: {
[key: string]: unknown
}
}
| {
type: "native"
url?: string
settings: {
[key: string]: unknown
}
}
request: {
headers: {
[key: string]: string
}
body: {
[key: string]: unknown
}
package: string
settings?: {
[key: string]: unknown
}
headers?: {
[key: string]: string
}
body?: {
[key: string]: unknown
}
}