Compare commits

..

7 Commits

Author SHA1 Message Date
Kit Langton 603b334b7f fix(core): preserve event routing semantics 2026-06-25 12:41:44 -04:00
Kit Langton 45071a8d8e refactor(core): prove event publication branches 2026-06-25 12:04:05 -04:00
Kit Langton 95f264e04e refactor(schema): distinguish published event durability 2026-06-25 11:51:54 -04:00
opencode-agent[bot] 77f2d2227a chore: generate 2026-06-25 15:44:27 +00:00
Kit Langton 7854f5b9f7 refactor(schema): normalize module patterns (#33770) 2026-06-25 15:42:22 +00:00
Brendan Allan 5565a8ee10 fix(app): restore new layout shortcuts (#33929) 2026-06-25 23:22:12 +08:00
Kit Langton 9ad7067971 docs(schema): document package conventions (#33768) 2026-06-25 11:12:09 -04:00
147 changed files with 2455 additions and 2537 deletions
+1
View File
@@ -141,6 +141,7 @@ _Avoid_: Response envelope
- Promise streaming methods return a lazy `AsyncIterable` directly rather than a Promise-wrapped stream object. Iteration opens the connection, `AbortSignal` cancels it, and ending iteration closes the underlying request; the Effect emitter analogously returns `Stream` directly.
- Promise SSE connection establishment, declared HTTP failures, and infrastructure failures occur during `AsyncIterable` iteration, beginning with its first `next()` call, rather than during synchronous method construction.
- Neither generated streaming runtime automatically reconnects after disconnection. Promise `AsyncIterable` and Effect `Stream` fail explicitly; live consumers refresh and resubscribe, while durable sequence-based resume remains explicit composition above the generated client.
- Event definition durability is authoritative for published payloads. Durable definitions publish and decode only with commit metadata (`aggregateID`, `seq`, and `version`); live definitions forbid that metadata. Core's pre-commit payload without an assigned sequence is a separate internal type and never reaches subscribers or projectors.
- Promise client construction is synchronous and network-free. It requires `baseUrl`, defaults to `globalThis.fetch`, accepts client-level headers, and merges them with per-call header overrides.
- Effect client construction accepts an explicit `baseUrl` and obtains `HttpClient.HttpClient` from the Effect environment. It does not install fetch or duplicate per-call transport policy; callers transform/provide the client for headers, tracing, retries, recording, and tests, while fiber interruption owns cancellation.
- Promise and Effect emitters each own their generated public type modules. The **SDK Contract IR**, not a physically shared generated type package, is the common source; this permits zero-Effect wire types and rich decoded Effect types to evolve independently.
+20
View File
@@ -3,12 +3,18 @@ import { useNavigate, useParams } from "@solidjs/router"
import { DebugBar } from "@/components/debug-bar"
import { HelpButton } from "@/components/help-button"
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
import { useCommand } from "@/context/command"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification"
import { usePlatform } from "@/context/platform"
import { setNavigate } from "@/utils/notification-click"
import { setV2Toast, ToastRegion } from "@/utils/toast"
export default function NewLayout(props: ParentProps) {
const command = useCommand()
const dialog = useDialog()
const language = useLanguage()
const platform = usePlatform()
const notification = useNotification()
const navigate = useNavigate()
@@ -22,6 +28,20 @@ export default function NewLayout(props: ParentProps) {
notification.session.markViewed(params.id)
})
command.register("layout", () => [
{
id: "settings.open",
title: language.t("command.settings.open"),
category: language.t("command.category.settings"),
keybind: "mod+comma",
onSelect: () => {
void import("@/components/settings-v2").then((x) => {
dialog.show(() => <x.DialogSettings />)
})
},
},
])
const update: TitlebarUpdate = {
version: () => {
const state = platform.updater?.state()
+1 -1
View File
@@ -26,7 +26,7 @@ export default function NewSessionPage() {
const serverSync = useServerSync()
const comments = useComments()
const route = useSessionKey()
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>()
let inputRef: HTMLDivElement | undefined
+12 -375
View File
@@ -1,46 +1,12 @@
export * as AISDK from "./aisdk"
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 type { LanguageModelV3 } from "@ai-sdk/provider"
import { Cause, Context, Effect, Layer, Schema, Scope } 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
@@ -107,10 +73,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.settings ?? {}),
headers: model.headers,
body: model.body,
...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}),
...model.request.body,
}
if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
const customFetch = options.fetch
const chunkTimeout = options.chunkTimeout
@@ -175,7 +141,6 @@ 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") {}
@@ -230,26 +195,24 @@ 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.modelID ?? model.id}/${JSON.stringify(model.settings)}`
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
const existing = languages.get(key)
if (existing) return existing
if (!ProviderV2.isAISDK(model.package))
if (model.api.type !== "aisdk")
return yield* new InitError({
providerID: model.providerID,
cause: new Error(`Unsupported package ${model.package}`),
cause: new Error(`Unsupported api ${model.api.type}`),
})
const packageName = ProviderV2.packageName(model.package) ?? ""
const options = prepareOptions(model, packageName)
const options = prepareOptions(model, model.api.package)
const sdkKey = JSON.stringify({
providerID: model.providerID,
package: packageName,
settings: model.settings,
api: model.api,
options,
})
const sdk =
sdks.get(sdkKey) ??
(yield* service.runSDK({ model, package: packageName, options }).pipe(initError(model.providerID))).sdk
(yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
if (!sdk)
return yield* new InitError({
providerID: model.providerID,
@@ -257,341 +220,15 @@ 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.modelID ?? model.id)).pipe(
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.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,
})
}
+24 -5
View File
@@ -69,21 +69,38 @@ export const layer = Layer.effect(
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
if (provider.disabled) return false
if (typeof provider.settings?.apiKey === "string") return true
if (typeof provider.request.body.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,
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),
api,
request,
})
}
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) => {
@@ -101,6 +118,7 @@ export const layer = Layer.effect(
draft.providers.set(providerID, current)
}
fn(current.provider)
normalizeApi(current.provider)
},
remove: (providerID) => {
draft.providers.delete(providerID)
@@ -123,6 +141,7 @@ 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)
+15 -17
View File
@@ -52,22 +52,17 @@ export const Plugin = define({
const providerID = id
catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name
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)
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)
}
})
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.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.api !== undefined) model.api = { ...model.api, ...config.api }
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
@@ -75,21 +70,24 @@ 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)
}
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)
Object.assign(existing.headers, variant.headers)
Object.assign(existing.body, variant.body)
}
}
if (config.cost !== undefined) {
+24 -13
View File
@@ -1,17 +1,12 @@
export * as ConfigProvider from "./provider"
import { Schema } from "effect"
import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model"
export const Overlays = {
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
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")({
@@ -35,16 +30,32 @@ 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),
package: Schema.String.pipe(Schema.optional),
...Overlays,
api: ModelApi.pipe(Schema.optional),
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
request: Schema.Struct({
...Request.fields,
variant: Schema.String.pipe(Schema.optional),
}).pipe(Schema.optional),
variants: Schema.Struct({
id: ModelV2.VariantID,
...Overlays,
...Request.fields,
}).pipe(Schema.Array, Schema.optional),
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional),
@@ -54,7 +65,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),
package: Schema.String.pipe(Schema.optional),
...Overlays,
api: ProviderV2.Api.pipe(Schema.optional),
request: Request.pipe(Schema.optional),
models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
}) {}
+67 -60
View File
@@ -2,7 +2,14 @@ export * as EventV2 from "./event"
import { Cause, Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event"
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
import type {
Data,
Definition,
DurableDefinition,
LiveDefinition,
Payload,
UncommittedPayload,
} from "@opencode-ai/schema/event"
import { and, asc, eq, gt } from "drizzle-orm"
import { Database } from "./database/database"
import { EventSequenceTable, EventTable } from "./event/sql"
@@ -124,8 +131,25 @@ export const layerWith = (options?: LayerOptions) =>
)
function commitDurableEvent(
definition: Definition,
event: Payload,
definition: DurableDefinition,
event: UncommittedPayload<DurableDefinition>,
input: undefined,
commit?: (seq: number) => Effect.Effect<void>,
): Effect.Effect<Payload<DurableDefinition>>
function commitDurableEvent(
definition: DurableDefinition,
event: UncommittedPayload<DurableDefinition>,
input: {
readonly seq: number
readonly aggregateID: string
readonly ownerID?: string
readonly strictOwner?: boolean
},
commit?: (seq: number) => Effect.Effect<void>,
): Effect.Effect<Payload<DurableDefinition> | undefined>
function commitDurableEvent(
definition: DurableDefinition,
event: UncommittedPayload<DurableDefinition>,
input?: {
readonly seq: number
readonly aggregateID: string
@@ -135,7 +159,7 @@ export const layerWith = (options?: LayerOptions) =>
commit?: (seq: number) => Effect.Effect<void>,
) {
return Effect.gen(function* () {
const durable = definition?.durable
const durable = definition.durable
if (durable) {
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
if (typeof aggregateID !== "string") {
@@ -200,7 +224,7 @@ export const layerWith = (options?: LayerOptions) =>
.run()
.pipe(Effect.orDie)
}
return
return undefined
}
yield* Effect.die(
new InvalidDurableEventError({
@@ -210,7 +234,7 @@ export const layerWith = (options?: LayerOptions) =>
)
}
if (input && row?.ownerID && row.ownerID !== input.ownerID) {
return
return undefined
}
const seq = input?.seq ?? latest + 1
if (input && seq !== latest + 1) {
@@ -234,10 +258,10 @@ export const layerWith = (options?: LayerOptions) =>
message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
}),
)
const committed = {
const committed: Payload<DurableDefinition> = {
...event,
durable: { aggregateID, seq, version: durable.version },
} as Payload
}
for (const projector of list) {
yield* projector(committed)
}
@@ -267,14 +291,14 @@ export const layerWith = (options?: LayerOptions) =>
])
.run()
.pipe(Effect.orDie)
return { aggregateID, seq }
return committed
}),
{ behavior: "immediate" },
)
.pipe(Effect.orDie)
if (committed) {
yield* Effect.forEach(
pubsub.durable.get(committed.aggregateID) ?? [],
pubsub.durable.get(committed.durable.aggregateID) ?? [],
(wake) => PubSub.publish(wake, undefined),
{ discard: true },
)
@@ -287,35 +311,6 @@ export const layerWith = (options?: LayerOptions) =>
})
}
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
return Effect.gen(function* () {
if (!definition?.durable && commit)
return yield* Effect.die(
new InvalidDurableEventError({
type: event.type,
message: "Local commit hooks require a durable event",
}),
)
if (definition?.durable) {
const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit)
if (committed) {
event = {
...event,
durable: {
aggregateID: committed.aggregateID,
seq: committed.seq,
version: definition.durable.version,
},
}
yield* notify(event as Payload, true)
return event
}
}
yield* notify(event as Payload, false)
return event
})
}
const observe = (event: Payload, observer: (event: Payload) => Effect.Effect<void>) =>
Effect.suspend(() => observer(event)).pipe(
Effect.catchCauseIf(
@@ -337,7 +332,12 @@ export const layerWith = (options?: LayerOptions) =>
})
}
function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
function publish<D extends Definition>(
definition: D,
data: Data<D>,
options?: PublishOptions,
): Effect.Effect<Payload<D>>
function publish(definition: Definition, data: unknown, options?: PublishOptions): Effect.Effect<Payload> {
return Effect.gen(function* () {
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
const location =
@@ -345,17 +345,34 @@ export const layerWith = (options?: LayerOptions) =>
(serviceLocation
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
: undefined)
return yield* publishEvent(
definition,
{
if (definition.durable) {
const event: UncommittedPayload<DurableDefinition> = {
id: options?.id ?? ID.create(),
...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type,
...(location ? { location } : {}),
data,
} as Payload<D>,
options?.commit,
)
}
const committed = yield* commitDurableEvent(definition, event, undefined, options?.commit)
yield* notify(committed, true)
return committed
}
if (options?.commit)
return yield* Effect.die(
new InvalidDurableEventError({
type: definition.type,
message: "Local commit hooks require a durable event",
}),
)
const event: Payload<LiveDefinition> = {
id: options?.id ?? ID.create(),
...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type,
...(location ? { location } : {}),
data,
}
yield* notify(event, false)
return event
})
}
@@ -370,11 +387,11 @@ export const layerWith = (options?: LayerOptions) =>
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
)
} else {
const payload = {
const payload: UncommittedPayload<DurableDefinition> = {
id: event.id,
type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data),
} as Payload
}
const committed = yield* commitDurableEvent(definition, payload, {
seq: event.seq,
aggregateID: event.aggregateID,
@@ -382,17 +399,7 @@ export const layerWith = (options?: LayerOptions) =>
strictOwner: options?.strictOwner,
})
if (committed && options?.publish) {
yield* notify(
{
...payload,
durable: {
aggregateID: committed.aggregateID,
seq: committed.seq,
version: definition.durable.version,
},
},
true,
)
yield* notify(committed, true)
}
}
})
@@ -459,7 +466,7 @@ export const layerWith = (options?: LayerOptions) =>
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
const decodeSerializedEvent = (event: SerializedEvent) => {
const decodeSerializedEvent = (event: SerializedEvent): Payload<DurableDefinition> => {
const definition = Durable.get(event.type)
if (!definition?.durable) {
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
+7 -2
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,10 +20,15 @@ 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 = DeepMutable<Info>
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
api: ProviderV2.MutableApi<Api>
}
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/")
+7 -8
View File
@@ -1,8 +1,8 @@
export * as PluginV2 from "./plugin"
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { PluginEvent, PluginID } from "@opencode-ai/schema/plugin"
import type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect"
import { Plugin } from "@opencode-ai/schema/plugin"
import { AgentV2 } from "./agent"
import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
@@ -15,13 +15,12 @@ import { Reference } from "./reference"
import { SkillV2 } from "./skill"
import { State } from "./state"
export const ID = PluginID
export const ID = Plugin.ID
export type ID = typeof ID.Type
export const Event = PluginEvent
export const Event = Plugin.Event
export interface Interface {
readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void>
readonly add: (id: ID, effect: PluginRuntime["effect"]) => Effect.Effect<void>
readonly remove: (id: ID) => Effect.Effect<void>
readonly wait: (id: ID) => Effect.Effect<void>
}
@@ -38,9 +37,9 @@ export const layer = Layer.effect(
const loading = new Set<ID>()
const waiters = new Map<ID, Set<Deferred.Deferred<void>>>()
const failures = new Map<ID, Exit.Exit<void, never>>()
let host: Parameters<Plugin["effect"]>[0]
let host: Parameters<PluginRuntime["effect"]>[0]
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: Plugin["effect"]) {
const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginRuntime["effect"]) {
if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`)
yield* locks.withLock(id)(
+24 -4
View File
@@ -75,8 +75,17 @@ export const ModelsDevPlugin = define({
const providerID = ProviderV2.ID.make(item.id)
catalog.provider.update(providerID, (provider) => {
provider.name = item.name
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
provider.api = item.npm
? {
type: "aisdk",
package: item.npm,
url: item.api,
}
: {
type: "native",
url: item.api,
settings: {},
}
})
for (const model of Object.values(item.models)) {
@@ -84,8 +93,19 @@ 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.package = model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined
draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
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.capabilities = {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
@@ -65,14 +65,15 @@ export const AmazonBedrockPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
evt.provider.update(item.provider.id, (provider) => {
if (typeof provider.settings?.endpoint !== "string") return
if (provider.api.type !== "aisdk") return
if (typeof provider.request.body.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.settings.baseURL = provider.settings.endpoint
delete provider.settings.endpoint
provider.api.url = provider.request.body.endpoint
delete provider.request.body.endpoint
})
}
}),
@@ -113,15 +114,12 @@ export const AmazonBedrockPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
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)
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
return
}
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.modelID ?? evt.model.id, region))
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
}),
)
}),
@@ -1,6 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const AnthropicPlugin = define({
id: "anthropic",
@@ -8,13 +7,11 @@ export const AnthropicPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {
...provider.headers,
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
}
provider.request.headers["anthropic-beta"] =
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
})
}
}),
+10 -21
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 (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue
const configured = item.provider.settings?.resourceName
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/azure") continue
const configured = item.provider.request.body.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.settings = { ...provider.settings, resourceName }
provider.request.body.resourceName = resourceName
})
}
}),
@@ -35,7 +35,7 @@ export const AzurePlugin = define({
if (
!evt.options.resourceName &&
!evt.options.baseURL &&
(!ProviderV2.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string")
(evt.model.api.type !== "aisdk" || !evt.model.api.url)
) {
throw new Error(
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
@@ -49,11 +49,7 @@ 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.modelID ?? evt.model.id,
Boolean(evt.options.useCompletionUrls),
)
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
}),
)
}),
@@ -67,14 +63,11 @@ export const AzureCognitiveServicesPlugin = define({
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (!item.provider.id.includes("azure-cognitive-services")) continue
evt.provider.update(item.provider.id, (provider) => {
provider.settings = {
...provider.settings,
baseURL: `https://${resourceName}.cognitiveservices.azure.com/openai`,
}
provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
})
}
}),
@@ -82,11 +75,7 @@ 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.modelID ?? evt.model.id,
Boolean(evt.options.useCompletionUrls),
)
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
}),
)
}),
@@ -1,6 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const CerebrasPlugin = define({
id: "cerebras",
@@ -8,10 +7,10 @@ export const CerebrasPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
provider.request.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 (!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) }
if (provider.api.type !== "aisdk") return
if (provider.api.url) return
const accountId = resolveAccountId(provider.request.body)
if (accountId) provider.api.url = 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) && !accountId) return
if (!hasWorkersEndpoint(evt.model.api) && !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.modelID ?? evt.model.id)
evt.language = evt.sdk.languageModel(evt.model.api.id)
}),
)
}),
@@ -54,11 +54,8 @@ function workersEndpoint(accountId: string) {
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
}
function hasWorkersEndpoint(model: {
readonly package?: string
readonly settings?: Readonly<Record<string, unknown>>
}) {
return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
function hasWorkersEndpoint(api: ProviderV2.Api) {
return api.type === "aisdk" && Boolean(api.url)
}
function sdkOptions(options: Record<string, any>) {
@@ -36,11 +36,12 @@ 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.modelID ?? evt.model.id)
evt.language = evt.sdk.languageModel(evt.model.api.id)
return
}
const id = evt.model.modelID ?? evt.model.id
evt.language = shouldUseResponses(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
evt.language = shouldUseResponses(evt.model.api.id)
? evt.sdk.responses(evt.model.api.id)
: evt.sdk.chat(evt.model.api.id)
}),
)
}),
+12 -10
View File
@@ -36,24 +36,26 @@ 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 : {}
const id = evt.model.modelID ?? evt.model.id
if (id.startsWith("duo-workflow-")) {
if (evt.model.api.id.startsWith("duo-workflow-")) {
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
const workflowRef =
typeof evt.model.settings?.workflowRef === "string" ? evt.model.settings.workflowRef : undefined
typeof evt.model.request.body.workflowRef === "string" ? evt.model.request.body.workflowRef : undefined
const workflowDefinition =
typeof evt.model.settings?.workflowDefinition === "string"
? evt.model.settings.workflowDefinition
typeof evt.model.request.body.workflowDefinition === "string"
? evt.model.request.body.workflowDefinition
: undefined
const language = evt.sdk.workflowChat(gitlab.isWorkflowModel(id) ? id : "duo-workflow", {
featureFlags,
workflowDefinition,
})
const language = evt.sdk.workflowChat(
gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow",
{
featureFlags,
workflowDefinition,
},
)
if (workflowRef) language.selectedModelRef = workflowRef
evt.language = language
return
}
evt.language = evt.sdk.agenticChat(id, {
evt.language = evt.sdk.agenticChat(evt.model.api.id, {
aiGatewayHeaders: evt.options.aiGatewayHeaders,
featureFlags,
})
@@ -60,28 +60,25 @@ export const GoogleVertexPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (item.provider.api.type !== "aisdk") continue
if (
ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
item.provider.api.package !== "@ai-sdk/google-vertex" &&
!(
item.provider.id === ProviderV2.ID.googleVertex &&
ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
item.provider.api.package.includes("@ai-sdk/openai-compatible")
)
)
continue
const project = resolveProject(item.provider.settings ?? {})
const location = String(resolveLocation(item.provider.settings ?? {}))
const project = resolveProject(item.provider.request.body)
const location = String(resolveLocation(item.provider.request.body))
evt.provider.update(item.provider.id, (provider) => {
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) }
: {}),
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)
}
})
}
@@ -109,7 +106,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.modelID ?? evt.model.id).trim())
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
}),
)
}),
@@ -121,20 +118,21 @@ export const GoogleVertexAnthropicPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
const project =
item.provider.settings?.project ??
item.provider.request.body.project ??
process.env.GOOGLE_CLOUD_PROJECT ??
process.env.GCP_PROJECT ??
process.env.GCLOUD_PROJECT
const location =
item.provider.settings?.location ??
item.provider.request.body.location ??
process.env.GOOGLE_CLOUD_LOCATION ??
process.env.VERTEX_LOCATION ??
"global"
evt.provider.update(item.provider.id, (provider) => {
provider.settings = { ...provider.settings, ...(project ? { project } : {}), location }
if (project) provider.request.body.project = project
provider.request.body.location = location
})
}
}),
@@ -168,7 +166,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.modelID ?? evt.model.id).trim())
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
}),
)
}),
+5 -5
View File
@@ -1,6 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const KiloPlugin = define({
id: "kilo",
@@ -8,11 +7,12 @@ export const KiloPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
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
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
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
})
}
}),
@@ -1,7 +1,6 @@
import { Effect } from "effect"
import { define } from "../internal"
import { Integration } from "../../integration"
import { ProviderV2 } from "../../provider"
export const LLMGatewayPlugin = define({
id: "llmgateway",
@@ -11,17 +10,14 @@ export const LLMGatewayPlugin = define({
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.disabled) 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 (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 (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {
...provider.headers,
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-Source": "opencode",
}
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-Source"] = "opencode"
})
}
}),
+6 -10
View File
@@ -1,6 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const NvidiaPlugin = define({
id: "nvidia",
@@ -8,16 +7,13 @@ export const NvidiaPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
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
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
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {
...provider.headers,
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": provider.headers?.["X-BILLING-INVOKE-ORIGIN"] ?? "OpenCode",
}
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.request.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 (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.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.modelID ?? evt.model.id)
evt.language = evt.sdk.responses(evt.model.api.id)
}),
)
}),
+21 -15
View File
@@ -111,34 +111,40 @@ 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.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 }
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))
})
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.modelID = config.id
if (config.id !== undefined) model.api.id = config.id
if (config.provider !== undefined) {
model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
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: {} }
}
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]
model.headers = { ...model.headers, ...config.headers }
model.settings = { ...model.settings, ...ConfigProviderOptionsV1.model(withoutCredentials(config.options)) }
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)))
if (config.variants !== undefined) {
model.variants = Object.entries(config.variants).map(([id, options]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(options.headers ?? {}) },
settings: ConfigProviderOptionsV1.model(withoutCredentials(options)),
body: lowerer.request(withoutCredentials(options)),
}))
}
if (config.release_date !== undefined) {
@@ -157,9 +163,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.settings?.apiKey)
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey)
catalog.provider.update(item.provider.id, (provider) => {
if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
if (!hasKey) provider.request.body.apiKey = "public"
})
if (hasKey) return
for (const model of item.models.values()) {
@@ -1,6 +1,5 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { define } from "../internal"
export const OpenRouterPlugin = define({
@@ -9,10 +8,11 @@ export const OpenRouterPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["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.modelID ?? evt.model.id)
evt.language = evt.sdk(evt.model.api.id)
}),
)
}),
+4 -4
View File
@@ -1,6 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const VercelPlugin = define({
id: "vercel",
@@ -8,10 +7,11 @@ export const VercelPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/vercel") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
provider.request.headers["http-referer"] = "https://opencode.ai/"
provider.request.headers["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.modelID ?? evt.model.id)
evt.language = evt.sdk.responses(evt.model.api.id)
}),
)
}),
+5 -9
View File
@@ -1,6 +1,5 @@
import { Effect } from "effect"
import { define } from "../internal"
import { ProviderV2 } from "../../provider"
export const ZenmuxPlugin = define({
id: "zenmux",
@@ -8,15 +7,12 @@ export const ZenmuxPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
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
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
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
...provider.headers,
}
provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/"
provider.request.headers["X-Title"] ??= "opencode"
})
}
}),
+11 -15
View File
@@ -1,8 +1,7 @@
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({
@@ -12,14 +11,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 as ModelV2.Info, record.provider)
const generated = generate(draft)
if (generated.length === 0) return
const explicit = new Map((draft.variants ?? []).map((variant) => [variant.id, variant]))
const generatedIDs = new Set<string>(generated.map((variant) => variant.id))
const explicit = new Map(draft.variants.map((variant) => [variant.id, variant]))
const generatedIDs = new Set(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)),
]
})
}
@@ -28,16 +27,13 @@ export const Plugin = define({
}),
})
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()
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()
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({
id: ModelV2.VariantID.make(id),
settings: { reasoningEffort: id },
id,
headers: {},
body: { reasoning_effort: id },
}))
}
+2 -2
View File
@@ -13,7 +13,7 @@ import { Slug } from "../util/slug"
import { EventV2 } from "../event"
import { Database } from "../database/database"
import { Location } from "../location"
import { ProjectDirectoriesEvent } from "@opencode-ai/schema/project-directories"
import { Event } from "@opencode-ai/schema/project-directories"
import { ProjectCopy } from "@opencode-ai/schema/project-copy"
export const StrategyID = ProjectCopy.StrategyID
@@ -96,7 +96,7 @@ export interface Strategy {
readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
}
export const Event = ProjectDirectoriesEvent
export { Event }
export interface Interface {
readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
+2 -2
View File
@@ -4,7 +4,7 @@ import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { LayerNode } from "../effect/layer-node"
import { AbsolutePath, optionalOmitUndefined } from "../schema"
import { AbsolutePath, optional } from "../schema"
import { ProjectSchema } from "./schema"
import { ProjectDirectoryTable } from "./sql"
import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
@@ -39,7 +39,7 @@ export type ListInput = typeof ListInput.Type
export const ListOutput = Schema.Array(
Schema.Struct({
directory: AbsolutePath,
strategy: optionalOmitUndefined(Schema.String),
strategy: optional(Schema.String),
}),
).annotate({ identifier: "Project.Directories" })
export type ListOutput = typeof ListOutput.Type
+9 -120
View File
@@ -1,91 +1,20 @@
export * as ProviderV2 from "./provider"
import { Effect, Schema } from "effect"
import { pathToFileURL } from "url"
import { Types } from "effect"
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_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 AISDK = Provider.AISDK
export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.LoadError", {
package: Schema.String,
cause: Schema.Defect(),
}) {}
export type ProviderPackage = ProviderPackageDefinition
export const Native = Provider.Native
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 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
export const Request = Provider.Request
export type Request = Provider.Request
@@ -93,44 +22,4 @@ export type Request = Provider.Request
export const Info = Provider.Info
export type Info = Provider.Info
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]
}
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
+4 -5
View File
@@ -2,7 +2,7 @@ export * as Pty from "./pty"
import type { Disp, Proc } from "#pty"
import { Context, Effect, Layer, Schema, Types } from "effect"
import { PtyEvent, PtyInfo, Pty } from "@opencode-ai/schema/pty"
import { Pty } from "@opencode-ai/schema/pty"
import { Config } from "./config"
import { EventV2 } from "./event"
import { Location } from "./location"
@@ -35,8 +35,7 @@ type Active = {
listeners: Disp[]
}
export const Info = PtyInfo
export const Info = Pty.Info
export type Info = Types.DeepMutable<typeof Info.Type>
export const CreateInput = Pty.CreateInput
@@ -47,6 +46,8 @@ export const UpdateInput = Pty.UpdateInput
export type UpdateInput = Types.DeepMutable<typeof UpdateInput.Type>
export const Event = Pty.Event
export type AttachInput = {
// Absolute output cursor to replay from. -1 tails from the current end; omitted replays the full retained buffer.
readonly cursor?: number
@@ -75,8 +76,6 @@ export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("Pty.Exi
ptyID: PtyID,
}) {}
export const Event = PtyEvent
export interface Interface {
readonly list: () => Effect.Effect<Info[]>
readonly get: (id: PtyID) => Effect.Effect<Info, NotFoundError>
+3 -11
View File
@@ -3,21 +3,13 @@ import {
AbsolutePath,
DateTimeUtcFromMillis,
NonNegativeInt,
optionalOmitUndefined,
optional,
PositiveInt,
RelativePath,
withStatics,
statics,
} from "@opencode-ai/schema/schema"
export {
AbsolutePath,
DateTimeUtcFromMillis,
NonNegativeInt,
optionalOmitUndefined,
PositiveInt,
RelativePath,
withStatics,
}
export { AbsolutePath, DateTimeUtcFromMillis, NonNegativeInt, optional, PositiveInt, RelativePath, statics }
/**
* Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable`
+2 -2
View File
@@ -9,7 +9,7 @@ import { ContextSnapshotDecodeError } from "./error"
import { SessionEvent } from "./event"
import { SessionHistory } from "./history"
import { SessionInput } from "./input"
import { SessionMessageID } from "./message-id"
import { SessionMessage } from "./message"
import { SessionSchema } from "./schema"
import { SessionContextEpochTable } from "./sql"
@@ -71,7 +71,7 @@ const prepareOnce = Effect.fnUntraced(function* (
yield* events.publish(
SessionEvent.ContextUpdated,
{ sessionID, messageID: SessionMessageID.ID.create(), timestamp: yield* DateTime.now, text: result.text },
{ sessionID, messageID: SessionMessage.ID.create(), timestamp: yield* DateTime.now, text: result.text },
{ commit: () => advance(db, sessionID, result.snapshot).pipe(Effect.orDie) },
)
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
+2 -2
View File
@@ -8,7 +8,7 @@ import { AbsolutePath, RelativePath } from "../schema"
import { WorkspaceV2 } from "../workspace"
import { SessionSchema } from "./schema"
import { SessionTable } from "./sql"
import { SessionMessageID } from "./message-id"
import { SessionMessage } from "./message"
import { Snapshot } from "../snapshot"
export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info {
@@ -40,7 +40,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined,
}),
subpath: row.path ? RelativePath.make(row.path) : undefined,
revert: row.revert ? { ...row.revert, messageID: SessionMessageID.ID.make(row.revert.messageID) } : undefined,
revert: row.revert ? { ...row.revert, messageID: SessionMessage.ID.make(row.revert.messageID) } : undefined,
time: {
created: DateTime.makeUnsafe(row.time_created),
updated: DateTime.makeUnsafe(row.time_updated),
-2
View File
@@ -1,2 +0,0 @@
export * as SessionMessageID from "./message-id"
export { ID } from "@opencode-ai/schema/session-message-id"
+1 -2
View File
@@ -15,7 +15,6 @@ import { WorkspaceV2 } from "../workspace"
import { SessionContextEpoch } from "./context-epoch"
import { MessageTable, PartTable, SessionInputTable, SessionMessageTable, SessionTable } from "./sql"
import type { DeepMutable } from "../schema"
import { SessionMessageID } from "./message-id"
type DatabaseService = Database.Interface["db"]
@@ -67,7 +66,7 @@ function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInse
tokens_reasoning: (info.tokens ?? { reasoning: 0 }).reasoning,
tokens_cache_read: (info.tokens ?? { cache: { read: 0 } }).cache.read,
tokens_cache_write: (info.tokens ?? { cache: { write: 0 } }).cache.write,
revert: info.revert ? { ...info.revert, messageID: SessionMessageID.ID.make(info.revert.messageID) } : null,
revert: info.revert ? { ...info.revert, messageID: SessionMessage.ID.make(info.revert.messageID) } : null,
permission: info.permission ? [...info.permission] : undefined,
time_created: info.time.created,
time_updated: info.time.updated,
+73 -78
View File
@@ -1,14 +1,16 @@
export * as SessionRunnerModel from "./model"
import { Model } from "@opencode-ai/llm"
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 { 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"
@@ -48,16 +50,16 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
}
}
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedPackageError",
export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()(
"SessionRunnerModel.UnsupportedApiError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
package: Schema.String,
api: Schema.String,
},
) {
override get message() {
return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
return `Unsupported API for ${this.providerID}/${this.modelID}: ${this.api}`
}
}
@@ -65,7 +67,7 @@ export type Error =
| ModelNotSelectedError
| ModelUnavailableError
| VariantUnavailableError
| UnsupportedPackageError
| UnsupportedApiError
| Integration.AuthorizationError
export interface Interface {
@@ -77,12 +79,33 @@ 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" ? undefined : variantID
const variant = model.variants?.find((item) => item.id === id)
const id = variantID === "default" || variantID === undefined ? model.request.variant : variantID
const variant = model.variants.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default")
return Effect.fail(
new VariantUnavailableError({
@@ -94,87 +117,65 @@ const withVariant = (
return Effect.succeed(
variant
? produce(model, (draft) => {
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
Object.assign(draft.request.headers, variant.headers)
Object.assign(draft.request.body, variant.body)
})
: model,
)
}
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,
})
const apiName = (model: ModelV2.Info) =>
model.api.type === "aisdk" ? `${model.api.type}:${model.api.package}` : model.api.type
export const fromCatalogModel = (
model: ModelV2.Info,
credential?: Credential.Value,
dependencies: Dependencies = {},
): Effect.Effect<Model, UnsupportedPackageError> => {
): Effect.Effect<Model, UnsupportedApiError> => {
const resolved =
credential?.metadata === undefined
? model
: produce(model, (draft) => {
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)),
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 }),
)
}
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/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 }),
)
}
return Effect.fail(unsupported(resolved))
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),
}),
)
}
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 resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
export const supported = (model: ModelV2.Info) => Boolean(model.package)
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))
/** Resolves models from the catalog belonging to the current Location runtime. */
export const locationLayer = Layer.effect(
@@ -182,8 +183,6 @@ 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.
@@ -209,10 +208,6 @@ 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),
},
)
}),
})
+2 -3
View File
@@ -2,15 +2,14 @@ export * as SessionTodo from "./todo"
import { asc, eq } from "drizzle-orm"
import { Context, Effect, Layer } from "effect"
import { SessionTodo, SessionTodoInfo } from "@opencode-ai/schema/session-todo"
import { SessionTodo } from "@opencode-ai/schema/session-todo"
import { Database } from "../database/database"
import { EventV2 } from "../event"
import { SessionSchema } from "./schema"
import { TodoTable } from "./sql"
export const Info = SessionTodoInfo
export const Info = SessionTodo.Info
export type Info = typeof Info.Type
export const Event = SessionTodo.Event
export interface Interface {
+32 -14
View File
@@ -6,7 +6,6 @@ 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",
@@ -169,22 +168,30 @@ function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
}
function migrateProvider(info: ConfigProviderV1.Info) {
const options = ConfigProviderOptionsV1.provider(info.options ?? {})
const lowerer = ConfigProviderOptionsV1.get(info.npm)
const options = lowerer.provider(info.options ?? {})
return {
name: info.name,
env: info.env,
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,
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 },
models:
info.models &&
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model)])),
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])),
}
}
function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
const settings = info.options && ConfigProviderOptionsV1.model(info.options)
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)
const costs = info.cost && [
{
input: info.cost.input,
@@ -207,18 +214,29 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
: undefined
return {
modelID: info.id,
family: info.family,
name: info.name,
package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined,
settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
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 },
capabilities,
headers: info.headers,
request: (info.headers || request) && {
headers: info.headers,
body: request,
},
variants:
info.variants &&
Object.entries(info.variants).map(([id, options]) => ({
id,
settings: ConfigProviderOptionsV1.model(options),
body: lowerer.request(options),
})),
cost: costs,
disabled: info.status === "deprecated" ? true : undefined,
+203 -15
View File
@@ -3,28 +3,216 @@ 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 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
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 {
return {
settings,
headers: headerOverlay,
body: bodyOverlay,
headers: headers(options.headers),
body: body(options.body),
settings: omit(options, ["headers", "body", ...extraKeys]),
}
}
export function model(options: Options) {
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) {
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)
}
+52 -48
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"))).body).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
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"))).body).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
}).pipe(Effect.provide(layer))
})
@@ -135,107 +135,111 @@ describe("CatalogV2", () => {
),
)
it.effect("keeps provider API settings flat", () =>
it.effect("normalizes provider baseURL into api url", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
yield* catalog.transform((catalog) =>
catalog.provider.update(providerID, (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://override.example.com" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://default.example.com",
}
provider.request.body.baseURL = "https://override.example.com"
}),
)
expect(required(yield* catalog.provider.get(providerID))).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
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",
})
}),
)
it.effect("resolves model settings and upstream model ID", () =>
it.effect("normalizes model baseURL into api url", () =>
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.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://provider.example.com" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
})
catalog.model.update(providerID, modelID, (model) => {
model.modelID = ModelV2.ID.make("upstream-model")
model.settings = { baseURL: "https://override.example.com" }
model.api = {
id: modelID,
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://model.example.com",
}
model.request.body.baseURL = "https://override.example.com"
})
})
expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
id: modelID,
modelID: ModelV2.ID.make("upstream-model"),
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://override.example.com" },
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://override.example.com",
settings: {},
})
}),
)
it.effect("resolves default model transport from provider transport", () =>
it.effect("resolves default model api from provider api", () =>
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.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { baseURL: "https://provider.example.com" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
})
catalog.model.update(providerID, modelID, () => {})
})
expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
id: modelID,
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://provider.example.com" },
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
})
}),
)
it.effect("resolves provider and model overlay merges", () =>
it.effect("resolves provider and model request 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.headers = { provider: "provider", "X-Shared": "provider" }
provider.body = {
provider: true,
reasoning: { type: "enabled", budget: 8_000 },
stop: ["END"],
disabled: { type: "enabled" },
}
provider.request.headers.provider = "provider"
provider.request.headers.shared = "provider"
provider.request.body.provider = true
})
catalog.model.update(providerID, modelID, (model) => {
model.headers = { model: "model", "x-shared": "model" }
model.body = {
model: true,
request: true,
reasoning: { budget: 32_000 },
stop: ["STOP"],
disabled: null,
}
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"
})
})
const model = required(yield* catalog.model.get(providerID, modelID))
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,
})
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
expect(model.request.body).toEqual({ provider: true, model: true, request: true, shared: "model" })
}),
)
+26 -16
View File
@@ -43,7 +43,11 @@ function testLayer(
}
const provider = {
package: "test-provider",
api: { type: "native", settings: {} },
request: {
headers: {},
body: {},
},
models: {},
}
@@ -89,7 +93,6 @@ describe("Config", () => {
provider: {
bedrock: {
npm: "@ai-sdk/amazon-bedrock",
api: "https://bedrock.example.com",
options: {
headers: { "x-test": "1" },
body: { trace: true },
@@ -100,9 +103,13 @@ describe("Config", () => {
},
})
expect(migrated.providers?.bedrock).toMatchObject({
package: "aisdk:@ai-sdk/amazon-bedrock",
settings: { baseURL: "https://bedrock.example.com", region: "us-east-1", profile: "dev" },
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({
headers: { "x-test": "1" },
body: { trace: true },
})
@@ -578,31 +585,34 @@ describe("Config", () => {
})
expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
expect(documents[0]?.info.providers?.custom).toMatchObject({
settings: { apiKey: "secret" },
request: { body: { apiKey: "secret" } },
models: {
model: {
settings: { reasoningEffort: "high" },
variants: [{ id: "fast", settings: { temperature: 0.2 } }],
request: { body: { reasoningEffort: "high" } },
variants: [{ id: "fast", body: { temperature: 0.2 } }],
},
},
})
expect(documents[0]?.info.providers?.openai).toMatchObject({
package: "aisdk:@ai-sdk/openai",
settings: { apiKey: "secret", organization: "org" },
api: { settings: {} },
request: { headers: { Authorization: "Bearer secret", "OpenAI-Organization": "org" } },
models: {
model: {
settings: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" },
variants: [{ id: "high", settings: { reasoningEffort: "high", reasoningSummary: "auto" } }],
request: {
body: { temperature: 0.3, reasoning_effort: "high", service_tier: "priority" },
},
variants: [{ id: "high", body: { reasoning_effort: "high", reasoning_summary: "auto" } }],
},
},
})
expect(documents[0]?.info.providers?.anthropic).toMatchObject({
models: {
model: {
settings: {
effort: "high",
taskBudget: 4096,
metadata: { userId: "user-1" },
request: {
body: {
output_config: { effort: "high", task_budget: 4096 },
metadata: { user_id: "user-1" },
},
},
},
},
@@ -2,47 +2,221 @@ import { describe, expect, test } from "bun:test"
import { ConfigProviderOptionsV1 } from "@opencode-ai/core/v1/config/provider-options"
describe("ConfigProviderOptionsV1", () => {
test("splits provider overlays without changing package settings", () => {
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")
expect(
ConfigProviderOptionsV1.provider({
lowerer.provider({
apiKey: "secret",
baseURL: "https://openai.example/v1",
organization: "org",
headers: { "x-test": "1", invalid: true },
project: "project",
headers: { "x-test": "1" },
body: { store: true },
nested: { camelCase: true },
timeout: 1000,
}),
).toEqual({
settings: {
apiKey: "secret",
baseURL: "https://openai.example/v1",
organization: "org",
nested: { camelCase: true },
url: "https://openai.example/v1",
headers: {
Authorization: "Bearer secret",
"OpenAI-Organization": "org",
"OpenAI-Project": "project",
"x-test": "1",
},
headers: { "x-test": "1" },
body: { store: true },
settings: { timeout: 1000 },
})
})
test("keeps model and variant options unchanged", () => {
expect(
ConfigProviderOptionsV1.model({
lowerer.request({
reasoningEffort: "high",
taskBudget: 1024,
metadata: { userId: "user" },
textVerbosity: "low",
text: { outputFormat: "plain" },
nestedValue: { camelCase: true },
}),
).toEqual({
reasoningEffort: "high",
taskBudget: 1024,
metadata: { userId: "user" },
reasoning_effort: "high",
text: { output_format: "plain", verbosity: "low" },
nested_value: { camel_case: true },
})
})
test("uses mechanical lowering for custom provider options", () => {
expect(ConfigProviderOptionsV1.provider({ enabled: true })).toEqual({
settings: { enabled: 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 },
}),
).toEqual({
output_config: { effort: "high", task_budget: 1024 },
metadata: { user_id: "user", trace_id: "trace" },
nested_value: { camel_case: true },
})
})
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",
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" },
})
},
)
})
+25 -24
View File
@@ -45,6 +45,13 @@ 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", () => {
@@ -61,8 +68,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
opencode: {
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://opencode.test/v1" },
api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
models: {
"alpha-gpt-next": {
variants: [
@@ -113,8 +119,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
opencode: {
package: "aisdk:@ai-sdk/openai",
settings: { baseURL: "https://opencode.test/v1" },
api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
},
},
}),
@@ -139,7 +144,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" },
})
@@ -164,8 +169,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
custom: {
name: "Configured",
env: ["CUSTOM_API_KEY"],
package: "native",
headers: { first: "first", shared: "first" },
api: { type: "native", settings: {} },
request: request({ first: "first", shared: "first" }),
models: {
chat: {
name: "First",
@@ -173,8 +178,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
disabled: true,
limit: { context: 100, output: 50 },
cost: { input: 1, output: 2 },
settings: { retained: true },
headers: { first: "first", shared: "first" },
request: request({ first: "first", shared: "first" }, "retained"),
variants: [
{
id: "fast",
@@ -193,18 +197,17 @@ describe("ConfigProviderPlugin.Plugin", () => {
model: "custom/default",
providers: {
custom: {
package: "aisdk:custom-sdk",
settings: { baseURL: "https://example.test" },
headers: { last: "last", shared: "last" },
api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" },
request: request({ last: "last", shared: "last" }),
models: {
default: {
name: "Default",
},
chat: {
modelID: "api-chat",
api: { id: "api-chat" },
name: "Last",
limit: { output: 75 },
headers: { last: "last", shared: "last" },
request: request({ last: "last", shared: "last" }),
variants: [
{
id: "fast",
@@ -244,24 +247,22 @@ describe("ConfigProviderPlugin.Plugin", () => {
})
expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
expect(provider.disabled).toBeUndefined()
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(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(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.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([
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([
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" })
}),
),
)
+46 -2
View File
@@ -116,7 +116,7 @@ describe("EventV2", () => {
const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" })
expect(event.type).toBe("test.versioned")
expect(event.durable?.version).toBe(2)
expect(event.durable.version).toBe(2)
}),
)
@@ -138,6 +138,50 @@ describe("EventV2", () => {
}),
)
it.effect("preserves same-type projector routing across durable versions", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const historical = EventV2.define({
type: "test.projector-version",
durable: { version: 1, aggregate: "id" },
schema: { id: Schema.String },
})
const current = EventV2.define({
type: "test.projector-version",
durable: { version: 2, aggregate: "id" },
schema: { id: Schema.String },
})
const received = new Array<EventV2.Payload>()
yield* events.project(historical, (event) => Effect.sync(() => received.push(event)))
const published = yield* events.publish(current, { id: "aggregate" })
expect(received).toEqual([published])
}),
)
it.effect("preserves same-type subscription routing across durable versions", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const historical = EventV2.define({
type: "test.subscription-version",
durable: { version: 1, aggregate: "id" },
schema: { id: Schema.String },
})
const current = EventV2.define({
type: "test.subscription-version",
durable: { version: 2, aggregate: "id" },
schema: { id: Schema.String },
})
const fiber = yield* events.subscribe(historical).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
const published = yield* events.publish(current, { id: "aggregate" })
expect(Array.from(yield* Fiber.join(fiber))).toEqual([published])
}),
)
it.effect("publishes to typed and wildcard subscriptions", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
@@ -764,7 +808,7 @@ describe("EventV2", () => {
const replayed = {
id: published.id,
type: EventV2.versionedType(DurableMessage.type, 1),
seq: published.durable!.seq,
seq: published.durable.seq,
aggregateID,
data: published.data,
}
+1 -1
View File
@@ -168,7 +168,7 @@ describe("LocationServiceMap", () => {
providers: {
unavailable: {
name: "Unavailable",
package: "test-provider",
api: { type: "native", settings: {} },
models: { chat: { disabled: true } },
},
},
+12 -11
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,28 +272,29 @@ function agentInfo(value: AgentV2.Info) {
function providerInfo(value: ProviderV2.MutableInfo) {
return {
...value,
settings: value.settings && { ...value.settings },
headers: value.headers && { ...value.headers },
body: value.body && { ...value.body },
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
}
}
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
return {
...value,
settings: value.settings && { ...value.settings },
headers: value.headers && { ...value.headers },
body: value.body && { ...value.body },
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
capabilities: {
...value.capabilities,
input: [...value.capabilities.input],
output: [...value.capabilities.output],
},
variants: value.variants?.map((variant) => ({
request: {
...value.request,
headers: { ...value.request.headers },
body: { ...value.request.body },
},
variants: value.variants.map((variant) => ({
...variant,
settings: variant.settings && { ...variant.settings },
headers: variant.headers && { ...variant.headers },
body: variant.body && { ...variant.body },
headers: { ...variant.headers },
body: { ...variant.body },
})),
time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
@@ -28,8 +28,7 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/alibaba",
options: { name: "alibaba" },
@@ -46,8 +45,7 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "alibaba" },
@@ -64,8 +62,7 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
modelID: ModelV2.ID.make("qwen"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/alibaba",
options: { name: "custom-alibaba", apiKey: "test" },
@@ -77,18 +74,17 @@ describe("AlibabaPlugin", () => {
}),
)
it.effect("uses the default languageModel(modelID) behavior", () =>
it.effect("uses the old default languageModel(api.id) 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")),
modelID: ModelV2.ID.make("qwen-plus"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
})
const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
const language = result.sdk?.languageModel(item.modelID ?? item.id)
const language = result.sdk?.languageModel(item.api.id)
expect(language?.modelId).toBe("qwen-plus")
expect(language?.provider).toBe("alibaba.chat")
}),
@@ -79,30 +79,31 @@ function openAIUrl(language: unknown, path: string, modelId: string) {
}
describe("AmazonBedrockPlugin", () => {
it.effect("moves endpoint option to baseURL", () =>
it.effect("moves endpoint option to api URL", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
const bedrock = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
package: "aisdk:@ai-sdk/amazon-bedrock",
headers: {},
settings: { endpoint: "https://bedrock.example" },
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
request: {
headers: {},
body: { endpoint: "https://bedrock.example" },
},
})
catalog.provider.update(bedrock.id, (item) => {
item.package = bedrock.package
item.package = bedrock.package
item.headers = bedrock.headers
item.settings = { endpoint: "https://bedrock.example" }
item.api = bedrock.api
item.request = bedrock.request
})
})
yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
expect(result).toMatchObject({
package: "aisdk:@ai-sdk/amazon-bedrock",
settings: { baseURL: "https://bedrock.example" },
expect(result.api).toEqual({
type: "aisdk",
package: "@ai-sdk/amazon-bedrock",
url: "https://bedrock.example",
})
expect(result.settings?.endpoint).toBeUndefined()
expect(result.request.body.endpoint).toBeUndefined()
}),
)
@@ -115,8 +116,7 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -141,8 +141,7 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -176,8 +175,11 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -197,8 +199,7 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock", region: "eu-west-1" },
@@ -217,8 +218,7 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -237,8 +237,7 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -258,8 +257,7 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -288,8 +286,7 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -317,8 +314,11 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
modelID: ModelV2.ID.make("openai.gpt-5.5"),
package: "aisdk:@ai-sdk/amazon-bedrock/mantle",
api: {
id: ModelV2.ID.make("openai.gpt-5.5"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
}),
package: "@ai-sdk/amazon-bedrock/mantle",
options: {
@@ -345,8 +345,11 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
modelID: ModelV2.ID.make("openai.gpt-5.5"),
package: "aisdk:@ai-sdk/amazon-bedrock/mantle",
api: {
id: ModelV2.ID.make("openai.gpt-5.5"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
}),
sdk: fakeSelectorSdk(calls),
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
@@ -354,8 +357,11 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
package: "aisdk:@ai-sdk/amazon-bedrock/mantle",
api: {
id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
}),
sdk: fakeSelectorSdk(calls),
options: { region: "us-east-1" },
@@ -372,8 +378,11 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:@ai-sdk/amazon-bedrock/anthropic",
api: {
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/anthropic",
},
}),
package: "@ai-sdk/amazon-bedrock/anthropic",
options: { name: "amazon-bedrock" },
@@ -400,8 +409,11 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -432,8 +444,7 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -441,8 +452,7 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -450,8 +460,11 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -459,8 +472,7 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-northeast-1" },
@@ -468,8 +480,7 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-southeast-2" },
@@ -494,8 +505,7 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -581,8 +591,7 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
modelID: ModelV2.ID.make(item.modelID),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: item.region },
@@ -601,8 +610,7 @@ 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")),
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -31,21 +31,19 @@ describe("AnthropicPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
package: "aisdk:@ai-sdk/anthropic",
headers: { Existing: "1" },
settings: {},
api: { type: "aisdk", package: "@ai-sdk/anthropic" },
request: { headers: { Existing: "1" }, body: {} },
})
catalog.provider.update(item.id, (draft) => {
draft.package = item.package
draft.package = item.package
draft.headers = item.headers
draft.api = item.api
draft.request = item.request
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.["anthropic-beta"]).toBe(
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.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)).headers?.Existing).toBe("1")
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
}),
)
@@ -54,7 +52,9 @@ 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)).headers?.["anthropic-beta"]).toBeUndefined()
expect(
required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
).toBeUndefined()
}),
)
@@ -66,8 +66,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:@ai-sdk/anthropic",
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
}),
package: "@ai-sdk/anthropic",
options: { name: "custom-anthropic", apiKey: "test" },
@@ -84,8 +83,7 @@ describe("AnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:@ai-sdk/anthropic",
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
}),
package: "@ai-sdk/anthropic",
options: { name: "anthropic", apiKey: "test" },
@@ -66,16 +66,18 @@ describe("AzureCognitiveServicesPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
item.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
})
})
yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
expect(result).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://cognitive.cognitiveservices.azure.com/openai" },
expect(result.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://cognitive.cognitiveservices.azure.com/openai",
})
expect(result.settings?.resourceName).toBeUndefined()
expect(result.request.body.baseURL).toBeUndefined()
expect(result.request.body.resourceName).toBeUndefined()
}),
),
)
@@ -87,28 +89,26 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")),
package: "aisdk:@ai-sdk/openai-compatible",
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
})
const openai = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
package: "aisdk:test-provider",
api: { type: "aisdk", package: "test-provider" },
})
catalog.provider.update(azure.id, (item) => {
item.package = azure.package
item.package = azure.package
item.api = azure.api
})
catalog.provider.update(openai.id, (item) => {
item.package = openai.package
item.package = openai.package
item.api = openai.api
})
})
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.settings?.baseURL).toBeUndefined()
expect(azure).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible" })
expect(openai.settings?.baseURL).toBeUndefined()
expect(openai).toMatchObject({ package: "aisdk:test-provider" })
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" })
}),
),
)
@@ -122,8 +122,7 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
@@ -141,8 +140,7 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -150,8 +148,7 @@ describe("AzureCognitiveServicesPlugin", () => {
const ignored = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -171,8 +168,7 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
modelID: ModelV2.ID.make("messages-deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
options: {},
@@ -180,8 +176,7 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
modelID: ModelV2.ID.make("chat-deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
options: {},
@@ -189,8 +184,7 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
modelID: ModelV2.ID.make("language-deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "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.package = ProviderV2.aisdk("@ai-sdk/azure")
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}),
),
)
@@ -82,21 +82,18 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
package: "aisdk:@ai-sdk/azure",
headers: {},
settings: { resourceName: "from-config" },
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "from-config" } },
})
catalog.provider.update(azure.id, (item) => {
item.package = azure.package
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: "from-config" }
item.api = azure.api
item.request = azure.request
})
catalog.provider.update(ProviderV2.ID.openai, () => {})
})
yield* addPlugin()
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()
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()
}),
),
)
@@ -108,19 +105,16 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
package: "aisdk:@ai-sdk/azure",
headers: {},
settings: { resourceName: "" },
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "" } },
})
catalog.provider.update(azure.id, (item) => {
item.package = azure.package
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: "" }
item.api = azure.api
item.request = azure.request
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}),
),
)
@@ -132,19 +126,16 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
package: "aisdk:@ai-sdk/azure",
headers: {},
settings: { resourceName: " " },
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: " " } },
})
catalog.provider.update(azure.id, (item) => {
item.package = azure.package
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: " " }
item.api = azure.api
item.request = azure.request
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}),
),
)
@@ -158,8 +149,7 @@ describe("AzurePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/azure",
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
@@ -178,8 +168,7 @@ describe("AzurePlugin", () => {
.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/azure",
options: { name: "azure" },
@@ -199,8 +188,7 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
@@ -218,8 +206,7 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
@@ -237,10 +224,8 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
headers: {},
settings: { useCompletionUrls: true },
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
request: { headers: {}, body: { useCompletionUrls: true } },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -258,8 +243,7 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -267,8 +251,7 @@ describe("AzurePlugin", () => {
const ignored = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
modelID: ModelV2.ID.make("deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -291,8 +274,7 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
modelID: ModelV2.ID.make("messages-deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
options: {},
@@ -300,8 +282,7 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
modelID: ModelV2.ID.make("language-deployment"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "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.package = ProviderV2.aisdk("@ai-sdk/cerebras")
item.headers = { ...item.headers, Existing: "1" }
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
item.request.headers.Existing = "1"
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.request.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")))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.request.headers).toEqual({})
}),
)
@@ -69,8 +69,11 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
}),
package: "@ai-sdk/cerebras",
options: { name: "custom-cerebras", apiKey: "test" },
@@ -92,8 +95,11 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
}),
package: "@ai-sdk/cerebras",
options: { name: "configured-cerebras", apiKey: "test" },
@@ -114,8 +120,11 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
}),
package: "@ai-sdk/groq",
options: { name: "custom-cerebras", apiKey: "test" },
@@ -118,8 +118,7 @@ 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")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -140,8 +139,7 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: {
@@ -185,8 +183,7 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: {
@@ -215,8 +212,7 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: {
@@ -253,8 +249,7 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: {
@@ -285,8 +280,7 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -308,8 +302,7 @@ 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")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -332,8 +325,7 @@ 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")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -362,8 +354,7 @@ 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")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
@@ -389,8 +380,11 @@ describe("CloudflareAIGatewayPlugin", () => {
ProviderV2.ID.make("cloudflare-ai-gateway"),
ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
),
modelID: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -418,8 +412,7 @@ 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")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "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.package = ProviderV2.aisdk("test-provider")
provider.api = { type: "aisdk", package: "test-provider" }
}),
)
yield* addPlugin()
@@ -95,16 +95,15 @@ 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")),
modelID: ModelV2.ID.make("@cf/model"),
package: provider.package,
settings: provider.settings,
api: { id: ModelV2.ID.make("@cf/model"), ...provider.api },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
})
expect(provider).toMatchObject({
package: "aisdk:test-provider",
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" },
expect(provider.api).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1",
})
expect(sdk.sdk).toBeDefined()
}),
@@ -117,14 +116,14 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.package = ProviderV2.aisdk("test-provider")
provider.settings = { ...provider.settings, baseURL: "https://proxy.example/v1" }
provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
}),
)
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
package: "aisdk:test-provider",
settings: { baseURL: "https://proxy.example/v1" },
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://proxy.example/v1",
})
}),
),
@@ -139,9 +138,12 @@ 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")),
modelID: ModelV2.ID.make("@cf/model"),
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://proxy.example/v1" },
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
@@ -157,14 +159,15 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.package = ProviderV2.aisdk("test-provider")
provider.settings = { ...provider.settings, accountId: "configured-acct" }
provider.api = { type: "aisdk", package: "test-provider" }
provider.request.body.accountId = "configured-acct"
}),
)
yield* addPlugin()
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" },
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",
})
}),
),
@@ -179,9 +182,12 @@ 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")),
modelID: ModelV2.ID.make("@cf/model"),
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://proxy.example/v1" },
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -208,9 +214,12 @@ 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")),
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" },
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",
},
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -234,8 +243,7 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("@cf/api-model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -254,9 +262,12 @@ 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")),
modelID: ModelV2.ID.make("@cf/model"),
package: "aisdk:@ai-sdk/anthropic",
settings: { baseURL: "https://proxy.example/v1" },
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/anthropic",
url: "https://proxy.example/v1",
},
}),
package: "@ai-sdk/anthropic",
options: { name: "cloudflare-workers-ai" },
@@ -56,8 +56,7 @@ describe("CoherePlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
modelID: ModelV2.ID.make("command"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cohere" },
@@ -67,8 +66,7 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
modelID: ModelV2.ID.make("command"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/cohere",
options: { name: "cohere" },
@@ -85,8 +83,7 @@ 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")),
modelID: ModelV2.ID.make("command-r-plus"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/cohere",
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
@@ -111,8 +108,7 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("command-r-plus"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
}),
sdk,
options: {},
@@ -48,8 +48,7 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
@@ -67,8 +66,7 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
}),
package: "@ai-sdk/deepinfra",
options: { name: "custom-deepinfra", apiKey: "test" },
@@ -87,8 +85,7 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra", apiKey: "test" },
@@ -114,8 +111,7 @@ describe("DeepInfraPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
}),
package: item,
options: { name: "deepinfra" },
@@ -126,8 +122,7 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:@ai-sdk/deepinfra",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
@@ -146,14 +141,17 @@ 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")),
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
package: "aisdk:@ai-sdk/deepinfra",
api: {
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
type: "aisdk",
package: "@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.modelID ?? result.model.id)
const language = result.language ?? result.sdk.languageModel(result.model.api.id)
expect(language.provider).toBe("deepinfra.chat")
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
}),
@@ -53,8 +53,7 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
modelID: ModelV2.ID.make("test-model"),
package: ProviderV2.aisdk(fixtureProvider),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
}),
package: fixtureProvider,
options: { name: "custom", marker: "dynamic" },
@@ -72,8 +71,7 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
modelID: ModelV2.ID.make("test-model"),
package: ProviderV2.aisdk(fixtureProvider),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
}),
package: fixtureProvider,
options: { name: "custom", marker: "dynamic" },
@@ -90,8 +88,7 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
modelID: ModelV2.ID.make("test-model"),
package: ProviderV2.aisdk(fixtureProvider),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
}),
package: fixtureProvider,
options: { name: "custom-provider", marker: "dynamic" },
@@ -107,8 +104,7 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
modelID: ModelV2.ID.make("test-model"),
package: "aisdk:fixture-provider",
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" },
}),
package: "fixture-provider",
options: { name: "npm-provider", marker: "npm" },
@@ -125,8 +121,7 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("alias"),
package: "aisdk:fixture-provider",
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
}),
)
.pipe(Effect.exit)
@@ -143,8 +138,7 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("alias"),
package: "aisdk:file:///missing/provider-factory.js",
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "file:///missing/provider-factory.js" },
}),
)
.pipe(Effect.exit)
@@ -163,8 +157,7 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("alias"),
package: "aisdk:fixture-provider",
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
}),
)
.pipe(Effect.exit)
@@ -173,7 +166,7 @@ describe("DynamicProviderPlugin", () => {
}),
)
itWithAISDK.effect("uses the model modelID for the default language model", () =>
itWithAISDK.effect("uses the model api.id for the default language model", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -181,8 +174,7 @@ describe("DynamicProviderPlugin", () => {
const language = yield* aisdk.language(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("test-model-api"),
package: ProviderV2.aisdk(fixtureProvider),
api: { id: ModelV2.ID.make("test-model-api"), type: "aisdk", package: fixtureProvider },
}),
)
expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } })
@@ -45,8 +45,7 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/gateway",
options: { name: "gateway" },
@@ -66,8 +65,11 @@ 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")),
modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("anthropic/claude-sonnet-4"),
type: "aisdk",
package: "test-provider",
},
}),
package: "@ai-sdk/gateway",
options: { name: "vercel", apiKey: "test-key" },
@@ -89,8 +91,7 @@ describe("GatewayPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
modelID: ModelV2.ID.make(modelID),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/vercel",
options: { name: "vercel" },
@@ -100,8 +101,7 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
modelID: ModelV2.ID.make(modelID),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/gateway",
options: { name: "vercel" },
@@ -47,8 +47,7 @@ describe("GithubCopilotPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "github-copilot" },
@@ -56,8 +55,7 @@ describe("GithubCopilotPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/github-copilot",
options: { name: "github-copilot" },
@@ -76,8 +74,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
modelID: ModelV2.ID.make("claude-sonnet-4"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -95,8 +92,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("claude-sonnet-4"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -114,8 +110,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -123,8 +118,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
modelID: ModelV2.ID.make("gpt-5.1-codex"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -132,8 +126,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
modelID: ModelV2.ID.make("gpt-4o"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -141,8 +134,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
modelID: ModelV2.ID.make("gpt-5-mini"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -150,8 +142,7 @@ 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")),
modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -175,8 +166,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -184,8 +174,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
modelID: ModelV2.ID.make("gpt-5-mini"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -193,8 +182,7 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
modelID: ModelV2.ID.make("claude-sonnet-4"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -242,8 +230,7 @@ describe("GithubCopilotPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -70,8 +70,7 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
}),
package: "gitlab-ai-provider",
options: { name: "gitlab" },
@@ -108,8 +107,7 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
}),
package: "gitlab-ai-provider",
options: { name: "gitlab" },
@@ -134,8 +132,7 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
}),
package: "gitlab-ai-provider",
options: {
@@ -176,8 +173,7 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai",
options: { name: "gitlab" },
@@ -196,10 +192,11 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
modelID: ModelV2.ID.make("duo-workflow-custom"),
package: "aisdk:test-provider",
headers: {},
settings: { workflowRef: "ref", workflowDefinition: "definition" },
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
request: {
headers: {},
body: { workflowRef: "ref", workflowDefinition: "definition" },
},
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
@@ -230,8 +227,7 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
modelID: ModelV2.ID.make("duo-workflow-exact"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" },
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
@@ -249,7 +245,7 @@ describe("GitLabPlugin", () => {
}),
)
it.effect("uses provider feature flags instead of model settings feature flags", () =>
it.effect("uses provider feature flags instead of request feature flags", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -258,10 +254,11 @@ describe("GitLabPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
modelID: ModelV2.ID.make("duo-workflow-custom"),
package: "aisdk:test-provider",
headers: {},
settings: { featureFlags: { request_flag: true } },
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
request: {
headers: {},
body: { featureFlags: { request_flag: true } },
},
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
@@ -285,10 +282,8 @@ describe("GitLabPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
modelID: ModelV2.ID.make("claude"),
package: "aisdk:test-provider",
headers: { h: "v" },
settings: {},
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
request: { headers: { h: "v" }, body: {} },
}),
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.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
}),
)
yield* addPlugin(GoogleVertexAnthropicPlugin)
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",
)
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")
}),
),
)
@@ -84,17 +84,18 @@ describe("GoogleVertexAnthropicPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
provider.settings = { ...provider.settings, project: "configured-project", location: "configured-location" }
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
provider.request.body.project = "configured-project"
provider.request.body.location = "configured-location"
}),
)
yield* addPlugin(GoogleVertexAnthropicPlugin)
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project).toBe(
"configured-project",
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
"configured-location",
)
expect(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
).toBe("configured-location")
}),
),
)
@@ -120,8 +121,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
ProviderV2.ID.make("google-vertex-anthropic"),
ModelV2.ID.make("claude-sonnet-4-5"),
),
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex-anthropic" },
@@ -147,8 +147,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
ProviderV2.ID.make("google-vertex-anthropic"),
ModelV2.ID.make("claude-sonnet-4-5"),
),
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex-anthropic" },
@@ -168,8 +167,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "eu" },
@@ -188,8 +186,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
@@ -207,8 +204,7 @@ 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 ")),
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "us" },
@@ -216,8 +212,7 @@ 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 ")),
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
}),
sdk: sdkResult.sdk,
options: {},
@@ -239,8 +234,7 @@ 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 ")),
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: selector(calls) },
options: {},
@@ -258,8 +252,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: selector(calls) },
options: {},
@@ -92,14 +92,17 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://opencode.ai/zen/v1",
}
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
expect(provider.settings).toEqual({ baseURL: "https://opencode.ai/zen/v1" })
expect(provider.request.body).toEqual({})
}),
)
@@ -118,24 +121,21 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
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.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "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.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",
},
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",
})
}),
),
@@ -160,11 +160,10 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
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.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
}),
)
@@ -173,20 +172,21 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/google-vertex",
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/google-vertex",
},
}),
package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" },
})
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(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(vertexOptions[0].project).toBe("vertex-project")
expect(vertexOptions[0].location).toBe("europe-west4")
@@ -209,22 +209,23 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
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.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
provider.settings = { ...provider.settings, project: "config-project", location: "global" }
provider.request.body.project = "config-project"
provider.request.body.location = "global"
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
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" },
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",
})
}),
),
@@ -235,20 +236,21 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
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.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
provider.settings = { ...provider.settings, project: "config-project", location: "eu" }
provider.request.body.project = "config-project"
provider.request.body.location = "eu"
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider).toMatchObject({
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" },
expect(provider.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu",
})
}),
)
@@ -268,14 +270,14 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex")
provider.settings = { ...provider.settings, project: "config-project" }
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
provider.request.body.project = "config-project"
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.settings?.project).toBe("config-project")
expect(provider.settings?.location).toBe("us-central1")
expect(provider.request.body.project).toBe("config-project")
expect(provider.request.body.location).toBe("us-central1")
}),
),
)
@@ -295,8 +297,11 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/google-vertex",
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/google-vertex",
},
}),
package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" },
@@ -340,8 +345,11 @@ describe("GoogleVertexPlugin", () => {
aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/openai-compatible",
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
},
}),
package: "@ai-sdk/openai-compatible",
options: { name: "google-vertex" },
@@ -368,8 +376,7 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
modelID: ModelV2.ID.make(" gemini-2.5-pro "),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -27,8 +27,7 @@ describe("GooglePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/google",
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
}),
package: "@ai-sdk/google",
options: { name: "custom-google", apiKey: "test" },
@@ -46,8 +45,7 @@ describe("GooglePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
modelID: ModelV2.ID.make("gemini"),
package: "aisdk:@ai-sdk/google",
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
}),
package: "@ai-sdk/google-vertex",
options: { name: "google" },
@@ -64,8 +62,7 @@ describe("GooglePlugin", () => {
const sdkEvent = yield* aisdk.runSDK({
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",
api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" },
}),
package: "@ai-sdk/google",
options: { name: "custom-google", apiKey: "test" },
@@ -75,29 +72,9 @@ describe("GooglePlugin", () => {
sdk: sdkEvent.sdk,
options: sdkEvent.options,
})
const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
const language = result.language ?? result.sdk.languageModel(result.model.api.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")
}),
)
})
+11 -12
View File
@@ -28,8 +28,7 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
}),
package: "@ai-sdk/groq",
options: { name: "groq" },
@@ -46,8 +45,7 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "groq" },
@@ -64,8 +62,7 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
}),
package: "@ai-sdk/groq/compat",
options: { name: "groq" },
@@ -82,8 +79,7 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
modelID: ModelV2.ID.make("llama"),
package: "aisdk:@ai-sdk/groq",
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
}),
package: "@ai-sdk/groq",
options: { name: "custom-groq", apiKey: "test" },
@@ -97,7 +93,7 @@ describe("GroqPlugin", () => {
}),
)
it.effect("uses the default languageModel(modelID) behavior", () =>
it.effect("uses the default languageModel(api.id) behavior", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -108,13 +104,16 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("llama-api"),
package: "aisdk:@ai-sdk/groq",
api: {
id: ModelV2.ID.make("llama-api"),
type: "aisdk",
package: "@ai-sdk/groq",
},
}),
sdk,
options: { name: "groq", apiKey: "test" },
})
const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
const language = result.language ?? sdk.languageModel(result.model.api.id)
expect(language.modelId).toBe("llama-api")
expect(language.provider).toBe("groq.chat")
}),
+27 -16
View File
@@ -27,19 +27,22 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
provider.headers = { Existing: "value" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
provider.request = { headers: { Existing: "value" }, body: {} }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
}),
)
@@ -48,19 +51,24 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
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")
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")
}),
)
@@ -69,20 +77,23 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
})
catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
provider.package = ProviderV2.aisdk("kilo")
provider.api = { type: "aisdk", package: "kilo" }
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.request.headers).toEqual({})
}),
)
})
@@ -34,20 +34,23 @@ describe("LLMGatewayPlugin", () => {
})
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.llmgateway.io/v1" }
provider.headers = { Existing: "value" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.llmgateway.io/v1",
}
provider.request = { headers: { Existing: "value" }, body: {} }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-Source": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
}),
)
@@ -61,14 +64,17 @@ describe("LLMGatewayPlugin", () => {
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
provider.disabled = true
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://api.llmgateway.io/v1" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "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")))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({})
}),
)
})
@@ -28,8 +28,7 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/mistral",
options: { name: "mistral" },
@@ -46,8 +45,7 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "mistral" },
@@ -70,8 +68,7 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/mistral",
options: { name: "mistral" },
@@ -95,8 +92,7 @@ describe("MistralPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/mistral",
options: { name: "custom-mistral" },
@@ -105,7 +101,7 @@ describe("MistralPlugin", () => {
}),
)
it.effect("leaves Mistral language selection on the default sdk.languageModel(modelID) path", () =>
it.effect("leaves Mistral language selection on the default sdk.languageModel(api.id) path", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -120,13 +116,12 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("mistral-large"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
}),
sdk,
options: {},
})
const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
const language = result.language ?? sdk.languageModel(result.model.api.id)
expect(calls).toEqual(["languageModel:mistral-large"])
expect(language).toBeDefined()
}),
@@ -27,20 +27,23 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://integrate.api.nvidia.com/v1" }
provider.headers = { Existing: "value" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
provider.request = { headers: { Existing: "value" }, body: {} }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.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))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
}),
)
@@ -49,13 +52,16 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://integrate.api.nvidia.com/v1" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -68,14 +74,20 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
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" }
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" },
}
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
@@ -27,8 +27,7 @@ describe("OpenAICompatiblePlugin", () => {
const defaulted = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "custom" },
@@ -36,8 +35,7 @@ describe("OpenAICompatiblePlugin", () => {
const disabled = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "custom", includeUsage: false },
@@ -55,8 +53,7 @@ describe("OpenAICompatiblePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "file:///tmp/@ai-sdk/openai-compatible-provider.js",
options: { name: "custom" },
@@ -79,8 +76,7 @@ describe("OpenAICompatiblePlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "custom-provider", baseURL: "https://example.com/v1" },
@@ -100,8 +96,7 @@ describe("OpenAICompatiblePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai" },
@@ -67,8 +67,7 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai",
options: { name: "custom-openai", apiKey: "test" },
@@ -85,8 +84,7 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "openai" },
@@ -104,8 +102,7 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -124,8 +121,7 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
modelID: ModelV2.ID.make("gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -141,11 +137,10 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
package: "aisdk:@ai-sdk/openai",
api: { type: "aisdk", package: "@ai-sdk/openai" },
})
catalog.provider.update(item.id, (draft) => {
draft.package = item.package
draft.package = item.package
draft.api = item.api
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
@@ -164,11 +159,10 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
package: "aisdk:test-provider",
api: { type: "aisdk", package: "test-provider" },
})
catalog.provider.update(item.id, (draft) => {
draft.package = item.package
draft.package = item.package
draft.api = item.api
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
@@ -133,11 +133,13 @@ describe("OpencodePlugin", () => {
expect(provider).toMatchObject({
name: "Remote",
integrationID: "opencode",
package: "aisdk:@ai-sdk/openai-compatible",
settings: { baseURL: `${server.url.origin}/v1` },
api: {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: `${server.url.origin}/v1`,
},
})
expect(provider.headers).toEqual({ "x-org-id": "org" })
expect(provider.settings).toMatchObject({ custom: "value" })
expect(provider.request).toEqual({ headers: { "x-org-id": "org" }, body: { 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")))
@@ -148,16 +150,12 @@ describe("OpencodePlugin", () => {
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 },
})
expect(model.settings).toEqual({
baseURL: `${server.url.origin}/v1`,
custom: "value",
temperature: 0.5,
})
expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 })
expect(model.variants).toEqual([
{
id: ModelV2.VariantID.make("high"),
headers: {},
settings: { temperature: 0.2 },
body: { temperature: 0.2 },
},
])
expect(
@@ -177,12 +175,11 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
package: "aisdk:test-provider",
api: { type: "aisdk", package: "test-provider" },
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
modelID: ModelV2.ID.make("paid"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -191,7 +188,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
}),
),
@@ -204,12 +201,11 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
package: "aisdk:test-provider",
api: { type: "aisdk", package: "test-provider" },
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
modelID: ModelV2.ID.make("free"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("free"), type: "aisdk", package: "test-provider" },
cost: cost(0),
})
catalog.provider.update(provider.id, () => {})
@@ -218,7 +214,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
}),
),
@@ -231,12 +227,11 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
package: "aisdk:test-provider",
api: { type: "aisdk", package: "test-provider" },
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
modelID: ModelV2.ID.make("output-only"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("output-only"), type: "aisdk", package: "test-provider" },
cost: cost(0, 1),
})
catalog.provider.update(provider.id, () => {})
@@ -245,7 +240,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(
true,
)
@@ -260,12 +255,11 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
package: "aisdk:test-provider",
api: { type: "aisdk", package: "test-provider" },
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
modelID: ModelV2.ID.make("paid"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -274,7 +268,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
@@ -294,12 +288,11 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
package: "aisdk:test-provider",
api: { type: "aisdk", package: "test-provider" },
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
modelID: ModelV2.ID.make("paid"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -308,7 +301,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
@@ -321,26 +314,26 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
package: "aisdk:test-provider",
headers: {},
settings: { apiKey: "configured" },
api: { type: "aisdk", package: "test-provider" },
request: {
headers: {},
body: { apiKey: "configured" },
},
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
modelID: ModelV2.ID.make("paid"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
cost: cost(1),
})
catalog.provider.update(provider.id, (draft) => {
draft.headers = provider.headers
draft.settings = { apiKey: "configured" }
draft.request = provider.request
})
catalog.model.update(provider.id, model.id, (draft) => {
draft.cost = [...model.cost]
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("configured")
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
@@ -353,12 +346,11 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
package: "aisdk:test-provider",
api: { type: "aisdk", package: "test-provider" },
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
modelID: ModelV2.ID.make("paid"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -367,7 +359,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
@@ -30,20 +30,19 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
provider.headers = { Existing: "value" }
provider.settings = {}
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
provider.request = { headers: { Existing: "value" }, body: {} }
})
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({})
}),
)
@@ -56,8 +55,7 @@ describe("OpenRouterPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "openrouter" },
@@ -67,8 +65,7 @@ describe("OpenRouterPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
modelID: ModelV2.ID.make("openai/gpt-5"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "@openrouter/ai-sdk-provider",
options: { name: "custom" },
@@ -82,7 +79,7 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
})
catalog.provider.update(ProviderV2.ID.openai, () => {})
catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {})
@@ -41,8 +41,7 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/perplexity",
options: { name: "perplexity" },
@@ -59,8 +58,7 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/perplexity-compatible",
options: { name: "perplexity" },
@@ -77,8 +75,7 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/perplexity",
options: { name: "perplexity" },
@@ -95,8 +92,7 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/perplexity",
options: { name: "custom-perplexity" },
@@ -114,8 +110,7 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("sonar"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -49,8 +49,7 @@ 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")),
modelID: ModelV2.ID.make("sap-model"),
package: ProviderV2.aisdk(fixtureProvider),
api: { id: ModelV2.ID.make("sap-model"), type: "aisdk", package: fixtureProvider },
})
}
@@ -57,8 +57,7 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
modelID: ModelV2.ID.make("gpt-4"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai",
options: { name: "openai" },
@@ -76,8 +75,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -96,8 +94,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -120,8 +117,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -140,8 +136,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -164,8 +159,7 @@ 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")),
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -41,8 +41,7 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/togetherai",
options: { name: "togetherai" },
@@ -60,8 +59,7 @@ describe("TogetherAIPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "file:///tmp/@ai-sdk/togetherai-provider.js",
options: { name: "togetherai" },
@@ -71,8 +69,7 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/togetherai",
options: { name: "togetherai" },
@@ -90,8 +87,7 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/togetherai",
options: { name: "custom-togetherai" },
@@ -114,8 +110,11 @@ describe("TogetherAIPlugin", () => {
ProviderV2.ID.make("togetherai"),
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
),
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
package: "aisdk:test-provider",
api: {
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
type: "aisdk",
package: "test-provider",
},
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -123,9 +122,7 @@ describe("TogetherAIPlugin", () => {
expect(result.language).toBeUndefined()
expect(calls).toEqual([])
expect(
result.language ?? fakeSelectorSdk(calls).languageModel(result.model.modelID ?? result.model.id),
).toBeDefined()
expect(result.language ?? fakeSelectorSdk(calls).languageModel(result.model.api.id)).toBeDefined()
expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"])
}),
)
@@ -41,8 +41,7 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "venice-ai-sdk-provider",
options: { name: "venice" },
@@ -59,8 +58,7 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "venice-ai-sdk-provider",
options: { name: "custom-venice", apiKey: "test" },
@@ -78,8 +76,7 @@ describe("VenicePlugin", () => {
const similar = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "file:///tmp/venice-ai-sdk-provider.js",
options: { name: "venice" },
@@ -87,8 +84,7 @@ describe("VenicePlugin", () => {
const other = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
modelID: ModelV2.ID.make("model"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "venice" },
@@ -107,8 +103,7 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("alias"),
package: "aisdk:test-provider",
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "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.package = ProviderV2.aisdk("@ai-sdk/vercel")
provider.headers = { ...provider.headers, Existing: "1" }
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
provider.request.headers.Existing = "1"
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).toEqual({
Existing: "1",
"http-referer": "https://opencode.ai/",
"x-title": "opencode",
@@ -43,12 +43,14 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/vercel")
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
}),
)
yield* addPlugin()
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")
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")
}),
)
@@ -60,8 +62,7 @@ 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")),
modelID: ModelV2.ID.make("v0-1.0-md"),
package: "aisdk:@ai-sdk/vercel",
api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" },
}),
package: "@ai-sdk/vercel",
options: { name: "custom-vercel" },
@@ -76,7 +77,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")))?.headers).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.request.headers).toEqual({})
}),
)
})
+6 -11
View File
@@ -42,8 +42,7 @@ describe("XAIPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
}),
package: "@ai-sdk/openai-compatible",
options: {},
@@ -52,8 +51,7 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
}),
package: "@ai-sdk/xai",
options: {},
@@ -73,8 +71,7 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
}),
package: "@ai-sdk/xai",
options: {},
@@ -84,7 +81,7 @@ describe("XAIPlugin", () => {
}),
)
it.effect("uses responses with the model modelID for xAI language models", () =>
it.effect("uses responses with the model api.id for xAI language models", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -94,8 +91,7 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -116,8 +112,7 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
modelID: ModelV2.ID.make("grok-4"),
package: "aisdk:@ai-sdk/xai",
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -32,14 +32,17 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
})
})
yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
expect(result.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
expect(Object.keys(required(result.headers)).sort()).toEqual(["HTTP-Referer", "X-Title"])
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"])
}),
)
@@ -48,14 +51,17 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
provider.headers = { ...provider.headers, Existing: "value" }
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
provider.request.headers.Existing = "value"
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@@ -68,14 +74,17 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
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" }
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" }
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
@@ -87,12 +96,12 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
+15 -9
View File
@@ -36,18 +36,21 @@ describe("VariantPlugin", () => {
const service = yield* Catalog.Service
yield* service.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
})
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
model.modelID = ModelV2.ID.make("glm-5.2")
model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
model.api = {
id: ModelV2.ID.make("glm-5.2"),
type: "aisdk",
package: "@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", settings: { reasoningEffort: "high" } }),
expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
expect.objectContaining({ id: "high", body: { reasoning_effort: "high" } }),
expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }),
])
}),
)
@@ -57,16 +60,19 @@ 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.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" } }]
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: {} }]
})
})
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", settings: { reasoningEffort: "max" } }),
expect.objectContaining({ id: "max", body: { reasoning_effort: "max" } }),
])
}),
)
+58 -97
View File
@@ -1,6 +1,5 @@
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"
@@ -13,22 +12,26 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { it } from "./lib/effect"
type Transport = {
readonly package: string
readonly settings?: Record<string, unknown>
}
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> }
const model = (transport: Transport, variants?: NonNullable<ModelV2.Info["variants"]>) =>
const model = (api: Api, variants: 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",
...transport,
settings: { apiKey: "secret", ...transport.settings },
api: { id: ModelV2.ID.make("api-test-model"), ...api },
capabilities: { tools: true, input: ["text"], output: ["text"] },
headers: { "x-test": "header" },
body: { custom_extension: { enabled: true } },
request: {
headers: { "x-test": "header" },
body: { apiKey: "secret", custom_extension: { enabled: true } },
},
variants,
time: { released: 0 },
cost: [],
@@ -37,49 +40,11 @@ const model = (transport: Transport, variants?: NonNullable<ModelV2.Info["varian
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("constructs native provider package models mechanically", () =>
it.effect("maps catalog OpenAI AI SDK models into native Responses routes", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
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" } }),
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
)
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
@@ -98,7 +63,7 @@ describe("SessionRunnerModel", () => {
it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }),
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
@@ -112,15 +77,12 @@ describe("SessionRunnerModel", () => {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({
package: "@opencode-ai/llm/providers/openai-compatible",
settings: {
apiKey: "settings-secret",
baseURL: "https://compatible.example/v1",
compatibility: "strict",
},
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://compatible.example/v1",
settings: { apiKey: "settings-secret", compatibility: "strict" },
}),
headers: {},
body: {},
request: { headers: {}, body: {} },
}),
)
const request = LLM.request({ model: resolved, prompt: "Hello" })
@@ -139,7 +101,7 @@ describe("SessionRunnerModel", () => {
it.effect("overlays selected OpenAI Session variant bodies", () =>
Effect.gen(function* () {
const catalog = model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }, [
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [
{
id: ModelV2.VariantID.make("high"),
headers: { "x-variant": "high" },
@@ -182,10 +144,7 @@ describe("SessionRunnerModel", () => {
it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
Effect.gen(function* () {
const catalog = model(
{
package: "@opencode-ai/llm/providers/openai-compatible",
settings: { baseURL: "https://compatible.example/v1" },
},
{ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" },
[
{
id: ModelV2.VariantID.make("high"),
@@ -217,7 +176,7 @@ describe("SessionRunnerModel", () => {
it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
Effect.gen(function* () {
const catalog = model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } })
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" })
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant_unavailable"),
projectID: ProjectV2.ID.global,
@@ -247,16 +206,13 @@ describe("SessionRunnerModel", () => {
it.effect("overlays selected Anthropic Session variant bodies", () =>
Effect.gen(function* () {
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 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 session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_anthropic_variant"),
projectID: ProjectV2.ID.global,
@@ -277,10 +233,10 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("maps catalog Anthropic native provider packages into native routes", () =>
it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ package: "@opencode-ai/llm/providers/anthropic", settings: { baseURL: "https://anthropic.example/v1" } }),
model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }),
)
expect(resolved.route).toMatchObject({
@@ -294,9 +250,8 @@ describe("SessionRunnerModel", () => {
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }),
headers: {},
body: {},
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: {} },
}),
Credential.Key.make({ type: "key", key: "secret" }),
)
@@ -318,10 +273,8 @@ describe("SessionRunnerModel", () => {
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({ package: "@opencode-ai/llm/providers/openai", settings: { baseURL: "https://openai.example/v1" } }),
settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
headers: {},
body: {},
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: { apiKey: "configured-secret" } },
}),
credential,
)
@@ -334,31 +287,39 @@ describe("SessionRunnerModel", () => {
})
expect(headers.authorization).toBe("Bearer stored-secret")
expect(resolved.route.defaults.http?.body).toEqual({})
expect(resolved.route.defaults.http?.body).toEqual({ tenant: "work" })
}),
)
it.effect("delegates aisdk-prefixed packages to the compatibility resolver", () =>
it.effect("rejects catalog APIs without a native route", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ package: "aisdk:@ai-sdk/google", settings: { baseURL: "https://google.example/v1" } }),
undefined,
aisdkDependencies,
)
const failure = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
).pipe(Effect.flip)
expect(resolved.route.id).toBe("openai-responses")
expect(resolved.route.endpoint).toMatchObject({ baseURL: "https://google.example/v1" })
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")
}),
)
it.effect("reports whether a catalog model has a supported package", () =>
it.effect("reports whether a catalog model has a supported native route", () =>
Effect.sync(() => {
expect(
SessionRunnerModel.supported(
model({ package: "aisdk:@ai-sdk/google", settings: { baseURL: "https://google.example/v1" } }),
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
),
).toBe(true)
expect(SessionRunnerModel.supported(model({ package: "native-provider-package" }))).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)
}),
)
})
@@ -17,7 +17,14 @@ const capture = () => {
const events = EventV2.Service.of({
publish: (definition, data) =>
Effect.sync(() => {
const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload<typeof definition>
const event = {
id: EventV2.ID.create(),
type: definition.type,
...(definition.durable
? { durable: { aggregateID: sessionID, seq: published.length, version: definition.durable.version } }
: {}),
data,
} as EventV2.Payload<typeof definition>
published.push({
type: definition.durable
? EventV2.versionedType(definition.type, definition.durable.version)
+31 -1
View File
@@ -9,6 +9,8 @@ import { Model } from "@opencode-ai/schema/model"
import { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/schema/prompt"
import { Provider } from "@opencode-ai/schema/provider"
import { Project } from "@opencode-ai/schema/project"
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
import { PermissionV1 } from "@opencode-ai/schema/permission-v1"
import { Session } from "@opencode-ai/schema/session"
import { SessionInput } from "@opencode-ai/schema/session-input"
import { SessionMessage } from "@opencode-ai/schema/session-message"
@@ -20,10 +22,14 @@ import { FileSystem } from "@opencode-ai/schema/filesystem"
import { Integration } from "@opencode-ai/schema/integration"
import { LLM } from "@opencode-ai/schema/llm"
import { Permission } from "@opencode-ai/schema/permission"
import { Plugin } from "@opencode-ai/schema/plugin"
import { Pty } from "@opencode-ai/schema/pty"
import { Reference } from "@opencode-ai/schema/reference"
import { SessionTodo } from "@opencode-ai/schema/session-todo"
import { Skill } from "@opencode-ai/schema/skill"
import { AbsolutePath, DateTimeUtcFromMillis } from "@opencode-ai/schema/schema"
import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { PluginV2 } from "@opencode-ai/core/plugin"
test("Core reuses the canonical shared schemas", async () => {
const [
@@ -35,13 +41,18 @@ test("Core reuses the canonical shared schemas", async () => {
coreLocation,
coreLLM,
corePermission,
corePermissionV1,
coreProjectCopy,
corePty,
coreProject,
coreReference,
coreSessionInput,
coreSessionMessage,
coreSessionTodo,
corePrompt,
coreSkill,
coreV2Schema,
coreSchema,
coreWorkspace,
] = await Promise.all([
import("@opencode-ai/core/command"),
@@ -52,13 +63,18 @@ test("Core reuses the canonical shared schemas", async () => {
import("@opencode-ai/core/location"),
import("@opencode-ai/llm"),
import("@opencode-ai/core/permission"),
import("@opencode-ai/core/v1/permission"),
import("@opencode-ai/core/project/copy"),
import("@opencode-ai/core/pty"),
import("@opencode-ai/core/project/schema"),
import("@opencode-ai/core/reference"),
import("@opencode-ai/core/session/input"),
import("@opencode-ai/core/session/message"),
import("@opencode-ai/core/session/todo"),
import("@opencode-ai/core/session/prompt"),
import("@opencode-ai/core/skill"),
import("@opencode-ai/core/v2-schema"),
import("@opencode-ai/core/schema"),
import("@opencode-ai/core/workspace"),
])
@@ -100,13 +116,23 @@ 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],
[corePermission.Rule, Permission.Rule],
[corePermission.Ruleset, Permission.Ruleset],
[corePermissionV1.Event, PermissionV1.Event],
[coreProjectCopy.Event, ProjectDirectories.Event],
[PluginV2.ID, Plugin.ID],
[PluginV2.Event, Plugin.Event],
[corePty.Info, Pty.Info],
[corePty.Event, Pty.Event],
[coreProject.ID, Project.ID],
[coreReference.LocalSource, Reference.LocalSource],
[coreReference.GitSource, Reference.GitSource],
@@ -136,6 +162,8 @@ test("Core reuses the canonical shared schemas", async () => {
[coreSessionMessage.Assistant, SessionMessage.Assistant],
[coreSessionMessage.Compaction, SessionMessage.Compaction],
[coreSessionMessage.Message, SessionMessage.Message],
[coreSessionTodo.Info, SessionTodo.Info],
[coreSessionTodo.Event, SessionTodo.Event],
[corePrompt.Source, Source],
[corePrompt.FileAttachment, FileAttachment],
[corePrompt.AgentAttachment, AgentAttachment],
@@ -146,6 +174,8 @@ test("Core reuses the canonical shared schemas", async () => {
[coreSkill.Source, Skill.Source],
[coreSkill.Info, Skill.Info],
[coreV2Schema.DateTimeUtcFromMillis, DateTimeUtcFromMillis],
[coreSchema.optional, optional],
[coreSchema.statics, statics],
[coreWorkspace.ID, Workspace.ID],
]
for (const [core, shared] of schemas) expect(core).toBe(shared)
-3
View File
@@ -15,7 +15,6 @@
"./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",
@@ -23,8 +22,6 @@
"./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,7 +1,6 @@
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,
@@ -32,7 +31,3 @@ export type {
ModelFactory as ProviderModelFactory,
ModelOptions as ProviderModelOptions,
} from "./provider"
export type {
Definition as ProviderPackageDefinition,
Settings as ProviderPackageSettings,
} from "./provider-package"
-16
View File
@@ -1,16 +0,0 @@
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"
+1 -26
View File
@@ -1,6 +1,5 @@
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"
@@ -16,15 +15,6 @@ 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`
@@ -50,19 +40,4 @@ export const configure = (input: Config = {}) => {
}
export const provider = configure()
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)
}
export const model = provider.model
+1 -14
View File
@@ -1,7 +1,6 @@
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"
@@ -11,11 +10,6 @@ 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")
@@ -38,11 +32,4 @@ export const configure = (input: Config = {}) => {
}
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,
}).model(modelID)
export const model = provider.model
@@ -2,7 +2,6 @@ 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")
@@ -13,12 +12,6 @@ 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
@@ -63,16 +56,6 @@ 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)
+1 -28
View File
@@ -1,6 +1,5 @@
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"
@@ -22,14 +21,6 @@ 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) => {
@@ -66,25 +57,7 @@ export const configure = (input: Config = {}) => {
export const provider = configure()
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 model = provider.model
export const responses = provider.responses
export const responsesWebSocket = provider.responsesWebSocket
export const chat = provider.chat
@@ -1,2 +0,0 @@
export { chatModel as model } from "../openai"
export type { Settings } from "../openai"
@@ -1,2 +0,0 @@
export { model } from "../openai"
export type { Settings } from "../openai"
+1
View File
@@ -30,6 +30,7 @@ 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()
@@ -1,41 +0,0 @@
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",
)
})
})
+1 -2
View File
@@ -6,9 +6,8 @@ import { Deferred, Effect, Layer, Context } from "effect"
import os from "os"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { EventV2Bridge } from "@/event-v2-bridge"
import { PermissionV1Event } from "@opencode-ai/schema/permission-v1"
export const Event = PermissionV1Event
export const Event = PermissionV1.Event
export interface Interface {
readonly ask: (input: PermissionV1.AskInput) => Effect.Effect<void, PermissionV1.Error>

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