Compare commits

..

5 Commits

Author SHA1 Message Date
Dax Raad c0814b0a8f refactor(core): migrate provider consumers 2026-06-24 13:00:46 -04:00
Dax Raad 7e6a7b91d5 fix(core): validate provider body options 2026-06-24 12:52:44 -04:00
Dax Raad 98d48b60be refactor(core): flatten provider config 2026-06-24 12:51:59 -04:00
Dax Raad f062021831 feat(core): generate model variants 2026-06-24 12:18:43 -04:00
Dax Raad 9db8c2e74b refactor(core): simplify model requests 2026-06-24 12:18:43 -04:00
216 changed files with 4611 additions and 4890 deletions
-1
View File
@@ -325,7 +325,6 @@ jobs:
run: bun run build
working-directory: packages/desktop
env:
NODE_OPTIONS: --max-old-space-size=4096
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
+27 -28
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -87,7 +87,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.17.10",
"version": "1.17.9",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -112,7 +112,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -148,7 +148,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -175,7 +175,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -197,7 +197,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -221,7 +221,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -241,7 +241,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.17.10",
"version": "1.17.9",
"bin": {
"opencode": "./bin/opencode",
},
@@ -334,7 +334,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"effect": "catalog:",
@@ -388,7 +388,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -402,7 +402,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"effect": "catalog:",
},
@@ -414,7 +414,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -446,7 +446,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -462,7 +462,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -481,7 +481,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -500,7 +500,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.17.10",
"version": "1.17.9",
"bin": {
"opencode": "./bin/opencode",
},
@@ -539,7 +539,6 @@
"@openauthjs/openauth": "catalog:",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/server": "workspace:*",
@@ -629,7 +628,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -679,7 +678,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -694,7 +693,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"drizzle-orm": "catalog:",
@@ -708,7 +707,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -752,7 +751,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -765,7 +764,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@ibm/plex": "6.4.1",
"@opencode-ai/stats-core": "workspace:*",
@@ -798,7 +797,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -817,7 +816,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -858,7 +857,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -885,7 +884,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -932,7 +931,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -2
View File
@@ -42,12 +42,11 @@ const inferenceEventTable = new aws.s3tables.Table(
{ name: "request", type: "string", required: false },
{ name: "client", type: "string", required: false },
{ name: "user_agent", type: "string", required: false },
{ name: "model", type: "string", required: false },
{ name: "model_tier", type: "string", required: false },
{ name: "model_variant", type: "string", required: false },
{ name: "source", type: "string", required: false },
{ name: "provider", type: "string", required: false },
{ name: "provider_model", type: "string", required: false },
{ name: "model", type: "string", required: false },
{ name: "llm_error_code", type: "int", required: false },
{ name: "llm_error_message", type: "string", required: false },
{ name: "error_response", type: "string", required: false },
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-gV5PVd9+MB5tM3pPH+S6YhGPRPXqHv5Ayw+8gngNlqY=",
"aarch64-linux": "sha256-1pxoseGyRwAawiPblo8FeaaAAdl0mkKRs6BS8qxxoh0=",
"aarch64-darwin": "sha256-81D/v2Ka7mtFHp4iM9eB9kjFcumSTvR07ABb1ahRLS0=",
"x86_64-darwin": "sha256-nolxEbc/1ItbmuHPUjaSu3DHwBV/0/Si2xuM6wpiq0k="
"x86_64-linux": "sha256-cXEEfBfOY7umTADufUAgFdwwq8tfFWDOS1+6rsHeC3o=",
"aarch64-linux": "sha256-xX9IutXeIEdj0IuW5ohXQfcK7ovMZ7bY0mIAwJXc5pU=",
"aarch64-darwin": "sha256-9iXck/18RsR+ncVUL0e2miccZz1mEde9ddVvsg6L29A=",
"x86_64-darwin": "sha256-lFO/GpThzUw1uymb9EllJdg5ZTW/iaJf/6LaXzL/1y0="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.17.10",
"version": "1.17.9",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.17.10",
"version": "1.17.9",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.17.10",
"version": "1.17.9",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.17.10",
"version": "1.17.9",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.17.10",
"version": "1.17.9",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -118,12 +118,11 @@ function toLakeEvent(time: string, data: Record<string, unknown>) {
request: string(data, "request"),
client: string(data, "client"),
user_agent: string(data, "user_agent"),
model: string(data, "model"),
model_tier: string(data, "model.tier"),
model_variant: string(data, "model.variant"),
source: string(data, "source"),
provider: string(data, "provider"),
provider_model: string(data, "provider.model"),
model: string(data, "model"),
llm_error_code: integer(data, "llm.error.code"),
llm_error_message: string(data, "llm.error.message"),
error_response: string(data, "error.response"),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.17.10",
"version": "1.17.9",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
"version": "1.17.10",
"version": "1.17.9",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.10",
"version": "1.17.9",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+12 -10
View File
@@ -73,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.api.type === "aisdk" ? (model.api.settings ?? {}) : {}),
...model.request.body,
...(model.settings ?? {}),
headers: model.headers,
body: model.body,
}
if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
const customFetch = options.fetch
const chunkTimeout = options.chunkTimeout
@@ -195,24 +195,26 @@ export const locationLayer = Layer.effect(
runSDK: (event) => run(sdkHooks, event),
runLanguage: (event) => run(languageHooks, event),
language: Effect.fn("AISDK.language")(function* (model) {
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
const key = `${model.providerID}/${model.id}/${model.modelID ?? model.id}/${JSON.stringify(model.settings)}`
const existing = languages.get(key)
if (existing) return existing
if (model.api.type !== "aisdk")
if (!model.aisdk)
return yield* new InitError({
providerID: model.providerID,
cause: new Error(`Unsupported api ${model.api.type}`),
cause: new Error(`Unsupported package ${model.package}`),
})
const options = prepareOptions(model, model.api.package)
const options = prepareOptions(model, model.package ?? "")
const sdkKey = JSON.stringify({
providerID: model.providerID,
api: model.api,
package: model.package,
settings: model.settings,
options,
})
const sdk =
sdks.get(sdkKey) ??
(yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
(yield* service.runSDK({ model, package: model.package ?? "", options }).pipe(initError(model.providerID)))
.sdk
if (!sdk)
return yield* new InitError({
providerID: model.providerID,
@@ -220,7 +222,7 @@ export const locationLayer = Layer.effect(
})
sdks.set(sdkKey, sdk)
const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.modelID ?? model.id)).pipe(
initError(model.providerID),
)
languages.set(key, language)
+46 -26
View File
@@ -1,7 +1,6 @@
export * as Catalog from "./catalog"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { Catalog } from "@opencode-ai/schema/catalog"
import { ModelV2 } from "./model"
import { ProviderV2 } from "./provider"
import { EventV2 } from "./event"
@@ -18,7 +17,9 @@ export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
export const PolicyActions = Schema.Literals(["provider.use"])
export const Event = Catalog.Event
export const Event = {
Updated: EventV2.define({ type: "catalog.updated", schema: {} }),
}
type Data = {
providers: Map<ProviderV2.ID, ProviderRecord>
@@ -69,38 +70,22 @@ export const layer = Layer.effect(
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
if (provider.disabled) return false
if (typeof provider.request.body.apiKey === "string") return true
if (typeof provider.settings?.apiKey === "string") return true
if (integration?.connections.length) return true
return provider.integrationID === undefined && !integration
}
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
const api =
model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0
? { ...provider.api, id: model.api.id }
: model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } }
: model.api.type === "aisdk" && provider.api.type === "aisdk"
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
: model.api
const request = {
headers: { ...provider.request.headers, ...model.request.headers },
body: { ...provider.request.body, ...model.request.body },
variant: model.request.variant,
}
return ModelV2.Info.make({
...model,
api,
request,
package: model.package ?? provider.package,
aisdk: model.package === undefined ? (model.aisdk ?? provider.aisdk) : model.aisdk,
settings: merge(provider.settings, model.settings),
headers: headers(provider.headers, model.headers),
body: merge(provider.body, model.body),
})
}
const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => {
if (typeof item.request.body.baseURL !== "string") return
item.api.url = item.request.body.baseURL
delete item.request.body.baseURL
}
const state = State.create<Data, Draft>({
initial: () => ({ providers: new Map() }),
draft: (draft) => {
@@ -118,7 +103,6 @@ export const layer = Layer.effect(
draft.providers.set(providerID, current)
}
fn(current.provider)
normalizeApi(current.provider)
},
remove: (providerID) => {
draft.providers.delete(providerID)
@@ -141,7 +125,6 @@ export const layer = Layer.effect(
fn(model)
model.id = modelID
model.providerID = providerID
normalizeApi(model)
},
remove: (providerID, modelID) => {
draft.providers.get(providerID)?.models.delete(modelID)
@@ -287,6 +270,43 @@ export const layer = Layer.effect(
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
function merge(
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 (plain(left) && plain(right)) return [key, merge(left, right)]
return [key, right]
}),
)
}
function plain(input: unknown): input is Readonly<Record<string, unknown>> {
if (typeof input !== "object" || input === null || Array.isArray(input)) return false
const prototype = Object.getPrototypeOf(input)
return prototype === Object.prototype || prototype === null
}
function headers(
base: Readonly<Record<string, string>> | undefined,
overlay: Readonly<Record<string, string>> | undefined,
) {
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 locationLayer = layer.pipe(
Layer.provideMerge(Integration.locationLayer),
Layer.provideMerge(Policy.locationLayer),
+16 -15
View File
@@ -52,17 +52,22 @@ export const Plugin = define({
const providerID = id
catalog.provider.update(providerID, (provider) => {
if (item.name !== undefined) provider.name = item.name
if (item.api !== undefined) provider.api = { ...item.api }
if (item.request !== undefined) {
Object.assign(provider.request.headers, item.request.headers)
Object.assign(provider.request.body, item.request.body)
}
if (item.package !== undefined) provider.package = item.package
if (item.aisdk !== undefined) provider.aisdk = item.aisdk
if (item.settings !== undefined) provider.settings = { ...provider.settings, ...item.settings }
if (item.headers !== undefined) provider.headers = { ...provider.headers, ...item.headers }
if (item.body !== undefined) provider.body = { ...provider.body, ...item.body }
})
for (const [id, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, id, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
if (config.modelID !== undefined) model.modelID = config.modelID
if (config.package !== undefined) model.package = config.package
if (config.aisdk !== undefined) model.aisdk = config.aisdk
if (config.settings !== undefined) model.settings = { ...model.settings, ...config.settings }
if (config.headers !== undefined) model.headers = { ...model.headers, ...config.headers }
if (config.body !== undefined) model.body = { ...model.body, ...config.body }
if (config.capabilities !== undefined) {
model.capabilities = {
tools: config.capabilities.tools,
@@ -70,24 +75,20 @@ export const Plugin = define({
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
Object.assign(model.request.headers, config.request.headers)
Object.assign(model.request.body, config.request.body)
if (config.request.variant !== undefined) model.request.variant = config.request.variant
}
if (config.variants !== undefined) {
model.variants ??= []
for (const variant of config.variants) {
let existing = model.variants.find((item) => item.id === variant.id)
if (!existing) {
existing = {
id: variant.id,
headers: {},
body: {},
}
model.variants.push(existing)
}
Object.assign(existing.headers, variant.headers)
Object.assign(existing.body, variant.body)
if (variant.settings !== undefined)
existing.settings = { ...existing.settings, ...variant.settings }
if (variant.headers !== undefined) existing.headers = { ...existing.headers, ...variant.headers }
if (variant.body !== undefined) existing.body = { ...existing.body, ...variant.body }
}
}
if (config.cost !== undefined) {
+15 -24
View File
@@ -1,12 +1,17 @@
export * as ConfigProvider from "./provider"
import { Schema } from "effect"
import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model"
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
export const Overlays = {
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
headers: Overlays.headers,
body: Overlays.body,
}) {}
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
@@ -30,32 +35,17 @@ class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
output: Schema.Int.pipe(Schema.optional),
}) {}
const ModelApi = Schema.Union([
Schema.Struct({
id: ModelV2.ID.pipe(Schema.optional),
...ProviderV2.AISDK.fields,
}),
Schema.Struct({
id: ModelV2.ID.pipe(Schema.optional),
...ProviderV2.Native.fields,
}),
Schema.Struct({
id: ModelV2.ID,
}),
])
class Model extends Schema.Class<Model>("ConfigV2.Model")({
modelID: ModelV2.ID.pipe(Schema.optional),
family: ModelV2.Family.pipe(Schema.optional),
name: Schema.String.pipe(Schema.optional),
api: ModelApi.pipe(Schema.optional),
package: Schema.String.pipe(Schema.optional),
aisdk: Schema.Literal(true).pipe(Schema.optional),
...Overlays,
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
request: Schema.Struct({
...Request.fields,
variant: Schema.String.pipe(Schema.optional),
}).pipe(Schema.optional),
variants: Schema.Struct({
id: ModelV2.VariantID,
...Request.fields,
...Overlays,
}).pipe(Schema.Array, Schema.optional),
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
disabled: Schema.Boolean.pipe(Schema.optional),
@@ -65,7 +55,8 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
name: Schema.String.pipe(Schema.optional),
env: Schema.String.pipe(Schema.Array, Schema.optional),
api: ProviderV2.Api.pipe(Schema.optional),
request: Request.pipe(Schema.optional),
package: Schema.String.pipe(Schema.optional),
aisdk: Schema.Literal(true).pipe(Schema.optional),
...Overlays,
models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
}) {}
+94 -23
View File
@@ -1,19 +1,46 @@
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 { and, asc, eq, gt } from "drizzle-orm"
import { Database } from "./database/database"
import { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location"
import { withStatics } from "./schema"
import { Identifier } from "./util/identifier"
import { LayerNode } from "./effect/layer-node"
import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
export const ID = Event.ID
export type ID = import("@opencode-ai/schema/event").ID
export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
Schema.brand("Event.ID"),
withStatics((schema) => ({
create: () => schema.make("evt_" + Identifier.ascending()),
})),
)
export type ID = typeof ID.Type
export type Definition<Type extends string = string, DataSchema extends Schema.Top = Schema.Top> = {
readonly type: Type
readonly durable?: {
readonly version: number
readonly aggregate: string
}
readonly data: DataSchema
}
export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
export type Payload<D extends Definition = Definition> = {
readonly id: ID
readonly type: D["type"]
readonly data: Data<D>
readonly durable?: {
readonly aggregateID: string
readonly seq: number
readonly version: number
}
readonly location?: Location.Ref
readonly metadata?: Record<string, unknown>
}
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
export type Unsubscribe = Effect.Effect<void>
@@ -47,8 +74,52 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
},
) {}
export const define = Event.define
export const versionedType = Event.versionedType
export function versionedType(type: string, version: number) {
return `${type}.${version}`
}
export const registry = new Map<string, Definition>()
const durableRegistry = new Map<string, Definition>()
export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
readonly type: Type
readonly durable?: {
readonly version: number
readonly aggregate: string
}
readonly schema: Fields
}): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
const Data = Schema.Struct(input.schema)
const Payload = Schema.Struct({
id: ID,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
type: Schema.Literal(input.type),
durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number })),
location: Schema.optional(Location.Ref),
data: Data,
}).annotate({ identifier: input.type })
const definition = Object.assign(Payload, {
type: input.type,
...(input.durable === undefined ? {} : { durable: input.durable }),
data: Data,
})
const existing = registry.get(input.type)
if (
input.durable === undefined ||
existing?.durable === undefined ||
input.durable.version >= existing.durable.version
) {
registry.set(input.type, definition)
}
if (input.durable) durableRegistry.set(versionedType(input.type, input.durable.version), definition)
return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
Definition<Type, Schema.Struct<Fields>>
}
export function definitions() {
return registry.values().toArray()
}
export interface PublishOptions {
readonly id?: ID
@@ -98,7 +169,6 @@ export const layerWith = (options?: LayerOptions) =>
typed: new Map<string, PubSub.PubSub<Payload>>(),
}
const projectors = new Map<string, Subscriber[]>()
// TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads.
const listeners = new Array<Subscriber>()
const { db } = yield* Database.Service
@@ -124,7 +194,6 @@ export const layerWith = (options?: LayerOptions) =>
)
function commitDurableEvent(
definition: Definition,
event: Payload,
input?: {
readonly seq: number
@@ -135,6 +204,7 @@ export const layerWith = (options?: LayerOptions) =>
commit?: (seq: number) => Effect.Effect<void>,
) {
return Effect.gen(function* () {
const definition = registry.get(event.type)
const durable = definition?.durable
if (durable) {
const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
@@ -168,10 +238,9 @@ export const layerWith = (options?: LayerOptions) =>
.get()
.pipe(Effect.orDie)
const latest = row?.seq ?? -1
const encoded = Schema.encodeUnknownSync(definition.data)(event.data) as Record<
string,
unknown
>
const encoded = Schema.encodeUnknownSync(
definition.data as Schema.Codec<unknown, unknown, never, never>,
)(event.data) as Record<string, unknown>
if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
yield* Effect.die(
new InvalidDurableEventError({
@@ -287,8 +356,9 @@ export const layerWith = (options?: LayerOptions) =>
})
}
function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
function publishEvent<D extends Definition>(event: Payload<D>, commit?: PublishOptions["commit"]) {
return Effect.gen(function* () {
const definition = registry.get(event.type)
if (!definition?.durable && commit)
return yield* Effect.die(
new InvalidDurableEventError({
@@ -297,7 +367,7 @@ export const layerWith = (options?: LayerOptions) =>
}),
)
if (definition?.durable) {
const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit)
const committed = yield* commitDurableEvent(event as Payload, undefined, commit)
if (committed) {
event = {
...event,
@@ -346,7 +416,6 @@ export const layerWith = (options?: LayerOptions) =>
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
: undefined)
return yield* publishEvent(
definition,
{
id: options?.id ?? ID.create(),
...(options?.metadata ? { metadata: options.metadata } : {}),
@@ -364,7 +433,7 @@ export const layerWith = (options?: LayerOptions) =>
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
) {
return Effect.gen(function* () {
const definition = Durable.get(event.type)
const definition = durableRegistry.get(event.type)
if (!definition?.durable) {
yield* Effect.die(
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
@@ -373,9 +442,11 @@ export const layerWith = (options?: LayerOptions) =>
const payload = {
id: event.id,
type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data),
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(
event.data,
),
} as Payload
const committed = yield* commitDurableEvent(definition, payload, {
const committed = yield* commitDurableEvent(payload, {
seq: event.seq,
aggregateID: event.aggregateID,
ownerID: options?.ownerID,
@@ -459,8 +530,8 @@ export const layerWith = (options?: LayerOptions) =>
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
const decodeSerializedEvent = (event: SerializedEvent) => {
const definition = Durable.get(event.type)
const decodeSerializedEvent = (event: SerializedEvent): Payload => {
const definition = durableRegistry.get(event.type)
if (!definition?.durable) {
throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
}
@@ -468,7 +539,7 @@ export const layerWith = (options?: LayerOptions) =>
id: event.id,
type: definition.type,
durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
data: Schema.decodeUnknownSync(definition.data)(event.data),
data: Schema.decodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)(event.data),
}
}
+10 -2
View File
@@ -2,11 +2,12 @@ export * as FileSystem from "./filesystem"
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { EventV2 } from "./event"
import { FSUtil } from "./fs-util"
import { Location } from "./location"
import { PositiveInt, RelativePath } from "./schema"
import { FileSystemSearch } from "./filesystem/search"
import { Entry, FileSystem, Match } from "@opencode-ai/schema/filesystem"
import { Entry, Match } from "@opencode-ai/schema/filesystem"
export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
export const ReadInput = Schema.Struct({
@@ -47,7 +48,14 @@ export class GrepInput extends Schema.Class<GrepInput>("FileSystem.GrepInput")({
limit: PositiveInt.pipe(Schema.optional),
}) {}
export const Event = FileSystem.Event
export const Event = {
Edited: EventV2.define({
type: "file.edited",
schema: {
file: Schema.String,
},
}),
}
export interface Interface {
readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
+10 -3
View File
@@ -3,8 +3,7 @@ export * as Watcher from "./watcher"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher"
import { Cause, Context, Effect, Layer } from "effect"
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
import { Cause, Context, Effect, Layer, Schema } from "effect"
import path from "path"
import { Config } from "../config"
import { EventV2 } from "../event"
@@ -20,7 +19,15 @@ declare const OPENCODE_LIBC: string | undefined
const SUBSCRIBE_TIMEOUT_MS = 10_000
export const Event = FileSystemWatcher.Event
export const Event = {
Updated: EventV2.define({
type: "file.watcher.updated",
schema: {
file: Schema.String,
event: Schema.Literals(["add", "change", "unlink"]),
},
}),
}
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try {
+10 -1
View File
@@ -136,7 +136,16 @@ export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationErr
export type Error = CodeRequiredError | AuthorizationError
export const Event = Integration.Event
export const Event = {
Updated: EventV2.define({
type: "integration.updated",
schema: {},
}),
ConnectionUpdated: EventV2.define({
type: "integration.connection.updated",
schema: { integrationID: ID },
}),
}
export const Ref = Integration.Ref
export type Ref = Integration.Ref
+1 -6
View File
@@ -20,15 +20,10 @@ export const Cost = Model.Cost
export const Ref = Model.Ref
export type Ref = typeof Ref.Type
export const Api = Model.Api
export type Api = Model.Api
export const Info = Model.Info
export type Info = Model.Info
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
api: ProviderV2.MutableApi<Api>
}
export type MutableInfo = Types.DeepMutable<Info>
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
const [providerID, ...modelID] = input.split("/")
+6 -2
View File
@@ -1,7 +1,6 @@
import path from "path"
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode-ai/schema/models-dev"
import { Global } from "./global"
import { Flag } from "./flag/flag"
import { Flock } from "./util/flock"
@@ -109,7 +108,12 @@ export const Provider = Schema.Struct({
export type Provider = Schema.Schema.Type<typeof Provider>
export const Event = ModelsDev.Event
export const Event = {
Refreshed: EventV2.define({
type: "models-dev.refreshed",
schema: {},
}),
}
declare const OPENCODE_MODELS_DEV: Record<string, Provider> | undefined
+35 -11
View File
@@ -7,31 +7,45 @@ import { Location } from "./location"
import { AgentV2 } from "./agent"
import { SessionV2 } from "./session"
import { SessionStore } from "./session/store"
import { withStatics } from "./schema"
import { Identifier } from "./util/identifier"
import { Wildcard } from "./util/wildcard"
import { PermissionSaved } from "./permission/saved"
export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission"
const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
export const ID = Permission.ID
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionV2.ID"),
withStatics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + Identifier.ascending()) })),
)
export type ID = typeof ID.Type
export const Source = Permission.Source
export const Source = Schema.Union([
Schema.Struct({
type: Schema.Literal("tool"),
messageID: Schema.String,
callID: Schema.String,
}),
]).annotate({ identifier: "PermissionV2.Source" })
export type Source = typeof Source.Type
const RequestFields = {
sessionID: Permission.Request.fields.sessionID,
action: Permission.Request.fields.action,
resources: Permission.Request.fields.resources,
save: Permission.Request.fields.save,
metadata: Permission.Request.fields.metadata,
source: Permission.Request.fields.source,
sessionID: SessionV2.ID,
action: Schema.String,
resources: Schema.Array(Schema.String),
save: Schema.Array(Schema.String).pipe(Schema.optional),
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
source: Source.pipe(Schema.optional),
}
export const Request = Permission.Request
export const Request = Schema.Struct({
id: ID,
...RequestFields,
}).annotate({ identifier: "PermissionV2.Request" })
export type Request = typeof Request.Type
export const Reply = Permission.Reply
export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" })
export type Reply = typeof Reply.Type
export const AssertInput = Schema.Struct({
@@ -54,7 +68,17 @@ export const AskResult = Schema.Struct({
}).annotate({ identifier: "PermissionV2.AskResult" })
export type AskResult = typeof AskResult.Type
export const Event = Permission.Event
export const Event = {
Asked: EventV2.define({ type: "permission.v2.asked", schema: Request.fields }),
Replied: EventV2.define({
type: "permission.v2.replied",
schema: {
sessionID: SessionV2.ID,
requestID: ID,
reply: Reply,
},
}),
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {}
+10 -4
View File
@@ -1,8 +1,7 @@
export * as PluginV2 from "./plugin"
import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
import { Context, Deferred, Effect, Exit, Layer, Schema, Scope } from "effect"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { PluginEvent, PluginID } from "@opencode-ai/schema/plugin"
import { AgentV2 } from "./agent"
import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
@@ -15,10 +14,17 @@ import { Reference } from "./reference"
import { SkillV2 } from "./skill"
import { State } from "./state"
export const ID = PluginID
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
export type ID = typeof ID.Type
export const Event = PluginEvent
export const Event = {
Added: EventV2.define({
type: "plugin.added",
schema: {
id: ID,
},
}),
}
export interface Interface {
readonly add: (id: ID, effect: Plugin["effect"]) => Effect.Effect<void>
+6 -24
View File
@@ -75,17 +75,9 @@ export const ModelsDevPlugin = define({
const providerID = ProviderV2.ID.make(item.id)
catalog.provider.update(providerID, (provider) => {
provider.name = item.name
provider.api = item.npm
? {
type: "aisdk",
package: item.npm,
url: item.api,
}
: {
type: "native",
url: item.api,
settings: {},
}
provider.package = item.npm ?? ""
provider.aisdk = item.npm ? true : undefined
provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
})
for (const model of Object.values(item.models)) {
@@ -93,19 +85,9 @@ export const ModelsDevPlugin = define({
catalog.model.update(providerID, modelID, (draft) => {
draft.name = model.name
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
draft.api = model.provider?.npm
? {
id: draft.api.id,
type: "aisdk",
package: model.provider?.npm,
url: model.provider.api,
}
: {
id: draft.api.id,
type: "native",
url: model.provider?.api,
settings: {},
}
draft.package = model.provider?.npm
draft.aisdk = model.provider?.npm ? true : undefined
draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
draft.capabilities = {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
@@ -65,15 +65,14 @@ export const AmazonBedrockPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/amazon-bedrock") continue
evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return
if (typeof provider.request.body.endpoint !== "string") return
if (typeof provider.settings?.endpoint !== "string") return
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
provider.api.url = provider.request.body.endpoint
delete provider.request.body.endpoint
provider.settings.baseURL = provider.settings.endpoint
delete provider.settings.endpoint
})
}
}),
@@ -114,12 +113,12 @@ export const AmazonBedrockPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
if (evt.model.aisdk && evt.model.package === "@ai-sdk/amazon-bedrock/mantle") {
evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
return
}
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.modelID ?? evt.model.id, region))
}),
)
}),
@@ -7,11 +7,13 @@ export const AnthropicPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/anthropic") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["anthropic-beta"] =
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
provider.headers = {
...provider.headers,
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
}
})
}
}),
+21 -10
View File
@@ -16,14 +16,14 @@ export const AzurePlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/azure") continue
const configured = item.provider.request.body.resourceName
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/azure") continue
const configured = item.provider.settings?.resourceName
const resourceName =
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
if (!resourceName) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.body.resourceName = resourceName
provider.settings = { ...provider.settings, resourceName }
})
}
}),
@@ -35,7 +35,7 @@ export const AzurePlugin = define({
if (
!evt.options.resourceName &&
!evt.options.baseURL &&
(evt.model.api.type !== "aisdk" || !evt.model.api.url)
(!evt.model.aisdk || typeof evt.model.settings?.baseURL !== "string")
) {
throw new Error(
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
@@ -49,7 +49,11 @@ export const AzurePlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
evt.language = selectLanguage(
evt.sdk,
evt.model.modelID ?? evt.model.id,
Boolean(evt.options.useCompletionUrls),
)
}),
)
}),
@@ -63,11 +67,14 @@ export const AzureCognitiveServicesPlugin = define({
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
if (!item.provider.id.includes("azure-cognitive-services")) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
provider.settings = {
...provider.settings,
baseURL: `https://${resourceName}.cognitiveservices.azure.com/openai`,
}
})
}
}),
@@ -75,7 +82,11 @@ export const AzureCognitiveServicesPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
evt.language = selectLanguage(
evt.sdk,
evt.model.modelID ?? evt.model.id,
Boolean(evt.options.useCompletionUrls),
)
}),
)
}),
@@ -7,10 +7,10 @@ export const CerebrasPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/cerebras") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
})
}
}),
@@ -14,10 +14,10 @@ export const CloudflareWorkersAIPlugin = define({
const item = evt.provider.get(providerID)
if (!item) return
evt.provider.update(item.provider.id, (provider) => {
if (provider.api.type !== "aisdk") return
if (provider.api.url) return
const accountId = resolveAccountId(provider.request.body)
if (accountId) provider.api.url = workersEndpoint(accountId)
if (!provider.aisdk) return
if (typeof provider.settings?.baseURL === "string") return
const accountId = resolveAccountId(provider.settings ?? {})
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
})
}),
)
@@ -27,7 +27,7 @@ export const CloudflareWorkersAIPlugin = define({
if (evt.package !== "@ai-sdk/openai-compatible") return
const accountId = resolveAccountId(evt.options)
if (!hasWorkersEndpoint(evt.model.api) && !accountId) return
if (!hasWorkersEndpoint(evt.model) && !accountId) return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
evt.sdk = mod.createOpenAICompatible(
sdkOptions({
@@ -40,7 +40,7 @@ export const CloudflareWorkersAIPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return
evt.language = evt.sdk.languageModel(evt.model.api.id)
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
}),
)
}),
@@ -54,8 +54,8 @@ function workersEndpoint(accountId: string) {
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
}
function hasWorkersEndpoint(api: ProviderV2.Api) {
return api.type === "aisdk" && Boolean(api.url)
function hasWorkersEndpoint(model: { readonly aisdk?: true; readonly settings?: Readonly<Record<string, unknown>> }) {
return model.aisdk && typeof model.settings?.baseURL === "string"
}
function sdkOptions(options: Record<string, any>) {
@@ -36,12 +36,11 @@ export const GithubCopilotPlugin = define({
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
evt.language = evt.sdk.languageModel(evt.model.api.id)
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
return
}
evt.language = shouldUseResponses(evt.model.api.id)
? evt.sdk.responses(evt.model.api.id)
: evt.sdk.chat(evt.model.api.id)
const id = evt.model.modelID ?? evt.model.id
evt.language = shouldUseResponses(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
}),
)
}),
+10 -12
View File
@@ -36,26 +36,24 @@ export const GitLabPlugin = define({
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
const featureFlags =
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
if (evt.model.api.id.startsWith("duo-workflow-")) {
const id = evt.model.modelID ?? evt.model.id
if (id.startsWith("duo-workflow-")) {
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
const workflowRef =
typeof evt.model.request.body.workflowRef === "string" ? evt.model.request.body.workflowRef : undefined
typeof evt.model.settings?.workflowRef === "string" ? evt.model.settings.workflowRef : undefined
const workflowDefinition =
typeof evt.model.request.body.workflowDefinition === "string"
? evt.model.request.body.workflowDefinition
typeof evt.model.settings?.workflowDefinition === "string"
? evt.model.settings.workflowDefinition
: undefined
const language = evt.sdk.workflowChat(
gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow",
{
featureFlags,
workflowDefinition,
},
)
const language = evt.sdk.workflowChat(gitlab.isWorkflowModel(id) ? id : "duo-workflow", {
featureFlags,
workflowDefinition,
})
if (workflowRef) language.selectedModelRef = workflowRef
evt.language = language
return
}
evt.language = evt.sdk.agenticChat(evt.model.api.id, {
evt.language = evt.sdk.agenticChat(id, {
aiGatewayHeaders: evt.options.aiGatewayHeaders,
featureFlags,
})
@@ -60,25 +60,28 @@ export const GoogleVertexPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (!item.provider.aisdk) continue
if (
item.provider.api.package !== "@ai-sdk/google-vertex" &&
item.provider.package !== "@ai-sdk/google-vertex" &&
!(
item.provider.id === ProviderV2.ID.googleVertex &&
item.provider.api.package.includes("@ai-sdk/openai-compatible")
item.provider.package.includes("@ai-sdk/openai-compatible")
)
)
continue
const project = resolveProject(item.provider.request.body)
const location = String(resolveLocation(item.provider.request.body))
const project = resolveProject(item.provider.settings ?? {})
const location = String(resolveLocation(item.provider.settings ?? {}))
evt.provider.update(item.provider.id, (provider) => {
if (project) provider.request.body.project = project
provider.request.body.location = location
if (provider.api.type === "aisdk" && provider.api.url) {
provider.api.url = replaceVertexVars(provider.api.url, project, location)
}
if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) {
provider.request.body.fetch = authFetch(provider.request.body.fetch)
provider.settings = {
...provider.settings,
...(project ? { project } : {}),
location,
...(typeof provider.settings?.baseURL === "string"
? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
: {}),
...(provider.package.includes("@ai-sdk/openai-compatible")
? { fetch: authFetch(provider.settings?.fetch) }
: {}),
}
})
}
@@ -106,7 +109,7 @@ export const GoogleVertexPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
}),
)
}),
@@ -118,21 +121,20 @@ export const GoogleVertexAnthropicPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/google-vertex/anthropic") continue
const project =
item.provider.request.body.project ??
item.provider.settings?.project ??
process.env.GOOGLE_CLOUD_PROJECT ??
process.env.GCP_PROJECT ??
process.env.GCLOUD_PROJECT
const location =
item.provider.request.body.location ??
item.provider.settings?.location ??
process.env.GOOGLE_CLOUD_LOCATION ??
process.env.VERTEX_LOCATION ??
"global"
evt.provider.update(item.provider.id, (provider) => {
if (project) provider.request.body.project = project
provider.request.body.location = location
provider.settings = { ...provider.settings, ...(project ? { project } : {}), location }
})
}
}),
@@ -166,7 +168,7 @@ export const GoogleVertexAnthropicPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
}),
)
}),
+4 -5
View File
@@ -7,12 +7,11 @@ export const KiloPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
})
}
}),
@@ -10,14 +10,17 @@ export const LLMGatewayPlugin = define({
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.disabled) continue
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-Source"] = "opencode"
provider.headers = {
...provider.headers,
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-Source": "opencode",
}
})
}
}),
+9 -6
View File
@@ -7,13 +7,16 @@ export const NvidiaPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode"
provider.headers = {
...provider.headers,
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": provider.headers?.["X-BILLING-INVOKE-ORIGIN"] ?? "OpenCode",
}
})
}
}),
+3 -3
View File
@@ -156,8 +156,8 @@ export const OpenAIPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/openai") continue
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
@@ -177,7 +177,7 @@ export const OpenAIPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return
evt.language = evt.sdk.responses(evt.model.api.id)
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
}),
)
}),
+17 -19
View File
@@ -111,11 +111,14 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
catalog.provider.update(providerID, (provider) => {
provider.integrationID = Integration.ID.make("opencode")
if (item.name !== undefined) provider.name = item.name
provider.api = item.npm
? { type: "aisdk", package: item.npm, url: item.api }
: { type: "native", url: item.api, settings: {} }
Object.assign(provider.request.headers, item.options?.headers)
Object.assign(provider.request.body, withoutCredentials(item.options))
provider.package = item.npm ?? ""
provider.aisdk = item.npm ? true : undefined
provider.settings = {
...provider.settings,
...withoutCredentials(item.options),
...(item.api ? { baseURL: item.api } : {}),
}
provider.headers = { ...provider.headers, ...item.options?.headers }
})
const modelIDs = new Set(Object.keys(item.models ?? {}))
@@ -127,29 +130,24 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
catalog.model.update(providerID, modelID, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.name !== undefined) model.name = config.name
if (config.id !== undefined) model.api.id = config.id
if (config.id !== undefined) model.modelID = config.id
if (config.provider !== undefined) {
model.api = config.provider.npm
? {
id: model.api.id,
type: "aisdk",
package: config.provider.npm,
url: config.provider.api,
}
: { id: model.api.id, type: "native", url: config.provider.api, settings: {} }
model.package = config.provider.npm
model.aisdk = config.provider.npm ? true : undefined
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
}
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
const packageName = config.provider?.npm ?? item.npm
const lowerer = ConfigProviderOptionsV1.get(packageName)
Object.assign(model.request.headers, config.headers)
Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options)))
model.headers = { ...model.headers, ...config.headers }
model.settings = { ...model.settings, ...lowerer.model(withoutCredentials(config.options)) }
if (config.variants !== undefined) {
model.variants = Object.entries(config.variants).map(([id, options]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(options.headers ?? {}) },
body: lowerer.request(withoutCredentials(options)),
settings: lowerer.model(withoutCredentials(options)),
}))
}
if (config.release_date !== undefined) {
@@ -168,9 +166,9 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
const item = catalog.provider.get(ProviderV2.ID.opencode)
if (!item) return
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey)
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
catalog.provider.update(item.provider.id, (provider) => {
if (!hasKey) provider.request.body.apiKey = "public"
if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
})
if (hasKey) return
for (const model of item.models.values()) {
@@ -8,11 +8,10 @@ export const OpenRouterPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@openrouter/ai-sdk-provider") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode"
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
})
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
if (!item.models.has(modelID)) continue
@@ -39,7 +39,7 @@ export const SapAICorePlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.api.id)
evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
}),
)
}),
+3 -4
View File
@@ -7,11 +7,10 @@ export const VercelPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/vercel") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/vercel") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["http-referer"] = "https://opencode.ai/"
provider.request.headers["x-title"] = "opencode"
provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
})
}
}),
+1 -1
View File
@@ -15,7 +15,7 @@ export const XAIPlugin = define({
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
evt.language = evt.sdk.responses(evt.model.api.id)
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
}),
)
}),
+8 -5
View File
@@ -7,12 +7,15 @@ export const ZenmuxPlugin = define({
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue
if (!item.provider.aisdk) continue
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/"
provider.request.headers["X-Title"] ??= "opencode"
provider.headers = {
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
...provider.headers,
}
})
}
}),
+13 -11
View File
@@ -1,7 +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 { define } from "./internal"
export const Plugin = define({
@@ -11,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)
const generated = generate(draft as ModelV2.Info, record.provider)
if (generated.length === 0) return
const explicit = new Map(draft.variants.map((variant) => [variant.id, variant]))
const generatedIDs = new Set(generated.map((variant) => variant.id))
const explicit = new Map((draft.variants ?? []).map((variant) => [variant.id, variant]))
const generatedIDs = new Set<string>(generated.map((variant) => variant.id))
draft.variants = [
...generated.map((variant) => explicit.get(variant.id) ?? variant),
...draft.variants.filter((variant) => !generatedIDs.has(variant.id)),
...(draft.variants ?? []).filter((variant) => !generatedIDs.has(variant.id)),
]
})
}
@@ -27,13 +27,15 @@ export const Plugin = define({
}),
})
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.api.id}`.toLowerCase()
export function generate(
model: ModelV2.Info,
provider?: { readonly package: string; readonly aisdk?: true },
): NonNullable<ModelV2.Info["variants"]> {
if (!(model.aisdk ?? provider?.aisdk) || (model.package ?? provider?.package) !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase()
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({
id,
headers: {},
body: { reasoning_effort: id },
id: ModelV2.VariantID.make(id),
settings: { reasoningEffort: id },
}))
}
+6 -2
View File
@@ -13,7 +13,6 @@ 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"
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
export type StrategyID = typeof StrategyID.Type
@@ -107,7 +106,12 @@ export interface Strategy {
readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
}
export const Event = ProjectDirectoriesEvent
export const Event = {
Updated: EventV2.define({
type: "project.directories.updated",
schema: { projectID: Project.ID },
}),
}
export interface Interface {
readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
+1 -11
View File
@@ -6,20 +6,10 @@ import { Provider } from "@opencode-ai/schema/provider"
export const ID = Provider.ID
export type ID = typeof ID.Type
export const AISDK = Provider.AISDK
export const Native = Provider.Native
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
export const Info = Provider.Info
export type Info = Provider.Info
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
export type MutableInfo = Types.DeepMutable<Info>
+19 -4
View File
@@ -2,11 +2,10 @@ export * as Pty from "./pty"
import type { Disp, Proc } from "#pty"
import { Context, Effect, Layer, Schema, Types } from "effect"
import { PtyEvent, PtyInfo } from "@opencode-ai/schema/pty"
import { Config } from "./config"
import { EventV2 } from "./event"
import { Location } from "./location"
import { PositiveInt } from "./schema"
import { NonNegativeInt, PositiveInt } from "./schema"
import { PtyID } from "./pty/schema"
import { Shell } from "./shell"
import { lazy } from "./util/lazy"
@@ -36,7 +35,18 @@ type Active = {
listeners: Disp[]
}
export const Info = PtyInfo
export const Info = Schema.Struct({
id: PtyID,
title: Schema.String,
command: Schema.String,
args: Schema.Array(Schema.String),
cwd: Schema.String,
status: Schema.Literals(["running", "exited"]),
// Windows ConPTY assigns the child pid asynchronously, so 0 is valid at spawn time.
pid: NonNegativeInt,
// Present once status is "exited".
exitCode: Schema.optional(NonNegativeInt),
}).annotate({ identifier: "Pty" })
export type Info = Types.DeepMutable<typeof Info.Type>
@@ -90,7 +100,12 @@ export class ExitedError extends Schema.TaggedErrorClass<ExitedError>()("Pty.Exi
ptyID: PtyID,
}) {}
export const Event = PtyEvent
export const Event = {
Created: EventV2.define({ type: "pty.created", schema: { info: Info } }),
Updated: EventV2.define({ type: "pty.updated", schema: { info: Info } }),
Exited: EventV2.define({ type: "pty.exited", schema: { id: PtyID, exitCode: NonNegativeInt } }),
Deleted: EventV2.define({ type: "pty.deleted", schema: { id: PtyID } }),
}
export interface Interface {
readonly list: () => Effect.Effect<Info[]>
+13 -1
View File
@@ -1 +1,13 @@
export { ID as PtyID } from "@opencode-ai/schema/pty"
import { Schema } from "effect"
import { Identifier } from "../id/id"
import { withStatics } from "../schema"
const ptyIdSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
export type PtyID = typeof ptyIdSchema.Type
export const PtyID = ptyIdSchema.pipe(
withStatics((schema: typeof ptyIdSchema) => ({
ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)),
})),
)
@@ -1,3 +0,0 @@
export * as PublicEventManifest from "./public-event-manifest"
export { ServerDefinitions as Definitions } from "@opencode-ai/schema/event-manifest"
+58 -10
View File
@@ -1,35 +1,83 @@
export * as QuestionV2 from "./question"
import { Context, Deferred, Effect, Layer, Schema } from "effect"
import { Question } from "@opencode-ai/schema/question"
import { EventV2 } from "./event"
import { Identifier } from "./id/id"
import { withStatics } from "./schema"
import { SessionSchema } from "./session/schema"
export const ID = Question.ID
export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
Schema.brand("QuestionV2.ID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(Identifier.ascending("question", id)) })),
)
export type ID = typeof ID.Type
export const Option = Question.Option
export const Option = Schema.Struct({
label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
description: Schema.String.annotate({ description: "Explanation of choice" }),
}).annotate({ identifier: "QuestionV2.Option" })
export type Option = typeof Option.Type
export const Info = Question.Info
const base = {
question: Schema.String.annotate({ description: "Complete question" }),
header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
options: Schema.Array(Option).annotate({ description: "Available choices" }),
multiple: Schema.Boolean.pipe(Schema.optional).annotate({ description: "Allow selecting multiple choices" }),
}
export const Info = Schema.Struct({
...base,
custom: Schema.Boolean.pipe(Schema.optional).annotate({
description: "Allow typing a custom answer (default: true)",
}),
}).annotate({ identifier: "QuestionV2.Info" })
export type Info = typeof Info.Type
export const Prompt = Question.Prompt
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" })
export type Prompt = typeof Prompt.Type
export const Tool = Question.Tool
export const Tool = Schema.Struct({
messageID: Schema.String,
callID: Schema.String,
}).annotate({ identifier: "QuestionV2.Tool" })
export type Tool = typeof Tool.Type
export const Request = Question.Request
export const Request = Schema.Struct({
id: ID,
sessionID: SessionSchema.ID,
questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
tool: Tool.pipe(Schema.optional),
}).annotate({ identifier: "QuestionV2.Request" })
export type Request = typeof Request.Type
export const Answer = Question.Answer
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" })
export type Answer = typeof Answer.Type
export const Reply = Question.Reply
export const Reply = Schema.Struct({
answers: Schema.Array(Answer).annotate({
description: "User answers in order of questions (each answer is an array of selected labels)",
}),
}).annotate({ identifier: "QuestionV2.Reply" })
export type Reply = typeof Reply.Type
export const Event = Question.Event
export const Event = {
Asked: EventV2.define({ type: "question.v2.asked", schema: Request.fields }),
Replied: EventV2.define({
type: "question.v2.replied",
schema: {
sessionID: SessionSchema.ID,
requestID: ID,
answers: Schema.Array(Answer),
},
}),
Rejected: EventV2.define({
type: "question.v2.rejected",
schema: {
sessionID: SessionSchema.ID,
requestID: ID,
},
}),
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionV2.RejectedError", {}) {
override get message() {
+3 -1
View File
@@ -18,7 +18,9 @@ export type GitSource = Reference.GitSource
export const Source = Reference.Source
export type Source = Reference.Source
export const Event = Reference.Event
export const Event = {
Updated: EventV2.define({ type: "reference.updated", schema: {} }),
}
export class Info extends Schema.Class<Info>("Reference.Info")({
name: Schema.String,
+468 -2
View File
@@ -1,2 +1,468 @@
export * from "@opencode-ai/schema/session-event"
export * as SessionEvent from "@opencode-ai/schema/session-event"
import { Schema } from "effect"
import { ProviderMetadata, ToolContent } from "@opencode-ai/schema/llm"
import { Delivery } from "@opencode-ai/schema/session-delivery"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "../schema"
import { FileAttachment, Prompt } from "./prompt"
import { SessionSchema } from "./schema"
import { Location } from "../location"
import { SessionMessageID } from "./message-id"
import { SessionMessage } from "./message"
export { FileAttachment }
export const Source = Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
text: Schema.String,
}).annotate({
identifier: "session.next.event.source",
})
export type Source = typeof Source.Type
const Base = {
timestamp: DateTimeUtcFromMillis,
sessionID: SessionSchema.ID,
}
const PromptFields = {
...Base,
messageID: SessionMessageID.ID,
prompt: Prompt,
delivery: Delivery,
}
const options = {
durable: {
aggregate: "sessionID",
version: 1,
},
} as const
const stepSettlementOptions = {
durable: {
aggregate: "sessionID",
version: 2,
},
} as const
export const UnknownError = SessionMessage.UnknownError
export type UnknownError = SessionMessage.UnknownError
export const AgentSwitched = EventV2.define({
type: "session.next.agent.switched",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
agent: Schema.String,
},
})
export type AgentSwitched = typeof AgentSwitched.Type
export const ModelSwitched = EventV2.define({
type: "session.next.model.switched",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
model: ModelV2.Ref,
},
})
export type ModelSwitched = typeof ModelSwitched.Type
export const Moved = EventV2.define({
type: "session.next.moved",
...options,
schema: {
...Base,
location: Location.Ref,
subdirectory: RelativePath.pipe(Schema.optional),
},
})
export type Moved = typeof Moved.Type
export const Prompted = EventV2.define({
type: "session.next.prompted",
...options,
schema: PromptFields,
})
export type Prompted = typeof Prompted.Type
export const PromptAdmitted = EventV2.define({
type: "session.next.prompt.admitted",
...options,
schema: PromptFields,
})
export type PromptAdmitted = typeof PromptAdmitted.Type
export const ContextUpdated = EventV2.define({
type: "session.next.context.updated",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type ContextUpdated = typeof ContextUpdated.Type
export const Synthetic = EventV2.define({
type: "session.next.synthetic",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type Synthetic = typeof Synthetic.Type
export namespace Shell {
export const Started = EventV2.define({
type: "session.next.shell.started",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
callID: Schema.String,
command: Schema.String,
},
})
export type Started = typeof Started.Type
export const Ended = EventV2.define({
type: "session.next.shell.ended",
...options,
schema: {
...Base,
callID: Schema.String,
output: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export namespace Step {
export const Started = EventV2.define({
type: "session.next.step.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
agent: Schema.String,
model: ModelV2.Ref,
snapshot: Schema.String.pipe(Schema.optional),
},
})
export type Started = typeof Started.Type
export const Ended = EventV2.define({
type: "session.next.step.ended",
...stepSettlementOptions,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
finish: Schema.String,
cost: Schema.Finite,
tokens: Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
snapshot: Schema.String.pipe(Schema.optional),
},
})
export type Ended = typeof Ended.Type
export const Failed = EventV2.define({
type: "session.next.step.failed",
...stepSettlementOptions,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
error: UnknownError,
},
})
export type Failed = typeof Failed.Type
}
export namespace Text {
export const Started = EventV2.define({
type: "session.next.text.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Text.Ended is the replayable full-value boundary.
export const Delta = EventV2.define({
type: "session.next.text.delta",
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.text.ended",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
textID: Schema.String,
text: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export namespace Reasoning {
export const Started = EventV2.define({
type: "session.next.reasoning.started",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
providerMetadata: ProviderMetadata.pipe(Schema.optional),
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Reasoning.Ended is the replayable full-value boundary.
export const Delta = EventV2.define({
type: "session.next.reasoning.delta",
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.reasoning.ended",
...options,
schema: {
...Base,
assistantMessageID: SessionMessageID.ID,
reasoningID: Schema.String,
text: Schema.String,
providerMetadata: ProviderMetadata.pipe(Schema.optional),
},
})
export type Ended = typeof Ended.Type
}
export namespace Tool {
const ToolBase = {
...Base,
assistantMessageID: SessionMessageID.ID,
callID: Schema.String,
}
export namespace Input {
export const Started = EventV2.define({
type: "session.next.tool.input.started",
...options,
schema: {
...ToolBase,
name: Schema.String,
},
})
export type Started = typeof Started.Type
// Stream fragments are live-only; Input.Ended is the replayable raw-input boundary.
export const Delta = EventV2.define({
type: "session.next.tool.input.delta",
schema: {
...ToolBase,
delta: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.tool.input.ended",
...options,
schema: {
...ToolBase,
text: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
export const Called = EventV2.define({
type: "session.next.tool.called",
...options,
schema: {
...ToolBase,
tool: Schema.String,
input: Schema.Record(Schema.String, Schema.Unknown),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Called = typeof Called.Type
/**
* Replayable bounded running-tool state. Tools should checkpoint semantic
* transitions or at a bounded cadence, not persist every stdout/stderr chunk.
*/
export const Progress = EventV2.define({
type: "session.next.tool.progress",
...options,
schema: {
...ToolBase,
structured: Schema.Record(Schema.String, Schema.Any),
content: Schema.Array(ToolContent),
},
})
export type Progress = typeof Progress.Type
export const Success = EventV2.define({
type: "session.next.tool.success",
...options,
schema: {
...ToolBase,
structured: Schema.Record(Schema.String, Schema.Any),
content: Schema.Array(ToolContent),
outputPaths: Schema.Array(Schema.String).pipe(Schema.optional),
result: Schema.Unknown.pipe(Schema.optional),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Success = typeof Success.Type
export const Failed = EventV2.define({
type: "session.next.tool.failed",
...options,
schema: {
...ToolBase,
error: UnknownError,
result: Schema.Unknown.pipe(Schema.optional),
provider: Schema.Struct({
executed: Schema.Boolean,
metadata: ProviderMetadata.pipe(Schema.optional),
}),
},
})
export type Failed = typeof Failed.Type
}
export const RetryError = Schema.Struct({
message: Schema.String,
statusCode: Schema.Finite.pipe(Schema.optional),
isRetryable: Schema.Boolean,
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
responseBody: Schema.String.pipe(Schema.optional),
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
}).annotate({
identifier: "session.next.retry_error",
})
export type RetryError = typeof RetryError.Type
export const Retried = EventV2.define({
type: "session.next.retried",
...options,
schema: {
...Base,
attempt: Schema.Finite,
error: RetryError,
},
})
export type Retried = typeof Retried.Type
export namespace Compaction {
export const Started = EventV2.define({
type: "session.next.compaction.started",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
},
})
export type Started = typeof Started.Type
export const Delta = EventV2.define({
type: "session.next.compaction.delta",
schema: {
...Base,
messageID: SessionMessageID.ID,
text: Schema.String,
},
})
export type Delta = typeof Delta.Type
export const Ended = EventV2.define({
type: "session.next.compaction.ended",
...options,
schema: {
...Base,
messageID: SessionMessageID.ID,
reason: Started.data.fields.reason,
text: Schema.String,
recent: Schema.String,
},
})
export type Ended = typeof Ended.Type
}
const DurableDefinitions = [
AgentSwitched,
ModelSwitched,
Moved,
Prompted,
PromptAdmitted,
ContextUpdated,
Synthetic,
Shell.Started,
Shell.Ended,
Step.Started,
Step.Ended,
Step.Failed,
Text.Started,
Text.Ended,
Tool.Input.Started,
Tool.Input.Ended,
Tool.Called,
Tool.Progress,
Tool.Success,
Tool.Failed,
Reasoning.Started,
Reasoning.Ended,
Retried,
Compaction.Started,
Compaction.Ended,
] as const
const EphemeralDefinitions = [Text.Delta, Tool.Input.Delta, Reasoning.Delta, Compaction.Delta] as const
export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
export type DurableEvent = typeof Durable.Type
export const All = Schema.Union([...DurableDefinitions, ...EphemeralDefinitions], { mode: "oneOf" }).pipe(
Schema.toTaggedUnion("type"),
)
export type Event = typeof All.Type
export type Type = Event["type"]
export * as SessionEvent from "./event"
+55 -30
View File
@@ -50,16 +50,16 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
}
}
export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()(
"SessionRunnerModel.UnsupportedApiError",
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
"SessionRunnerModel.UnsupportedPackageError",
{
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
api: Schema.String,
package: Schema.String,
},
) {
override get message() {
return `Unsupported API for ${this.providerID}/${this.modelID}: ${this.api}`
return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
}
}
@@ -67,7 +67,7 @@ export type Error =
| ModelNotSelectedError
| ModelUnavailableError
| VariantUnavailableError
| UnsupportedApiError
| UnsupportedPackageError
| Integration.AuthorizationError
export interface Interface {
@@ -82,19 +82,19 @@ export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Servic
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
const value = model.settings?.apiKey
if (typeof value === "string") return Auth.value(value)
}
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
const body = model.request.body
const body = model.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,
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
headers: model.headers,
http: { body: httpBody },
limits: { context: model.limit.context, output: model.limit.output },
})
@@ -104,8 +104,8 @@ const withVariant = (
model: ModelV2.Info,
variantID: ModelV2.VariantID | undefined,
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
const id = variantID === "default" || variantID === undefined ? model.request.variant : variantID
const variant = model.variants.find((item) => item.id === id)
const id = variantID === "default" ? undefined : variantID
const variant = model.variants?.find((item) => item.id === id)
if (!variant && variantID !== undefined && variantID !== "default")
return Effect.fail(
new VariantUnavailableError({
@@ -117,53 +117,55 @@ const withVariant = (
return Effect.succeed(
variant
? produce(model, (draft) => {
Object.assign(draft.request.headers, variant.headers)
Object.assign(draft.request.body, variant.body)
draft.settings = merge(draft.settings, variant.settings)
draft.headers = { ...draft.headers, ...variant.headers }
draft.body = merge(draft.body, variant.body)
})
: model,
)
}
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,
): Effect.Effect<Model, UnsupportedApiError> => {
): Effect.Effect<Model, UnsupportedPackageError> => {
const resolved =
credential?.metadata === undefined
? model
: produce(model, (draft) => {
Object.assign(draft.request.body, credential.metadata)
draft.settings = { ...draft.settings, ...credential.metadata }
})
const key = apiKey(resolved, credential)
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
if (resolved.aisdk && resolved.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 }),
.model({ id: resolved.modelID ?? resolved.id }),
)
}
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/anthropic") {
if (resolved.aisdk && resolved.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 }),
.model({ id: resolved.modelID ?? resolved.id }),
)
}
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai-compatible" && resolved.api.url) {
if (
resolved.aisdk &&
resolved.package === "@ai-sdk/openai-compatible" &&
typeof resolved.settings?.baseURL === "string"
) {
return Effect.succeed(
withDefaults(resolved, OpenAICompatibleChat.route)
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
.model({ id: resolved.api.id }),
.model({ id: resolved.modelID ?? resolved.id }),
)
}
return Effect.fail(
new UnsupportedApiError({
new UnsupportedPackageError({
providerID: resolved.providerID,
modelID: resolved.id,
api: apiName(resolved),
package: resolved.aisdk ? `aisdk:${resolved.package}` : (resolved.package ?? "unknown"),
}),
)
}
@@ -172,10 +174,10 @@ export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, creden
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
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))
model.aisdk === true &&
(model.package === "@ai-sdk/openai" ||
model.package === "@ai-sdk/anthropic" ||
(model.package === "@ai-sdk/openai-compatible" && typeof model.settings?.baseURL === "string"))
/** Resolves models from the catalog belonging to the current Location runtime. */
export const locationLayer = Layer.effect(
@@ -213,3 +215,26 @@ export const locationLayer = Layer.effect(
})
}),
)
function merge(
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(
Array.from(new Set([...Object.keys(base), ...Object.keys(overlay)])).map((key) => {
const left = base[key]
const right = overlay[key]
if (right === undefined) return [key, left]
if (plain(left) && plain(right)) return [key, merge(left, right)]
return [key, right]
}),
)
}
function plain(input: unknown): input is Readonly<Record<string, unknown>> {
if (typeof input !== "object" || input === null || Array.isArray(input)) return false
const prototype = Object.getPrototypeOf(input)
return prototype === Object.prototype || prototype === null
}
+17 -4
View File
@@ -1,17 +1,30 @@
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 { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { EventV2 } from "../event"
import { SessionSchema } from "./schema"
import { TodoTable } from "./sql"
export const Info = SessionTodoInfo
export const Info = Schema.Struct({
content: Schema.String.annotate({ description: "Brief description of the task" }),
status: Schema.String.annotate({
description: "Current status of the task: pending, in_progress, completed, cancelled",
}),
priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
}).annotate({ identifier: "SessionTodo.Info" })
export type Info = typeof Info.Type
export const Event = SessionTodo.Event
export const Event = {
Updated: EventV2.define({
type: "todo.updated",
schema: {
sessionID: SessionSchema.ID,
todos: Schema.Array(Info),
},
}),
}
export interface Interface {
readonly update: (input: {
+12 -26
View File
@@ -173,15 +173,11 @@ function migrateProvider(info: ConfigProviderV1.Info) {
return {
name: info.name,
env: info.env,
api: info.npm
? {
type: "aisdk" as const,
package: info.npm,
url: info.api ?? options.url,
settings: options.settings ?? {},
}
: undefined,
request: info.options && { headers: options.headers, body: options.body },
package: info.npm,
aisdk: info.npm ? (true as const) : undefined,
settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings,
headers: info.options && options.headers,
body: info.options && options.body,
models:
info.models &&
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])),
@@ -191,7 +187,7 @@ function migrateProvider(info: ConfigProviderV1.Info) {
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 settings = info.options && lowerer.model(info.options)
const costs = info.cost && [
{
input: info.cost.input,
@@ -214,29 +210,19 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
: undefined
return {
modelID: info.id,
family: info.family,
name: info.name,
api: info.provider?.npm
? {
...(info.id === undefined ? {} : { id: info.id }),
type: "aisdk" as const,
package: info.provider.npm,
url: info.provider.api,
settings: {},
}
: info.id === undefined
? undefined
: { id: info.id },
package: info.provider?.npm,
aisdk: info.provider?.npm ? (true as const) : undefined,
settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
capabilities,
request: (info.headers || request) && {
headers: info.headers,
body: request,
},
headers: info.headers,
variants:
info.variants &&
Object.entries(info.variants).map(([id, options]) => ({
id,
body: lowerer.request(options),
settings: lowerer.model(options),
})),
cost: costs,
disabled: info.status === "deprecated" ? true : undefined,
+12 -189
View File
@@ -3,209 +3,32 @@ export * as ConfigProviderOptionsV1 from "./provider-options"
type Options = Readonly<Record<string, unknown>>
export interface ProviderResult {
readonly settings: Record<string, unknown>
readonly headers?: Record<string, string>
readonly body?: Record<string, unknown>
readonly url?: string
readonly settings?: Record<string, unknown>
}
export interface Lowerer {
readonly provider: (options: Options) => ProviderResult
readonly request: (options: Options) => Record<string, unknown>
readonly model: (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 = {
const lowerer: 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"]),
settings: Object.fromEntries(Object.entries(options).filter(([key]) => key !== "headers" && key !== "body")),
headers: record(options.headers, (value): value is string => typeof value === "string"),
body: record(options.body, (_value): _value is unknown => true),
}
},
request: snake,
model: (options) => ({ ...options }),
}
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
},
export function get(_packageName?: string): Lowerer {
return lowerer
}
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 {
headers: headers(options.headers),
body: body(options.body),
settings: omit(options, ["headers", "body", ...extraKeys]),
}
}
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)
function record<T>(input: unknown, guard: (value: unknown) => value is T) {
if (typeof input !== "object" || input === null || Array.isArray(input)) return undefined
return Object.fromEntries(Object.entries(input).filter((entry): entry is [string, T] => guard(entry[1])))
}
+65 -2
View File
@@ -1,8 +1,71 @@
export * as PermissionV1 from "./permission"
import { Schema } from "effect"
export * from "@opencode-ai/schema/permission-v1"
import { ID } from "@opencode-ai/schema/permission-v1"
import { ProjectV2 } from "../project"
import { withStatics } from "../schema"
import { SessionSchema } from "../session/schema"
import { Identifier } from "../util/identifier"
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + Identifier.ascending()) })),
)
export type ID = typeof ID.Type
export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" })
export type Action = typeof Action.Type
export const Rule = Schema.Struct({
permission: Schema.String,
pattern: Schema.String,
action: Action,
}).annotate({ identifier: "PermissionRule" })
export type Rule = typeof Rule.Type
export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" })
export type Ruleset = typeof Ruleset.Type
export const Request = Schema.Struct({
id: ID,
sessionID: SessionSchema.ID,
permission: Schema.String,
patterns: Schema.Array(Schema.String),
metadata: Schema.Record(Schema.String, Schema.Unknown),
always: Schema.Array(Schema.String),
tool: Schema.Struct({
messageID: Schema.String,
callID: Schema.String,
}).pipe(Schema.optional),
}).annotate({ identifier: "PermissionRequest" })
export type Request = typeof Request.Type
export const Reply = Schema.Literals(["once", "always", "reject"])
export type Reply = typeof Reply.Type
export const ReplyBody = Schema.Struct({
reply: Reply,
message: Schema.String.pipe(Schema.optional),
}).annotate({ identifier: "PermissionReplyBody" })
export type ReplyBody = typeof ReplyBody.Type
export const Approval = Schema.Struct({
projectID: ProjectV2.ID,
patterns: Schema.Array(Schema.String),
}).annotate({ identifier: "PermissionApproval" })
export type Approval = typeof Approval.Type
export const AskInput = Schema.Struct({
...Request.fields,
id: ID.pipe(Schema.optional),
ruleset: Ruleset,
}).annotate({ identifier: "PermissionAskInput" })
export type AskInput = typeof AskInput.Type
export const ReplyInput = Schema.Struct({
requestID: ID,
...ReplyBody.fields,
}).annotate({ identifier: "PermissionReplyInput" })
export type ReplyInput = typeof ReplyInput.Type
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionRejectedError", {}) {
override get message() {
+607 -43
View File
@@ -1,52 +1,39 @@
export * as SessionV1 from "./session"
import { Schema } from "effect"
import { Effect, Schema, Types } from "effect"
import { EventV2 } from "../event"
import { PermissionV1 } from "./permission"
import { ProjectV2 } from "../project"
import { ProviderV2 } from "../provider"
import { ModelV2 } from "../model"
import { optionalOmitUndefined, withStatics } from "../schema"
import { Identifier } from "../util/identifier"
import { NonNegativeInt } from "../schema"
import { NamedError } from "../util/error"
import { SessionSchema } from "../session/schema"
import { WorkspaceV2 } from "../workspace"
export {
AgentPart,
AgentPartInput,
Assistant,
CompactionPart,
Event,
FilePart,
FilePartInput,
FilePartSource,
FileSource,
Format,
Info,
MessageID,
OutputFormatJsonSchema,
OutputFormatText,
Part,
PartID,
PatchPart,
Range,
ReasoningPart,
ResourceSource,
RetryPart,
SessionInfo,
SnapshotPart,
StepFinishPart,
StepStartPart,
SubtaskPart,
SubtaskPartInput,
SymbolSource,
TextPart,
TextPartInput,
ToolPart,
ToolState,
ToolStateCompleted,
ToolStateError,
ToolStatePending,
ToolStateRunning,
User,
WithParts,
} from "@opencode-ai/schema/session-v1"
const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0))
export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(
Schema.brand("MessageID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + Identifier.ascending()) })),
)
export type MessageID = typeof MessageID.Type
export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe(
Schema.brand("PartID"),
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + Identifier.ascending()) })),
)
export type PartID = typeof PartID.Type
export const OutputLengthError = NamedError.create("MessageOutputLengthError", {})
export const AuthError = NamedError.create("ProviderAuthError", { providerID: Schema.String, message: Schema.String })
export const AuthError = NamedError.create("ProviderAuthError", {
providerID: Schema.String,
message: Schema.String,
})
export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String })
export const StructuredOutputError = NamedError.create("StructuredOutputError", {
message: Schema.String,
@@ -65,4 +52,581 @@ export const ContextOverflowError = NamedError.create("ContextOverflowError", {
message: Schema.String,
responseBody: Schema.optional(Schema.String),
})
export const ContentFilterError = NamedError.create("ContentFilterError", { message: Schema.String })
export const ContentFilterError = NamedError.create("ContentFilterError", {
message: Schema.String,
})
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
type: Schema.Literal("text"),
}) {}
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
type: Schema.Literal("json_schema"),
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
}) {}
export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
discriminator: "type",
identifier: "OutputFormat",
})
export type OutputFormat = Schema.Schema.Type<typeof Format>
const partBase = {
id: PartID,
sessionID: SessionSchema.ID,
messageID: MessageID,
}
export const SnapshotPart = Schema.Struct({
...partBase,
type: Schema.Literal("snapshot"),
snapshot: Schema.String,
}).annotate({ identifier: "SnapshotPart" })
export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>>
export const PatchPart = Schema.Struct({
...partBase,
type: Schema.Literal("patch"),
hash: Schema.String,
files: Schema.Array(Schema.String),
}).annotate({ identifier: "PatchPart" })
export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>>
export const TextPart = Schema.Struct({
...partBase,
type: Schema.Literal("text"),
text: Schema.String,
synthetic: Schema.optional(Schema.Boolean),
ignored: Schema.optional(Schema.Boolean),
time: Schema.optional(
Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "TextPart" })
export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>>
export const ReasoningPart = Schema.Struct({
...partBase,
type: Schema.Literal("reasoning"),
text: Schema.String,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
}).annotate({ identifier: "ReasoningPart" })
export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>>
const filePartSourceBase = {
text: Schema.Struct({
value: Schema.String,
start: Schema.Finite,
end: Schema.Finite,
}).annotate({ identifier: "FilePartSourceText" }),
}
export const Range = Schema.Struct({
start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
}).annotate({ identifier: "Range" })
export type Range = typeof Range.Type
export const FileSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("file"),
path: Schema.String,
}).annotate({ identifier: "FileSource" })
export const SymbolSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("symbol"),
path: Schema.String,
range: Range,
name: Schema.String,
kind: NonNegativeInt,
}).annotate({ identifier: "SymbolSource" })
export const ResourceSource = Schema.Struct({
...filePartSourceBase,
type: Schema.Literal("resource"),
clientName: Schema.String,
uri: Schema.String,
}).annotate({ identifier: "ResourceSource" })
export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({
discriminator: "type",
identifier: "FilePartSource",
})
export const FilePart = Schema.Struct({
...partBase,
type: Schema.Literal("file"),
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(FilePartSource),
}).annotate({ identifier: "FilePart" })
export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>>
export const AgentPart = Schema.Struct({
...partBase,
type: Schema.Literal("agent"),
name: Schema.String,
source: Schema.optional(
Schema.Struct({
value: Schema.String,
start: NonNegativeInt,
end: NonNegativeInt,
}),
),
}).annotate({ identifier: "AgentPart" })
export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>>
export const CompactionPart = Schema.Struct({
...partBase,
type: Schema.Literal("compaction"),
auto: Schema.Boolean,
overflow: Schema.optional(Schema.Boolean),
tail_start_id: Schema.optional(MessageID),
}).annotate({ identifier: "CompactionPart" })
export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
export const SubtaskPart = Schema.Struct({
...partBase,
type: Schema.Literal("subtask"),
prompt: Schema.String,
description: Schema.String,
agent: Schema.String,
model: Schema.optional(
Schema.Struct({
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
}),
),
command: Schema.optional(Schema.String),
}).annotate({ identifier: "SubtaskPart" })
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
export const RetryPart = Schema.Struct({
...partBase,
type: Schema.Literal("retry"),
attempt: NonNegativeInt,
error: APIError.EffectSchema,
time: Schema.Struct({
created: NonNegativeInt,
}),
}).annotate({ identifier: "RetryPart" })
export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & {
error: APIError
}
export const StepStartPart = Schema.Struct({
...partBase,
type: Schema.Literal("step-start"),
snapshot: Schema.optional(Schema.String),
}).annotate({ identifier: "StepStartPart" })
export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>>
export const StepFinishPart = Schema.Struct({
...partBase,
type: Schema.Literal("step-finish"),
reason: Schema.String,
snapshot: Schema.optional(Schema.String),
cost: Schema.Finite,
tokens: Schema.Struct({
total: Schema.optional(Schema.Finite),
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
}).annotate({ identifier: "StepFinishPart" })
export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>>
export const ToolStatePending = Schema.Struct({
status: Schema.Literal("pending"),
input: Schema.Record(Schema.String, Schema.Any),
raw: Schema.String,
}).annotate({ identifier: "ToolStatePending" })
export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>>
export const ToolStateRunning = Schema.Struct({
status: Schema.Literal("running"),
input: Schema.Record(Schema.String, Schema.Any),
title: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
}),
}).annotate({ identifier: "ToolStateRunning" })
export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>>
export const ToolStateCompleted = Schema.Struct({
status: Schema.Literal("completed"),
input: Schema.Record(Schema.String, Schema.Any),
output: Schema.String,
title: Schema.String,
metadata: Schema.Record(Schema.String, Schema.Any),
time: Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
compacted: Schema.optional(NonNegativeInt),
}),
attachments: Schema.optional(Schema.Array(FilePart)),
}).annotate({ identifier: "ToolStateCompleted" })
export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>>
export const ToolStateError = Schema.Struct({
status: Schema.Literal("error"),
input: Schema.Record(Schema.String, Schema.Any),
error: Schema.String,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
start: NonNegativeInt,
end: NonNegativeInt,
}),
}).annotate({ identifier: "ToolStateError" })
export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>>
export const ToolState = Schema.Union([
ToolStatePending,
ToolStateRunning,
ToolStateCompleted,
ToolStateError,
]).annotate({
discriminator: "status",
identifier: "ToolState",
})
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
export const ToolPart = Schema.Struct({
...partBase,
type: Schema.Literal("tool"),
callID: Schema.String,
tool: Schema.String,
state: ToolState,
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "ToolPart" })
export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & {
state: ToolState
}
const messageBase = {
id: MessageID,
sessionID: partBase.sessionID,
}
const FileDiff = Schema.Struct({
file: Schema.optional(Schema.String),
patch: Schema.optional(Schema.String),
additions: Schema.Finite,
deletions: Schema.Finite,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
}).annotate({ identifier: "SnapshotFileDiff" })
export const User = Schema.Struct({
...messageBase,
role: Schema.Literal("user"),
time: Schema.Struct({
created: Timestamp,
}),
format: Schema.optional(Format),
summary: Schema.optional(
Schema.Struct({
title: Schema.optional(Schema.String),
body: Schema.optional(Schema.String),
diffs: Schema.Array(FileDiff),
}),
),
agent: Schema.String,
model: Schema.Struct({
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
variant: Schema.optional(Schema.String),
}),
system: Schema.optional(Schema.String),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
}).annotate({ identifier: "UserMessage" })
export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
export const Part = Schema.Union([
TextPart,
SubtaskPart,
ReasoningPart,
FilePart,
ToolPart,
StepStartPart,
StepFinishPart,
SnapshotPart,
PatchPart,
AgentPart,
RetryPart,
CompactionPart,
]).annotate({ discriminator: "type", identifier: "Part" })
export type Part =
| TextPart
| SubtaskPart
| ReasoningPart
| FilePart
| ToolPart
| StepStartPart
| StepFinishPart
| SnapshotPart
| PatchPart
| AgentPart
| RetryPart
| CompactionPart
const AssistantErrorSchema = Schema.Union([
AuthError.EffectSchema,
NamedError.Unknown.EffectSchema,
OutputLengthError.EffectSchema,
AbortedError.EffectSchema,
StructuredOutputError.EffectSchema,
ContextOverflowError.EffectSchema,
ContentFilterError.EffectSchema,
APIError.EffectSchema,
]).annotate({ discriminator: "name" })
type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
export const TextPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("text"),
text: Schema.String,
synthetic: Schema.optional(Schema.Boolean),
ignored: Schema.optional(Schema.Boolean),
time: Schema.optional(
Schema.Struct({
start: NonNegativeInt,
end: Schema.optional(NonNegativeInt),
}),
),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}).annotate({ identifier: "TextPartInput" })
export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>>
export const FilePartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("file"),
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(FilePartSource),
}).annotate({ identifier: "FilePartInput" })
export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>>
export const AgentPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("agent"),
name: Schema.String,
source: Schema.optional(
Schema.Struct({
value: Schema.String,
start: NonNegativeInt,
end: NonNegativeInt,
}),
),
}).annotate({ identifier: "AgentPartInput" })
export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
export const SubtaskPartInput = Schema.Struct({
id: Schema.optional(PartID),
type: Schema.Literal("subtask"),
prompt: Schema.String,
description: Schema.String,
agent: Schema.String,
model: Schema.optional(
Schema.Struct({
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
}),
),
command: Schema.optional(Schema.String),
}).annotate({ identifier: "SubtaskPartInput" })
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
export const Assistant = Schema.Struct({
...messageBase,
role: Schema.Literal("assistant"),
time: Schema.Struct({
created: NonNegativeInt,
completed: Schema.optional(NonNegativeInt),
}),
error: Schema.optional(AssistantErrorSchema),
parentID: MessageID,
modelID: ModelV2.ID,
providerID: ProviderV2.ID,
mode: Schema.String,
agent: Schema.String,
path: Schema.Struct({
cwd: Schema.String,
root: Schema.String,
}),
summary: Schema.optional(Schema.Boolean),
cost: Schema.Finite,
tokens: Schema.Struct({
total: Schema.optional(Schema.Finite),
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
}),
structured: Schema.optional(Schema.Any),
variant: Schema.optional(Schema.String),
finish: Schema.optional(Schema.String),
}).annotate({ identifier: "AssistantMessage" })
export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
error?: AssistantError
}
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
export type Info = User | Assistant
export const WithParts = Schema.Struct({
info: Info,
parts: Schema.Array(Part),
})
export type WithParts = {
info: Info
parts: Part[]
}
const options = {
durable: {
aggregate: "sessionID",
version: 1,
},
} as const
const SessionSummary = Schema.Struct({
additions: Schema.Finite,
deletions: Schema.Finite,
files: Schema.Finite,
diffs: optionalOmitUndefined(Schema.Array(FileDiff)),
})
const SessionTokens = Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
reasoning: Schema.Finite,
cache: Schema.Struct({
read: Schema.Finite,
write: Schema.Finite,
}),
})
const SessionShare = Schema.Struct({
url: Schema.String,
})
const SessionRevert = Schema.Struct({
messageID: MessageID,
partID: optionalOmitUndefined(PartID),
snapshot: optionalOmitUndefined(Schema.String),
diff: optionalOmitUndefined(Schema.String),
})
const SessionModel = Schema.Struct({
id: ModelV2.ID,
providerID: ProviderV2.ID,
variant: optionalOmitUndefined(Schema.String),
})
export const SessionInfo = Schema.Struct({
id: SessionSchema.ID,
slug: Schema.String,
projectID: ProjectV2.ID,
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
directory: Schema.String,
path: optionalOmitUndefined(Schema.String),
parentID: optionalOmitUndefined(SessionSchema.ID),
summary: optionalOmitUndefined(SessionSummary),
cost: optionalOmitUndefined(Schema.Finite),
tokens: optionalOmitUndefined(SessionTokens),
share: optionalOmitUndefined(SessionShare),
title: Schema.String,
agent: optionalOmitUndefined(Schema.String),
model: optionalOmitUndefined(SessionModel),
version: Schema.String,
metadata: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Any)),
time: Schema.Struct({
created: NonNegativeInt,
updated: NonNegativeInt,
compacting: optionalOmitUndefined(NonNegativeInt),
archived: optionalOmitUndefined(Schema.Finite),
}),
permission: optionalOmitUndefined(PermissionV1.Ruleset),
revert: optionalOmitUndefined(SessionRevert),
}).annotate({ identifier: "Session" })
export type SessionInfo = typeof SessionInfo.Type
export const Event = {
Created: EventV2.define({
type: "session.created",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
Updated: EventV2.define({
type: "session.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
Deleted: EventV2.define({
type: "session.deleted",
...options,
schema: {
sessionID: SessionSchema.ID,
info: SessionInfo,
},
}),
MessageUpdated: EventV2.define({
type: "message.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
info: Info,
},
}),
MessageRemoved: EventV2.define({
type: "message.removed",
...options,
schema: {
sessionID: SessionSchema.ID,
messageID: MessageID,
},
}),
PartUpdated: EventV2.define({
type: "message.part.updated",
...options,
schema: {
sessionID: SessionSchema.ID,
part: Part,
time: Schema.Finite,
},
}),
PartRemoved: EventV2.define({
type: "message.part.removed",
...options,
schema: {
sessionID: SessionSchema.ID,
messageID: MessageID,
partID: PartID,
},
}),
}
+33 -49
View File
@@ -65,14 +65,14 @@ describe("CatalogV2", () => {
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
yield* credentials.create({
integrationID,
label: "Second",
value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }),
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
}).pipe(Effect.provide(layer))
})
@@ -135,111 +135,95 @@ describe("CatalogV2", () => {
),
)
it.effect("normalizes provider baseURL into api url", () =>
it.effect("keeps provider API settings flat", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
yield* catalog.transform((catalog) =>
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://default.example.com",
}
provider.request.body.baseURL = "https://override.example.com"
provider.package = "@ai-sdk/openai-compatible"
provider.aisdk = true
provider.settings = { baseURL: "https://override.example.com" }
}),
)
expect(required(yield* catalog.provider.get(providerID)).api).toEqual({
type: "aisdk",
expect(required(yield* catalog.provider.get(providerID))).toMatchObject({
package: "@ai-sdk/openai-compatible",
url: "https://override.example.com",
aisdk: true,
settings: { baseURL: "https://override.example.com" },
})
}),
)
it.effect("normalizes model baseURL into api url", () =>
it.effect("resolves model settings and upstream model ID", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
provider.package = "@ai-sdk/openai-compatible"
provider.aisdk = true
provider.settings = { baseURL: "https://provider.example.com" }
})
catalog.model.update(providerID, modelID, (model) => {
model.api = {
id: modelID,
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://model.example.com",
}
model.request.body.baseURL = "https://override.example.com"
model.modelID = ModelV2.ID.make("upstream-model")
model.settings = { baseURL: "https://override.example.com" }
})
})
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
id: modelID,
type: "aisdk",
modelID: ModelV2.ID.make("upstream-model"),
package: "@ai-sdk/openai-compatible",
url: "https://override.example.com",
settings: {},
aisdk: true,
settings: { baseURL: "https://override.example.com" },
})
}),
)
it.effect("resolves default model api from provider api", () =>
it.effect("resolves default model transport from provider transport", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
}
provider.package = "@ai-sdk/openai-compatible"
provider.aisdk = true
provider.settings = { baseURL: "https://provider.example.com" }
})
catalog.model.update(providerID, modelID, () => {})
})
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
id: modelID,
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://provider.example.com",
aisdk: true,
settings: { baseURL: "https://provider.example.com" },
})
}),
)
it.effect("resolves provider and model request merges", () =>
it.effect("resolves provider and model overlay merges", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const modelID = ModelV2.ID.make("model")
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.request.headers.provider = "provider"
provider.request.headers.shared = "provider"
provider.request.body.provider = true
provider.headers = { provider: "provider", shared: "provider" }
provider.body = { provider: true }
})
catalog.model.update(providerID, modelID, (model) => {
model.request.headers.model = "model"
model.request.headers.shared = "model"
model.request.body.model = true
model.request.body.request = true
model.request.body.shared = "model"
model.headers = { model: "model", shared: "model" }
model.body = { model: true, request: true, shared: "model" }
})
})
const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
expect(model.request.body).toEqual({ provider: true, model: true, request: true, shared: "model" })
expect(model.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
expect(model.body).toEqual({ provider: true, model: true, request: true, shared: "model" })
}),
)
+17 -25
View File
@@ -43,11 +43,7 @@ function testLayer(
}
const provider = {
api: { type: "native", settings: {} },
request: {
headers: {},
body: {},
},
package: "test-provider",
models: {},
}
@@ -93,6 +89,7 @@ describe("Config", () => {
provider: {
bedrock: {
npm: "@ai-sdk/amazon-bedrock",
api: "https://bedrock.example.com",
options: {
headers: { "x-test": "1" },
body: { trace: true },
@@ -103,13 +100,10 @@ describe("Config", () => {
},
})
expect(migrated.providers?.bedrock?.api).toEqual({
type: "aisdk",
expect(migrated.providers?.bedrock).toMatchObject({
package: "@ai-sdk/amazon-bedrock",
url: undefined,
settings: { region: "us-east-1", profile: "dev" },
})
expect(migrated.providers?.bedrock?.request).toEqual({
aisdk: true,
settings: { baseURL: "https://bedrock.example.com", region: "us-east-1", profile: "dev" },
headers: { "x-test": "1" },
body: { trace: true },
})
@@ -585,34 +579,32 @@ describe("Config", () => {
})
expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
expect(documents[0]?.info.providers?.custom).toMatchObject({
request: { body: { apiKey: "secret" } },
settings: { apiKey: "secret" },
models: {
model: {
request: { body: { reasoningEffort: "high" } },
variants: [{ id: "fast", body: { temperature: 0.2 } }],
settings: { reasoningEffort: "high" },
variants: [{ id: "fast", settings: { temperature: 0.2 } }],
},
},
})
expect(documents[0]?.info.providers?.openai).toMatchObject({
api: { settings: {} },
request: { headers: { Authorization: "Bearer secret", "OpenAI-Organization": "org" } },
package: "@ai-sdk/openai",
aisdk: true,
settings: { apiKey: "secret", organization: "org" },
models: {
model: {
request: {
body: { temperature: 0.3, reasoning_effort: "high", service_tier: "priority" },
},
variants: [{ id: "high", body: { reasoning_effort: "high", reasoning_summary: "auto" } }],
settings: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" },
variants: [{ id: "high", settings: { reasoningEffort: "high", reasoningSummary: "auto" } }],
},
},
})
expect(documents[0]?.info.providers?.anthropic).toMatchObject({
models: {
model: {
request: {
body: {
output_config: { effort: "high", task_budget: 4096 },
metadata: { user_id: "user-1" },
},
settings: {
effort: "high",
taskBudget: 4096,
metadata: { userId: "user-1" },
},
},
},
@@ -2,20 +2,7 @@ import { describe, expect, test } from "bun:test"
import { ConfigProviderOptionsV1 } from "@opencode-ai/core/v1/config/provider-options"
describe("ConfigProviderOptionsV1", () => {
test("keeps raw provider and request options unchanged", () => {
const lowerer = ConfigProviderOptionsV1.get("custom-provider")
expect(lowerer.provider({ apiKey: "secret", headers: { "x-test": "1" }, nested: { camelCase: true } })).toEqual({
body: { apiKey: "secret", headers: { "x-test": "1" }, nested: { camelCase: true } },
})
expect(lowerer.request({ nested: { camelCase: true } })).toEqual({ nested: { camelCase: true } })
})
test("falls back to raw lowering for prototype property package names", () => {
expect(ConfigProviderOptionsV1.get("toString").provider({ enabled: true })).toEqual({ body: { enabled: true } })
})
test("lowers OpenAI provider and request options", () => {
test("splits provider overlays without changing package settings", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/openai")
expect(
@@ -23,189 +10,43 @@ describe("ConfigProviderOptionsV1", () => {
apiKey: "secret",
baseURL: "https://openai.example/v1",
organization: "org",
project: "project",
headers: { "x-test": "1" },
headers: { "x-test": "1", invalid: true },
body: { store: true },
timeout: 1000,
nested: { camelCase: true },
}),
).toEqual({
url: "https://openai.example/v1",
headers: {
Authorization: "Bearer secret",
"OpenAI-Organization": "org",
"OpenAI-Project": "project",
"x-test": "1",
settings: {
apiKey: "secret",
baseURL: "https://openai.example/v1",
organization: "org",
nested: { camelCase: true },
},
headers: { "x-test": "1" },
body: { store: true },
settings: { timeout: 1000 },
})
expect(lowerer.request({ reasoningEffort: "high", nestedValue: { camelCase: true } })).toEqual({
reasoning_effort: "high",
nested_value: { camel_case: true },
})
})
test("lowers Anthropic provider and request options", () => {
test("keeps model and variant options unchanged", () => {
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",
lowerer.model({
reasoningEffort: "high",
taskBudget: 1024,
metadata: { userId: "user", traceId: "trace" },
nestedValue: { camelCase: true },
metadata: { userId: "user" },
}),
).toEqual({
output_config: { effort: "high", task_budget: 1024 },
metadata: { user_id: "user", trace_id: "trace" },
nested_value: { camel_case: true },
reasoningEffort: "high",
taskBudget: 1024,
metadata: { userId: "user" },
})
})
test("lowers Google provider and request options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/google")
expect(
lowerer.provider({
apiKey: "secret",
baseURL: "https://google.example",
headers: { "x-test": "1" },
body: { trace: true },
project: "project",
}),
).toEqual({
url: "https://google.example",
headers: { "x-goog-api-key": "secret", "x-test": "1" },
body: { trace: true },
settings: { project: "project" },
})
expect(
lowerer.request({
thinkingConfig: { thinkingBudget: 1024 },
responseModalities: ["TEXT"],
mediaResolution: "high",
imageConfig: { aspectRatio: "16:9" },
safetySettings: ["safe"],
}),
).toEqual({
safetySettings: ["safe"],
generationConfig: {
thinkingConfig: { thinkingBudget: 1024 },
responseModalities: ["TEXT"],
mediaResolution: "high",
imageConfig: { aspectRatio: "16:9" },
},
})
})
test("lowers Azure provider options and uses OpenAI request lowering", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/azure")
expect(
lowerer.provider({
apiKey: "secret",
baseURL: "https://azure.example",
headers: { "x-test": "1" },
body: { trace: true },
resourceName: "resource",
}),
).toEqual({
url: "https://azure.example",
headers: { "api-key": "secret", "x-test": "1" },
body: { trace: true },
settings: { resourceName: "resource" },
})
expect(lowerer.request({ reasoningEffort: "high" })).toEqual({ reasoning_effort: "high" })
})
test("lowers Amazon Bedrock provider and request options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/amazon-bedrock")
expect(
lowerer.provider({
headers: { "x-test": "1" },
body: { trace: true },
region: "us-east-1",
profile: "dev",
}),
).toEqual({
headers: { "x-test": "1" },
body: { trace: true },
settings: { region: "us-east-1", profile: "dev" },
})
expect(lowerer.request({ temperature: 0.2 })).toEqual({
additionalModelRequestFields: { temperature: 0.2 },
})
})
test("lowers OpenAI-compatible provider and request options", () => {
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/openai-compatible")
expect(
lowerer.provider({
baseURL: "https://compatible.example/v1",
headers: { "x-test": "1" },
body: { trace: true },
apiKey: "secret",
}),
).toEqual({
url: "https://compatible.example/v1",
headers: { "x-test": "1" },
body: { trace: true },
settings: { apiKey: "secret" },
})
expect(lowerer.request({ reasoningEffort: "high", serviceTier: "priority" })).toEqual({
reasoning_effort: "high",
serviceTier: "priority",
})
})
test.each([
"@ai-sdk/cerebras",
"@ai-sdk/deepinfra",
"@ai-sdk/groq",
"@ai-sdk/mistral",
"@ai-sdk/togetherai",
"@ai-sdk/xai",
"@openrouter/ai-sdk-provider",
"ai-gateway-provider",
"venice-ai-sdk-provider",
])("uses OpenAI-compatible lowering for %s", (packageName) => {
const lowerer = ConfigProviderOptionsV1.get(packageName)
expect(lowerer.provider({ baseURL: "https://example.test", apiKey: "secret" })).toEqual({
url: "https://example.test",
test("uses the same mechanical lowering for every package", () => {
expect(ConfigProviderOptionsV1.get("custom-provider").provider({ enabled: true })).toEqual({
settings: { enabled: true },
headers: undefined,
body: undefined,
settings: { apiKey: "secret" },
})
expect(lowerer.request({ reasoningEffort: "high" })).toEqual({ reasoning_effort: "high" })
})
test.each(["@ai-sdk/google-vertex", "@ai-sdk/google-vertex/anthropic"])(
"uses provider family lowering for %s",
(packageName) => {
const lowerer = ConfigProviderOptionsV1.get(packageName)
expect(lowerer.provider({ baseURL: "https://example.test", profile: "dev" })).toMatchObject({
url: "https://example.test",
settings: { profile: "dev" },
})
},
)
})
+28 -25
View File
@@ -45,13 +45,6 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
)
}
function request(headers: Record<string, string>, variant?: string) {
return {
headers,
variant,
}
}
const decode = Schema.decodeUnknownSync(Config.Info)
describe("ConfigProviderPlugin.Plugin", () => {
@@ -68,7 +61,9 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
opencode: {
api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
package: "@ai-sdk/openai",
aisdk: true,
settings: { baseURL: "https://opencode.test/v1" },
models: {
"alpha-gpt-next": {
variants: [
@@ -119,7 +114,9 @@ describe("ConfigProviderPlugin.Plugin", () => {
info: decode({
providers: {
opencode: {
api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
package: "@ai-sdk/openai",
aisdk: true,
settings: { baseURL: "https://opencode.test/v1" },
},
},
}),
@@ -144,7 +141,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* addPlugin(config)
const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants[0]).toMatchObject({
expect(model.variants?.[0]).toMatchObject({
id: "high",
body: { reasoningEffort: "high" },
})
@@ -169,8 +166,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
custom: {
name: "Configured",
env: ["CUSTOM_API_KEY"],
api: { type: "native", settings: {} },
request: request({ first: "first", shared: "first" }),
package: "native",
headers: { first: "first", shared: "first" },
models: {
chat: {
name: "First",
@@ -178,7 +175,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
disabled: true,
limit: { context: 100, output: 50 },
cost: { input: 1, output: 2 },
request: request({ first: "first", shared: "first" }, "retained"),
settings: { retained: true },
headers: { first: "first", shared: "first" },
variants: [
{
id: "fast",
@@ -197,17 +195,19 @@ describe("ConfigProviderPlugin.Plugin", () => {
model: "custom/default",
providers: {
custom: {
api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" },
request: request({ last: "last", shared: "last" }),
package: "custom-sdk",
aisdk: true,
settings: { baseURL: "https://example.test" },
headers: { last: "last", shared: "last" },
models: {
default: {
name: "Default",
},
chat: {
api: { id: "api-chat" },
modelID: "api-chat",
name: "Last",
limit: { output: 75 },
request: request({ last: "last", shared: "last" }),
headers: { last: "last", shared: "last" },
variants: [
{
id: "fast",
@@ -247,22 +247,25 @@ describe("ConfigProviderPlugin.Plugin", () => {
})
expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
expect(provider.disabled).toBeUndefined()
expect(provider.api).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" })
expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
expect(provider.package).toBe("custom-sdk")
expect(provider.aisdk).toBe(true)
expect(provider.settings).toEqual({ baseURL: "https://example.test" })
expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.id).toBe(modelID)
expect(model.modelID).toBe(ModelV2.ID.make("api-chat"))
expect(model.name).toBe("Last")
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
expect(model.enabled).toBe(false)
expect(model.limit).toEqual({ context: 100, output: 75 })
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
expect(model.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.request.variant).toBe("retained")
expect(model.variants.map((variant) => variant.id)).toEqual([
expect(model.settings).toEqual({ baseURL: "https://example.test", retained: true })
expect(model.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants?.map((variant) => variant.id)).toEqual([
ModelV2.VariantID.make("fast"),
ModelV2.VariantID.make("slow"),
])
expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants[1]?.headers).toEqual({ slow: "slow" })
expect(model.variants?.[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants?.[1]?.headers).toEqual({ slow: "slow" })
}),
),
)
+138 -126
View File
@@ -1,14 +1,10 @@
import { describe, expect } from "bun:test"
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect"
import { EventV2 } from "@opencode-ai/core/event"
import { Event } from "@opencode-ai/schema/event"
import { Session } from "@opencode-ai/schema/session"
import { SessionEvent } from "@opencode-ai/schema/session-event"
import { SessionV1 } from "@opencode-ai/schema/session-v1"
import { Database } from "@opencode-ai/core/database/database"
import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { AbsolutePath, DateTimeUtcFromMillis } from "@opencode-ai/core/schema"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { eq } from "drizzle-orm"
import { location } from "./fixture/location"
@@ -20,6 +16,10 @@ const locationLayer = Layer.succeed(
location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }),
),
)
const eventLayer = Layer.mergeAll(EventV2.defaultLayer, Database.defaultLayer)
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
const itWithoutLocation = testEffect(eventLayer)
const Message = EventV2.define({
type: "test.message",
schema: {
@@ -70,16 +70,18 @@ const VersionedMessage = EventV2.define({
},
})
const DurableMessage = SessionV1.Event.MessageRemoved
const durableData = (sessionID: Session.ID, text: string) => ({
sessionID,
messageID: SessionV1.MessageID.ascending(`msg_${text}`),
const SyncTimestamp = EventV2.define({
type: "test.timestamp",
durable: {
version: 1,
aggregate: "id",
},
schema: {
id: Schema.String,
timestamp: DateTimeUtcFromMillis,
},
})
const eventLayer = Layer.mergeAll(EventV2.layerWith().pipe(Layer.provide(Database.defaultLayer)), Database.defaultLayer)
const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
const itWithoutLocation = testEffect(eventLayer)
describe("EventV2", () => {
it.effect("publishes events with the current location", () =>
Effect.gen(function* () {
@@ -120,21 +122,26 @@ describe("EventV2", () => {
}),
)
it.effect("selects the latest durable definition independent of declaration order", () =>
it.effect("stores definitions in the exported registry", () =>
Effect.sync(() => {
expect(EventV2.registry.get(Message.type)).toBe(Message)
}),
)
it.effect("keeps the latest sync definition in the registry", () =>
Effect.sync(() => {
const latest = EventV2.define({
type: "test.out-of-order",
durable: { version: 2, aggregate: "id" },
schema: { id: Schema.String },
})
const historical = EventV2.define({
EventV2.define({
type: "test.out-of-order",
durable: { version: 1, aggregate: "id" },
schema: { id: Schema.String },
})
expect(Event.latest([latest, historical]).get("test.out-of-order")).toBe(latest)
expect(Event.latest([historical, latest]).get("test.out-of-order")).toBe(latest)
expect(EventV2.registry.get("test.out-of-order")).toBe(latest)
}),
)
@@ -356,19 +363,19 @@ describe("EventV2", () => {
it.effect("replays durable aggregate events after a sequence and tails new events", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "zero"))
yield* events.publish(DurableMessage, durableData(aggregateID, "one"))
const aggregateID = EventV2.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
const fiber = yield* events
.durable({ aggregateID, after: 0 })
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
yield* events.publish(DurableMessage, durableData(aggregateID, "two"))
yield* events.publish(SyncMessage, { id: aggregateID, text: "two" })
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
[1, durableData(aggregateID, "one")],
[2, durableData(aggregateID, "two")],
[1, { id: aggregateID, text: "one" }],
[2, { id: aggregateID, text: "two" }],
])
}),
)
@@ -376,15 +383,20 @@ describe("EventV2", () => {
it.effect("catches durable aggregate events published during replay handoff", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "zero"))
const aggregateID = EventV2.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
yield* events.publish(DurableMessage, durableData(aggregateID, "one"))
yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
[0, durableData(aggregateID, "zero")],
[1, durableData(aggregateID, "one")],
expect(
Array.from(yield* Fiber.join(fiber)).map((event) => [
event.durable?.seq,
(event.data as { text: string }).text,
]),
).toEqual([
[0, "zero"],
[1, "one"],
])
}),
)
@@ -403,16 +415,16 @@ describe("EventV2", () => {
yield* Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Deferred.await(readStarted)
pause = false
yield* events.publish(DurableMessage, durableData(aggregateID, "during handoff"))
yield* events.publish(SyncMessage, { id: aggregateID, text: "during handoff" })
yield* Deferred.succeed(continueRead, undefined)
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
[0, durableData(aggregateID, "during handoff")],
[0, { id: aggregateID, text: "during handoff" }],
])
}).pipe(Effect.provide(Layer.mergeAll(Database.defaultLayer, eventLayer)))
}),
@@ -421,7 +433,7 @@ describe("EventV2", () => {
it.effect("coalesces durable aggregate wakes while draining every committed event", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const count = 64
const fiber = yield* events
.durable({ aggregateID })
@@ -429,11 +441,11 @@ describe("EventV2", () => {
yield* Effect.yieldNow
for (let index = 0; index < count; index++) {
yield* events.publish(DurableMessage, durableData(aggregateID, String(index)))
yield* events.publish(SyncMessage, { id: aggregateID, text: String(index) })
}
expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual(
Array.from({ length: count }, (_, index) => [index, durableData(aggregateID, String(index))]),
Array.from({ length: count }, (_, index) => [index, { id: aggregateID, text: String(index) }]),
)
}),
)
@@ -441,14 +453,14 @@ describe("EventV2", () => {
it.effect("omits live-only events from durable aggregate streams", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const fiber = yield* events.durable({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
yield* events.publish(Message, { text: "live only" })
yield* events.publish(DurableMessage, durableData(aggregateID, "durable"))
yield* events.publish(SyncMessage, { id: aggregateID, text: "durable" })
expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([DurableMessage.type])
expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([SyncMessage.type])
}),
)
@@ -475,23 +487,23 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>()
yield* events.project(DurableMessage, (event) =>
yield* events.project(SyncMessage, (event) =>
Effect.sync(() => {
received.push(event)
}),
)
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
yield* events.replay({
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "hello"),
data: { id: aggregateID, text: "hello" },
})
expect(received[0]?.type).toBe(DurableMessage.type)
expect(received[0]?.data).toEqual(durableData(aggregateID, "hello"))
expect(received[0]?.type).toBe(SyncMessage.type)
expect(received[0]?.data).toEqual({ id: aggregateID, text: "hello" })
}),
)
@@ -499,14 +511,14 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const { db } = yield* Database.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
yield* events.replay({
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "replayed"),
data: { id: aggregateID, text: "replayed" },
})
const rows = yield* db
.select()
@@ -526,11 +538,11 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const { db } = yield* Database.Service
const envelopeAggregateID = Session.ID.create()
const payloadAggregateID = Session.ID.create()
const envelopeAggregateID = EventV2.ID.create()
const payloadAggregateID = EventV2.ID.create()
const received = new Array<EventV2.Payload>()
yield* events.publish(DurableMessage, durableData(payloadAggregateID, "seed"))
yield* events.project(DurableMessage, (event) =>
yield* events.publish(SyncMessage, { id: payloadAggregateID, text: "seed" })
yield* events.project(SyncMessage, (event) =>
Effect.sync(() => {
received.push(event)
}),
@@ -539,10 +551,10 @@ describe("EventV2", () => {
const exit = yield* events
.replay({
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1,
aggregateID: envelopeAggregateID,
data: durableData(payloadAggregateID, "replayed"),
data: { id: payloadAggregateID, text: "replayed" },
})
.pipe(Effect.exit)
const rows = yield* db
@@ -568,22 +580,22 @@ describe("EventV2", () => {
it.effect("replay defects on sequence mismatch", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
yield* events.replay({
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "first"),
data: { id: aggregateID, text: "first" },
})
const exit = yield* events
.replay({
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 5,
aggregateID,
data: durableData(aggregateID, "bad"),
data: { id: aggregateID, text: "bad" },
})
.pipe(Effect.exit)
@@ -594,9 +606,9 @@ describe("EventV2", () => {
it.effect("replay decodes synchronized transformed values before projection", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const received = new Array<typeof SessionEvent.ContextUpdated.Type>()
yield* events.project(SessionEvent.ContextUpdated, (event) =>
const aggregateID = EventV2.ID.create()
const received = new Array<typeof SyncTimestamp.Type>()
yield* events.project(SyncTimestamp, (event) =>
Effect.sync(() => {
received.push(event)
}),
@@ -604,10 +616,10 @@ describe("EventV2", () => {
yield* events.replay({
id: EventV2.ID.create(),
type: EventV2.versionedType(SessionEvent.ContextUpdated.type, 1),
type: EventV2.versionedType(SyncTimestamp.type, 1),
seq: 0,
aggregateID,
data: { sessionID: aggregateID, messageID: "msg_context", timestamp: 0, text: "context" },
data: { id: aggregateID, timestamp: 0 },
})
expect(received[0]?.data.timestamp).toEqual(DateTime.makeUnsafe(0))
@@ -634,21 +646,21 @@ describe("EventV2", () => {
it.effect("replayAll validates contiguous aggregate events", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const source = yield* events.replayAll([
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "one"),
data: { id: aggregateID, text: "one" },
},
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1,
aggregateID,
data: durableData(aggregateID, "two"),
data: { id: aggregateID, text: "two" },
},
])
@@ -660,38 +672,38 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const { db } = yield* Database.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const one = yield* events.replayAll([
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "one"),
data: { id: aggregateID, text: "one" },
},
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1,
aggregateID,
data: durableData(aggregateID, "two"),
data: { id: aggregateID, text: "two" },
},
])
const two = yield* events.replayAll([
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 2,
aggregateID,
data: durableData(aggregateID, "three"),
data: { id: aggregateID, text: "three" },
},
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 3,
aggregateID,
data: durableData(aggregateID, "four"),
data: { id: aggregateID, text: "four" },
},
])
const rows = yield* db
@@ -711,10 +723,10 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>()
const aggregateID = Session.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "seed"))
const aggregateID = EventV2.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "seed" })
yield* events.claim(aggregateID, "owner-a")
yield* events.project(DurableMessage, (event) =>
yield* events.project(SyncMessage, (event) =>
Effect.sync(() => {
received.push(event)
}),
@@ -723,10 +735,10 @@ describe("EventV2", () => {
yield* events.replay(
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1,
aggregateID,
data: durableData(aggregateID, "ignored"),
data: { id: aggregateID, text: "ignored" },
},
{ ownerID: "owner-b" },
)
@@ -738,14 +750,14 @@ describe("EventV2", () => {
it.effect("strict owner fences exact replay", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const id = EventV2.ID.create()
const replayed = {
id,
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "owned"),
data: { id: aggregateID, text: "owned" },
}
yield* events.replay(replayed, { ownerID: "owner-a" })
@@ -759,11 +771,11 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const { db } = yield* Database.Service
const aggregateID = Session.ID.create()
const published = yield* events.publish(DurableMessage, durableData(aggregateID, "owned"))
const aggregateID = EventV2.ID.create()
const published = yield* events.publish(SyncMessage, { id: aggregateID, text: "owned" })
const replayed = {
id: published.id,
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: published.durable!.seq,
aggregateID,
data: published.data,
@@ -780,7 +792,7 @@ describe("EventV2", () => {
expect(row?.ownerID).toBe("owner-a")
const exit = yield* events
.replay(
{ ...replayed, id: EventV2.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") },
{ ...replayed, id: EventV2.ID.create(), seq: 1, data: { id: aggregateID, text: "conflict" } },
{ ownerID: "owner-b", strictOwner: true },
)
.pipe(Effect.exit)
@@ -792,15 +804,15 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const { db } = yield* Database.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
yield* events.replay(
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "owned"),
data: { id: aggregateID, text: "owned" },
},
{ ownerID: "owner-1" },
)
@@ -819,26 +831,26 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const { db } = yield* Database.Service
const aggregateID = Session.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "local"))
const aggregateID = EventV2.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "local" })
yield* events.replay(
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1,
aggregateID,
data: durableData(aggregateID, "claimed"),
data: { id: aggregateID, text: "claimed" },
},
{ ownerID: "owner-1" },
)
yield* events.replay(
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 2,
aggregateID,
data: durableData(aggregateID, "fenced"),
data: { id: aggregateID, text: "fenced" },
},
{ ownerID: "owner-2" },
)
@@ -863,14 +875,14 @@ describe("EventV2", () => {
it.effect("strict replay rejects an owner conflict instead of silently skipping it", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
yield* events.replay(
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "claimed"),
data: { id: aggregateID, text: "claimed" },
},
{ ownerID: "owner-1" },
)
@@ -879,10 +891,10 @@ describe("EventV2", () => {
.replay(
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1,
aggregateID,
data: durableData(aggregateID, "conflict"),
data: { id: aggregateID, text: "conflict" },
},
{ ownerID: "owner-2", strictOwner: true },
)
@@ -896,14 +908,14 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>()
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
yield* events.listen((event) => Effect.sync(() => received.push(event)))
const replayed = {
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "replayed"),
data: { id: aggregateID, text: "replayed" },
}
yield* events.replay(replayed, { publish: true })
@@ -917,19 +929,19 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>()
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const replayed = {
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "original"),
data: { id: aggregateID, text: "original" },
}
yield* events.listen((event) => Effect.sync(() => received.push(event)))
yield* events.replay(replayed, { publish: true })
const exit = yield* events
.replay({ ...replayed, data: durableData(aggregateID, "divergent") }, { publish: true })
.replay({ ...replayed, data: { id: aggregateID, text: "divergent" } }, { publish: true })
.pipe(Effect.exit)
expect(String(exit)).toContain("Replay diverged")
@@ -940,23 +952,23 @@ describe("EventV2", () => {
it.effect("rejects an event ID reused at another aggregate position", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const id = EventV2.ID.create()
yield* events.replay({
id,
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "first"),
data: { id: aggregateID, text: "first" },
})
const exit = yield* events
.replay({
id,
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1,
aggregateID,
data: durableData(aggregateID, "second"),
data: { id: aggregateID, text: "second" },
})
.pipe(Effect.exit)
@@ -968,27 +980,27 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const { db } = yield* Database.Service
const aggregateID = Session.ID.create()
const aggregateID = EventV2.ID.create()
const received = new Array<EventV2.Payload>()
yield* events.listen((event) => Effect.sync(() => received.push(event)))
yield* events.replay(
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "first"),
data: { id: aggregateID, text: "first" },
},
{ ownerID: "owner-1" },
)
yield* events.replay(
{
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 1,
aggregateID,
data: durableData(aggregateID, "ignored"),
data: { id: aggregateID, text: "ignored" },
},
{ ownerID: "owner-2", publish: true },
)
@@ -1035,10 +1047,10 @@ describe("EventV2", () => {
Effect.gen(function* () {
const events = yield* EventV2.Service
const received = new Array<EventV2.Payload>()
const aggregateID = Session.ID.create()
yield* events.publish(DurableMessage, durableData(aggregateID, "seed"))
const aggregateID = EventV2.ID.create()
yield* events.publish(SyncMessage, { id: aggregateID, text: "seed" })
yield* events.remove(aggregateID)
yield* events.project(DurableMessage, (event) =>
yield* events.project(SyncMessage, (event) =>
Effect.sync(() => {
received.push(event)
}),
@@ -1046,13 +1058,13 @@ describe("EventV2", () => {
yield* events.replay({
id: EventV2.ID.create(),
type: EventV2.versionedType(DurableMessage.type, 1),
type: EventV2.versionedType(SyncMessage.type, 1),
seq: 0,
aggregateID,
data: durableData(aggregateID, "replayed"),
data: { id: aggregateID, text: "replayed" },
})
expect(received[0]?.data).toEqual(durableData(aggregateID, "replayed"))
expect(received[0]?.data).toEqual({ id: aggregateID, text: "replayed" })
}),
)
})
@@ -1,16 +0,0 @@
import { describe, expect, test } from "bun:test"
import { SessionV1 as Wire } from "@opencode-ai/schema/session-v1"
import { SessionV1 } from "../src/v1/session"
describe("legacy event schema compatibility", () => {
test("Core references canonical SessionV1 definitions", () => {
expect(SessionV1.Event.Created).toBe(Wire.Event.Created)
expect(SessionV1.Event.PartUpdated).toBe(Wire.Event.PartUpdated)
})
test("Core retains NamedError constructor identity", () => {
const error = new SessionV1.APIError({ message: "failed", isRetryable: false })
expect(error).toBeInstanceOf(SessionV1.APIError)
expect(error.toObject()).toEqual({ name: "APIError", data: { message: "failed", isRetryable: false } })
})
})
+1 -1
View File
@@ -168,7 +168,7 @@ describe("LocationServiceMap", () => {
providers: {
unavailable: {
name: "Unavailable",
api: { type: "native", settings: {} },
package: "test-provider",
models: { chat: { disabled: true } },
},
},
+11 -12
View File
@@ -118,7 +118,7 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
id: ModelV2.ID.make(current.id),
providerID: ProviderV2.ID.make(current.providerID),
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
variants: current.variants.map((variant) => ({
variants: current.variants?.map((variant) => ({
...variant,
id: ModelV2.VariantID.make(variant.id),
})),
@@ -272,29 +272,28 @@ function agentInfo(value: AgentV2.Info) {
function providerInfo(value: ProviderV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
settings: value.settings && { ...value.settings },
headers: value.headers && { ...value.headers },
body: value.body && { ...value.body },
}
}
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
settings: value.settings && { ...value.settings },
headers: value.headers && { ...value.headers },
body: value.body && { ...value.body },
capabilities: {
...value.capabilities,
input: [...value.capabilities.input],
output: [...value.capabilities.output],
},
request: {
...value.request,
headers: { ...value.request.headers },
body: { ...value.request.body },
},
variants: value.variants.map((variant) => ({
variants: value.variants?.map((variant) => ({
...variant,
headers: { ...variant.headers },
body: { ...variant.body },
settings: variant.settings && { ...variant.settings },
headers: variant.headers && { ...variant.headers },
body: variant.body && { ...variant.body },
})),
time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
@@ -28,7 +28,9 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("qwen"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/alibaba",
options: { name: "alibaba" },
@@ -45,7 +47,9 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("qwen"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "alibaba" },
@@ -62,7 +66,9 @@ describe("AlibabaPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("qwen"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/alibaba",
options: { name: "custom-alibaba", apiKey: "test" },
@@ -74,17 +80,19 @@ describe("AlibabaPlugin", () => {
}),
)
it.effect("uses the old default languageModel(api.id) behavior", () =>
it.effect("uses the default languageModel(modelID) behavior", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin()
const item = ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("qwen-plus"),
aisdk: true,
package: "test-provider",
})
const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
const language = result.sdk?.languageModel(item.api.id)
const language = result.sdk?.languageModel(item.modelID ?? item.id)
expect(language?.modelId).toBe("qwen-plus")
expect(language?.provider).toBe("alibaba.chat")
}),
@@ -79,31 +79,32 @@ function openAIUrl(language: unknown, path: string, modelId: string) {
}
describe("AmazonBedrockPlugin", () => {
it.effect("moves endpoint option to api URL", () =>
it.effect("moves endpoint option to baseURL", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
const bedrock = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
request: {
headers: {},
body: { endpoint: "https://bedrock.example" },
},
aisdk: true,
package: "@ai-sdk/amazon-bedrock",
headers: {},
settings: { endpoint: "https://bedrock.example" },
})
catalog.provider.update(bedrock.id, (item) => {
item.api = bedrock.api
item.request = bedrock.request
item.aisdk = bedrock.aisdk
item.package = bedrock.package
item.headers = bedrock.headers
item.settings = { endpoint: "https://bedrock.example" }
})
})
yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
expect(result.api).toEqual({
type: "aisdk",
expect(result).toMatchObject({
aisdk: true,
package: "@ai-sdk/amazon-bedrock",
url: "https://bedrock.example",
settings: { baseURL: "https://bedrock.example" },
})
expect(result.request.body.endpoint).toBeUndefined()
expect(result.settings?.endpoint).toBeUndefined()
}),
)
@@ -116,7 +117,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -141,7 +144,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -175,11 +180,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: {
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -199,7 +202,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock", region: "eu-west-1" },
@@ -218,7 +223,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -237,7 +244,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: { name: "amazon-bedrock" },
@@ -257,7 +266,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -286,7 +297,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -314,11 +327,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
api: {
id: ModelV2.ID.make("openai.gpt-5.5"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
modelID: ModelV2.ID.make("openai.gpt-5.5"),
aisdk: true,
package: "@ai-sdk/amazon-bedrock/mantle",
}),
package: "@ai-sdk/amazon-bedrock/mantle",
options: {
@@ -345,11 +356,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
api: {
id: ModelV2.ID.make("openai.gpt-5.5"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
modelID: ModelV2.ID.make("openai.gpt-5.5"),
aisdk: true,
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" },
@@ -357,11 +366,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
api: {
id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
aisdk: true,
package: "@ai-sdk/amazon-bedrock/mantle",
}),
sdk: fakeSelectorSdk(calls),
options: { region: "us-east-1" },
@@ -378,11 +385,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: {
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/anthropic",
},
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "@ai-sdk/amazon-bedrock/anthropic",
}),
package: "@ai-sdk/amazon-bedrock/anthropic",
options: { name: "amazon-bedrock" },
@@ -409,11 +414,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: {
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/amazon-bedrock",
options: {
@@ -444,7 +447,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -452,7 +457,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -460,11 +467,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
api: {
id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -472,7 +477,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-northeast-1" },
@@ -480,7 +487,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-southeast-2" },
@@ -505,7 +514,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -591,7 +602,9 @@ describe("AmazonBedrockPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make(item.modelID),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: item.region },
@@ -610,7 +623,9 @@ describe("AmazonBedrockPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "eu-west-1" },
@@ -31,19 +31,22 @@ describe("AnthropicPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
api: { type: "aisdk", package: "@ai-sdk/anthropic" },
request: { headers: { Existing: "1" }, body: {} },
aisdk: true,
package: "@ai-sdk/anthropic",
headers: { Existing: "1" },
settings: {},
})
catalog.provider.update(item.id, (draft) => {
draft.api = item.api
draft.request = item.request
draft.aisdk = item.aisdk
draft.package = item.package
draft.headers = item.headers
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.["anthropic-beta"]).toBe(
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
)
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.Existing).toBe("1")
}),
)
@@ -52,9 +55,7 @@ describe("AnthropicPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {}))
yield* addPlugin()
expect(
required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined()
}),
)
@@ -66,7 +67,9 @@ describe("AnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
aisdk: true,
package: "@ai-sdk/anthropic",
}),
package: "@ai-sdk/anthropic",
options: { name: "custom-anthropic", apiKey: "test" },
@@ -83,7 +86,9 @@ describe("AnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
aisdk: true,
package: "@ai-sdk/anthropic",
}),
package: "@ai-sdk/anthropic",
options: { name: "anthropic", apiKey: "test" },
@@ -66,18 +66,18 @@ describe("AzureCognitiveServicesPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
item.aisdk = true
item.package = "@ai-sdk/openai-compatible"
})
})
yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
expect(result.api).toEqual({
type: "aisdk",
expect(result).toMatchObject({
aisdk: true,
package: "@ai-sdk/openai-compatible",
url: "https://cognitive.cognitiveservices.azure.com/openai",
settings: { baseURL: "https://cognitive.cognitiveservices.azure.com/openai" },
})
expect(result.request.body.baseURL).toBeUndefined()
expect(result.request.body.resourceName).toBeUndefined()
expect(result.settings?.resourceName).toBeUndefined()
}),
),
)
@@ -89,26 +89,30 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")),
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
aisdk: true,
package: "@ai-sdk/openai-compatible",
})
const openai = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "test-provider" },
aisdk: true,
package: "test-provider",
})
catalog.provider.update(azure.id, (item) => {
item.api = azure.api
item.aisdk = azure.aisdk
item.package = azure.package
})
catalog.provider.update(openai.id, (item) => {
item.api = openai.api
item.aisdk = openai.aisdk
item.package = openai.package
})
})
yield* addPlugin()
const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
expect(azure.request.body.baseURL).toBeUndefined()
expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
expect(openai.request.body.baseURL).toBeUndefined()
expect(openai.api).toEqual({ type: "aisdk", package: "test-provider" })
expect(azure.settings?.baseURL).toBeUndefined()
expect(azure).toMatchObject({ aisdk: true, package: "@ai-sdk/openai-compatible" })
expect(openai.settings?.baseURL).toBeUndefined()
expect(openai).toMatchObject({ aisdk: true, package: "test-provider" })
}),
),
)
@@ -122,7 +126,9 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
@@ -140,7 +146,9 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -148,7 +156,9 @@ describe("AzureCognitiveServicesPlugin", () => {
const ignored = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -168,7 +178,9 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("messages-deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
options: {},
@@ -176,7 +188,9 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("chat-deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
options: {},
@@ -184,7 +198,9 @@ describe("AzureCognitiveServicesPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("language-deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: sdk.languageModel },
options: {},
@@ -66,11 +66,12 @@ describe("AzurePlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
item.aisdk = true
item.package = "@ai-sdk/azure"
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
}),
),
)
@@ -82,18 +83,22 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "from-config" } },
aisdk: true,
package: "@ai-sdk/azure",
headers: {},
settings: { resourceName: "from-config" },
})
catalog.provider.update(azure.id, (item) => {
item.api = azure.api
item.request = azure.request
item.aisdk = azure.aisdk
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: "from-config" }
})
catalog.provider.update(ProviderV2.ID.openai, () => {})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-config")
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.resourceName).toBeUndefined()
}),
),
)
@@ -105,16 +110,20 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "" } },
aisdk: true,
package: "@ai-sdk/azure",
headers: {},
settings: { resourceName: "" },
})
catalog.provider.update(azure.id, (item) => {
item.api = azure.api
item.request = azure.request
item.aisdk = azure.aisdk
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: "" }
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
}),
),
)
@@ -126,16 +135,20 @@ describe("AzurePlugin", () => {
yield* catalog.transform((catalog) => {
const azure = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.azure),
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: " " } },
aisdk: true,
package: "@ai-sdk/azure",
headers: {},
settings: { resourceName: " " },
})
catalog.provider.update(azure.id, (item) => {
item.api = azure.api
item.request = azure.request
item.aisdk = azure.aisdk
item.package = azure.package
item.headers = azure.headers
item.settings = { resourceName: " " }
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
}),
),
)
@@ -149,7 +162,9 @@ describe("AzurePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/azure",
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
@@ -168,7 +183,9 @@ describe("AzurePlugin", () => {
.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/azure",
options: { name: "azure" },
@@ -188,7 +205,9 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
@@ -206,7 +225,9 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
@@ -224,8 +245,11 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
request: { headers: {}, body: { useCompletionUrls: true } },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
headers: {},
settings: { useCompletionUrls: true },
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -243,7 +267,9 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -251,7 +277,9 @@ describe("AzurePlugin", () => {
const ignored = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -274,7 +302,9 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("messages-deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
options: {},
@@ -282,7 +312,9 @@ describe("AzurePlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("language-deployment"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: make("languageModel") },
options: {},
@@ -36,12 +36,13 @@ describe("CerebrasPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
item.request.headers.Existing = "1"
item.aisdk = true
item.package = "@ai-sdk/cerebras"
item.headers = { ...item.headers, Existing: "1" }
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.headers).toEqual({
Existing: "1",
"X-Cerebras-3rd-Party-Integration": "opencode",
})
@@ -53,7 +54,7 @@ describe("CerebrasPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.headers).toBeUndefined()
}),
)
@@ -69,11 +70,9 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/cerebras",
options: { name: "custom-cerebras", apiKey: "test" },
@@ -95,11 +94,9 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/cerebras",
options: { name: "configured-cerebras", apiKey: "test" },
@@ -120,11 +117,9 @@ describe("CerebrasPlugin", () => {
ProviderV2.ID.make("custom-cerebras"),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
),
api: {
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/groq",
options: { name: "custom-cerebras", apiKey: "test" },
@@ -118,7 +118,9 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -139,7 +141,9 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: {
@@ -183,7 +187,9 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: {
@@ -212,7 +218,9 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: {
@@ -249,7 +257,9 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: {
@@ -280,7 +290,9 @@ describe("CloudflareAIGatewayPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -302,7 +314,9 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -325,7 +339,9 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -354,7 +370,9 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
@@ -380,11 +398,9 @@ describe("CloudflareAIGatewayPlugin", () => {
ProviderV2.ID.make("cloudflare-ai-gateway"),
ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
),
api: {
id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "ai-gateway-provider",
options: { name: "cloudflare-ai-gateway" },
@@ -412,7 +428,9 @@ describe("CloudflareAIGatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-ai-gateway" },
@@ -87,7 +87,8 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider" }
provider.aisdk = true
provider.package = "test-provider"
}),
)
yield* addPlugin()
@@ -95,15 +96,18 @@ describe("CloudflareWorkersAIPlugin", () => {
const sdk = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: { id: ModelV2.ID.make("@cf/model"), ...provider.api },
modelID: ModelV2.ID.make("@cf/model"),
aisdk: provider.aisdk,
package: provider.package,
settings: provider.settings,
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
})
expect(provider.api).toEqual({
type: "aisdk",
expect(provider).toMatchObject({
aisdk: true,
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1",
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" },
})
expect(sdk.sdk).toBeDefined()
}),
@@ -116,14 +120,16 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
provider.aisdk = true
provider.package = "test-provider"
provider.settings = { ...provider.settings, baseURL: "https://proxy.example/v1" }
}),
)
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
type: "aisdk",
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
aisdk: true,
package: "test-provider",
url: "https://proxy.example/v1",
settings: { baseURL: "https://proxy.example/v1" },
})
}),
),
@@ -138,12 +144,10 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
modelID: ModelV2.ID.make("@cf/model"),
aisdk: true,
package: "@ai-sdk/openai-compatible",
settings: { baseURL: "https://proxy.example/v1" },
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
@@ -159,15 +163,16 @@ describe("CloudflareWorkersAIPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider" }
provider.request.body.accountId = "configured-acct"
provider.aisdk = true
provider.package = "test-provider"
provider.settings = { ...provider.settings, accountId: "configured-acct" }
}),
)
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
type: "aisdk",
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
aisdk: true,
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" },
})
}),
),
@@ -182,12 +187,10 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
modelID: ModelV2.ID.make("@cf/model"),
aisdk: true,
package: "@ai-sdk/openai-compatible",
settings: { baseURL: "https://proxy.example/v1" },
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -214,12 +217,10 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
},
modelID: ModelV2.ID.make("@cf/model"),
aisdk: true,
package: "@ai-sdk/openai-compatible",
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" },
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -243,7 +244,9 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("@cf/api-model"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -262,12 +265,10 @@ describe("CloudflareWorkersAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
api: {
id: ModelV2.ID.make("@cf/model"),
type: "aisdk",
package: "@ai-sdk/anthropic",
url: "https://proxy.example/v1",
},
modelID: ModelV2.ID.make("@cf/model"),
aisdk: true,
package: "@ai-sdk/anthropic",
settings: { baseURL: "https://proxy.example/v1" },
}),
package: "@ai-sdk/anthropic",
options: { name: "cloudflare-workers-ai" },
@@ -56,7 +56,9 @@ describe("CoherePlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("command"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cohere" },
@@ -66,7 +68,9 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("command"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/cohere",
options: { name: "cohere" },
@@ -83,7 +87,9 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("command-r-plus"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/cohere",
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
@@ -108,7 +114,9 @@ describe("CoherePlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("command-r-plus"),
aisdk: true,
package: "test-provider",
}),
sdk,
options: {},
@@ -48,7 +48,9 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
@@ -66,7 +68,9 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "custom-deepinfra", apiKey: "test" },
@@ -85,7 +89,9 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra", apiKey: "test" },
@@ -111,7 +117,9 @@ describe("DeepInfraPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "@ai-sdk/deepinfra",
}),
package: item,
options: { name: "deepinfra" },
@@ -122,7 +130,9 @@ describe("DeepInfraPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "@ai-sdk/deepinfra",
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
@@ -141,17 +151,15 @@ describe("DeepInfraPlugin", () => {
const sdkEvent = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")),
api: {
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
type: "aisdk",
package: "@ai-sdk/deepinfra",
},
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
aisdk: true,
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.api.id)
const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
expect(language.provider).toBe("deepinfra.chat")
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
}),
@@ -53,7 +53,9 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
modelID: ModelV2.ID.make("test-model"),
aisdk: true,
package: fixtureProvider,
}),
package: fixtureProvider,
options: { name: "custom", marker: "dynamic" },
@@ -71,7 +73,9 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
modelID: ModelV2.ID.make("test-model"),
aisdk: true,
package: fixtureProvider,
}),
package: fixtureProvider,
options: { name: "custom", marker: "dynamic" },
@@ -88,7 +92,9 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
modelID: ModelV2.ID.make("test-model"),
aisdk: true,
package: fixtureProvider,
}),
package: fixtureProvider,
options: { name: "custom-provider", marker: "dynamic" },
@@ -104,7 +110,9 @@ describe("DynamicProviderPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" },
modelID: ModelV2.ID.make("test-model"),
aisdk: true,
package: "fixture-provider",
}),
package: "fixture-provider",
options: { name: "npm-provider", marker: "npm" },
@@ -121,7 +129,9 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
modelID: ModelV2.ID.make("alias"),
aisdk: true,
package: "fixture-provider",
}),
)
.pipe(Effect.exit)
@@ -138,7 +148,9 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "file:///missing/provider-factory.js" },
modelID: ModelV2.ID.make("alias"),
aisdk: true,
package: "file:///missing/provider-factory.js",
}),
)
.pipe(Effect.exit)
@@ -157,7 +169,9 @@ describe("DynamicProviderPlugin", () => {
.language(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
modelID: ModelV2.ID.make("alias"),
aisdk: true,
package: "fixture-provider",
}),
)
.pipe(Effect.exit)
@@ -166,7 +180,7 @@ describe("DynamicProviderPlugin", () => {
}),
)
itWithAISDK.effect("uses the model api.id for the default language model", () =>
itWithAISDK.effect("uses the model modelID for the default language model", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -174,7 +188,9 @@ describe("DynamicProviderPlugin", () => {
const language = yield* aisdk.language(
ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("test-model-api"), type: "aisdk", package: fixtureProvider },
modelID: ModelV2.ID.make("test-model-api"),
aisdk: true,
package: fixtureProvider,
}),
)
expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } })
@@ -45,7 +45,9 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/gateway",
options: { name: "gateway" },
@@ -65,11 +67,9 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")),
api: {
id: ModelV2.ID.make("anthropic/claude-sonnet-4"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/gateway",
options: { name: "vercel", apiKey: "test-key" },
@@ -91,7 +91,9 @@ describe("GatewayPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make(modelID),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/vercel",
options: { name: "vercel" },
@@ -101,7 +103,9 @@ describe("GatewayPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make(modelID),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/gateway",
options: { name: "vercel" },
@@ -47,7 +47,9 @@ describe("GithubCopilotPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "github-copilot" },
@@ -55,7 +57,9 @@ describe("GithubCopilotPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/github-copilot",
options: { name: "github-copilot" },
@@ -74,7 +78,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -92,7 +98,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -110,7 +118,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -118,7 +128,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5.1-codex"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -126,7 +138,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-4o"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -134,7 +148,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5-mini"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -142,7 +158,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -166,7 +184,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -174,7 +194,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5-mini"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -182,7 +204,9 @@ describe("GithubCopilotPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -230,7 +254,9 @@ describe("GithubCopilotPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -70,7 +70,9 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude"),
aisdk: true,
package: "test-provider",
}),
package: "gitlab-ai-provider",
options: { name: "gitlab" },
@@ -107,7 +109,9 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude"),
aisdk: true,
package: "test-provider",
}),
package: "gitlab-ai-provider",
options: { name: "gitlab" },
@@ -132,7 +136,9 @@ describe("GitLabPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude"),
aisdk: true,
package: "test-provider",
}),
package: "gitlab-ai-provider",
options: {
@@ -173,7 +179,9 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai",
options: { name: "gitlab" },
@@ -192,11 +200,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")),
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
request: {
headers: {},
body: { workflowRef: "ref", workflowDefinition: "definition" },
},
modelID: ModelV2.ID.make("duo-workflow-custom"),
aisdk: true,
package: "test-provider",
headers: {},
settings: { workflowRef: "ref", workflowDefinition: "definition" },
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
@@ -227,7 +235,9 @@ describe("GitLabPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("duo-workflow-exact"),
aisdk: true,
package: "test-provider",
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
@@ -245,7 +255,7 @@ describe("GitLabPlugin", () => {
}),
)
it.effect("uses provider feature flags instead of request feature flags", () =>
it.effect("uses provider feature flags instead of model settings feature flags", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -254,11 +264,11 @@ describe("GitLabPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
request: {
headers: {},
body: { featureFlags: { request_flag: true } },
},
modelID: ModelV2.ID.make("duo-workflow-custom"),
aisdk: true,
package: "test-provider",
headers: {},
settings: { featureFlags: { request_flag: true } },
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
@@ -282,8 +292,11 @@ describe("GitLabPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
request: { headers: { h: "v" }, body: {} },
modelID: ModelV2.ID.make("claude"),
aisdk: true,
package: "test-provider",
headers: { h: "v" },
settings: {},
}),
sdk: {
workflowChat: () => undefined,
@@ -64,16 +64,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
provider.aisdk = true
provider.package = "@ai-sdk/google-vertex/anthropic"
}),
)
yield* addPlugin(GoogleVertexAnthropicPlugin)
expect(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project,
).toBe("cloud-project")
expect(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
).toBe("cloud-location")
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
"cloud-project",
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
"cloud-location",
)
}),
),
)
@@ -84,18 +85,18 @@ describe("GoogleVertexAnthropicPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
provider.request.body.project = "configured-project"
provider.request.body.location = "configured-location"
provider.aisdk = true
provider.package = "@ai-sdk/google-vertex/anthropic"
provider.settings = { ...provider.settings, project: "configured-project", location: "configured-location" }
}),
)
yield* addPlugin(GoogleVertexAnthropicPlugin)
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project).toBe(
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
"configured-project",
)
expect(
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
).toBe("configured-location")
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
"configured-location",
)
}),
),
)
@@ -121,7 +122,9 @@ describe("GoogleVertexAnthropicPlugin", () => {
ProviderV2.ID.make("google-vertex-anthropic"),
ModelV2.ID.make("claude-sonnet-4-5"),
),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex-anthropic" },
@@ -147,7 +150,9 @@ describe("GoogleVertexAnthropicPlugin", () => {
ProviderV2.ID.make("google-vertex-anthropic"),
ModelV2.ID.make("claude-sonnet-4-5"),
),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex-anthropic" },
@@ -167,7 +172,9 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "eu" },
@@ -186,7 +193,9 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
@@ -204,7 +213,9 @@ describe("GoogleVertexAnthropicPlugin", () => {
const sdkResult = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/google-vertex/anthropic",
options: { name: "google-vertex", project: "project", location: "us" },
@@ -212,7 +223,9 @@ describe("GoogleVertexAnthropicPlugin", () => {
const languageResult = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
aisdk: true,
package: "test-provider",
}),
sdk: sdkResult.sdk,
options: {},
@@ -234,7 +247,9 @@ describe("GoogleVertexAnthropicPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: selector(calls) },
options: {},
@@ -252,7 +267,9 @@ describe("GoogleVertexAnthropicPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: selector(calls) },
options: {},
@@ -92,17 +92,15 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://opencode.ai/zen/v1",
}
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" }
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
expect(provider.request.body).toEqual({})
expect(provider.settings).toEqual({ baseURL: "https://opencode.ai/zen/v1" })
}),
)
@@ -121,21 +119,26 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = {
...provider.settings,
baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("google-cloud-project")
expect(provider.request.body.location).toBe("google-vertex-location")
expect(provider.api).toEqual({
type: "aisdk",
expect(provider.settings?.project).toBe("google-cloud-project")
expect(provider.settings?.location).toBe("google-vertex-location")
expect(provider).toMatchObject({
aisdk: true,
package: "@ai-sdk/openai-compatible",
url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
settings: {
baseURL:
"https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
},
})
}),
),
@@ -160,10 +163,12 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = {
...provider.settings,
baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
}),
)
@@ -172,21 +177,22 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/google-vertex",
},
modelID: ModelV2.ID.make("gemini"),
aisdk: true,
package: "@ai-sdk/google-vertex",
}),
package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" },
})
expect(provider.request.body.project).toBe("vertex-project")
expect(provider.api).toEqual({
type: "aisdk",
expect(provider.settings?.project).toBe("vertex-project")
expect(provider).toMatchObject({
aisdk: true,
package: "@ai-sdk/openai-compatible",
url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
settings: {
baseURL:
"https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
},
})
expect(vertexOptions[0].project).toBe("vertex-project")
expect(vertexOptions[0].location).toBe("europe-west4")
@@ -209,23 +215,24 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = {
...provider.settings,
baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
provider.request.body.project = "config-project"
provider.request.body.location = "global"
provider.settings = { ...provider.settings, project: "config-project", location: "global" }
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("config-project")
expect(provider.request.body.location).toBe("global")
expect(provider.api).toEqual({
type: "aisdk",
expect(provider.settings?.project).toBe("config-project")
expect(provider.settings?.location).toBe("global")
expect(provider).toMatchObject({
aisdk: true,
package: "@ai-sdk/openai-compatible",
url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global",
settings: { baseURL: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global" },
})
}),
),
@@ -236,21 +243,22 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = {
...provider.settings,
baseURL:
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
provider.request.body.project = "config-project"
provider.request.body.location = "eu"
provider.settings = { ...provider.settings, project: "config-project", location: "eu" }
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.api).toEqual({
type: "aisdk",
expect(provider).toMatchObject({
aisdk: true,
package: "@ai-sdk/openai-compatible",
url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu",
settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" },
})
}),
)
@@ -270,14 +278,15 @@ describe("GoogleVertexPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
provider.request.body.project = "config-project"
provider.aisdk = true
provider.package = "@ai-sdk/google-vertex"
provider.settings = { ...provider.settings, project: "config-project" }
}),
)
yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("config-project")
expect(provider.request.body.location).toBe("us-central1")
expect(provider.settings?.project).toBe("config-project")
expect(provider.settings?.location).toBe("us-central1")
}),
),
)
@@ -297,11 +306,9 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/google-vertex",
},
modelID: ModelV2.ID.make("gemini"),
aisdk: true,
package: "@ai-sdk/google-vertex",
}),
package: "@ai-sdk/google-vertex",
options: { name: "google-vertex" },
@@ -345,11 +352,9 @@ describe("GoogleVertexPlugin", () => {
aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
api: {
id: ModelV2.ID.make("gemini"),
type: "aisdk",
package: "@ai-sdk/openai-compatible",
},
modelID: ModelV2.ID.make("gemini"),
aisdk: true,
package: "@ai-sdk/openai-compatible",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "google-vertex" },
@@ -376,7 +381,9 @@ describe("GoogleVertexPlugin", () => {
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make(" gemini-2.5-pro "),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -27,7 +27,9 @@ describe("GooglePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
modelID: ModelV2.ID.make("gemini"),
aisdk: true,
package: "@ai-sdk/google",
}),
package: "@ai-sdk/google",
options: { name: "custom-google", apiKey: "test" },
@@ -45,7 +47,9 @@ describe("GooglePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
modelID: ModelV2.ID.make("gemini"),
aisdk: true,
package: "@ai-sdk/google",
}),
package: "@ai-sdk/google-vertex",
options: { name: "google" },
@@ -62,7 +66,9 @@ describe("GooglePlugin", () => {
const sdkEvent = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" },
modelID: ModelV2.ID.make("gemini-api"),
aisdk: true,
package: "@ai-sdk/google",
}),
package: "@ai-sdk/google",
options: { name: "custom-google", apiKey: "test" },
@@ -72,7 +78,7 @@ describe("GooglePlugin", () => {
sdk: sdkEvent.sdk,
options: sdkEvent.options,
})
const language = result.language ?? result.sdk.languageModel(result.model.api.id)
const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
expect(language.modelId).toBe("gemini-api")
expect(language.provider).toBe("custom-google")
}),
+17 -11
View File
@@ -28,7 +28,9 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
modelID: ModelV2.ID.make("llama"),
aisdk: true,
package: "@ai-sdk/groq",
}),
package: "@ai-sdk/groq",
options: { name: "groq" },
@@ -45,7 +47,9 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
modelID: ModelV2.ID.make("llama"),
aisdk: true,
package: "@ai-sdk/groq",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "groq" },
@@ -62,7 +66,9 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
modelID: ModelV2.ID.make("llama"),
aisdk: true,
package: "@ai-sdk/groq",
}),
package: "@ai-sdk/groq/compat",
options: { name: "groq" },
@@ -79,7 +85,9 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
modelID: ModelV2.ID.make("llama"),
aisdk: true,
package: "@ai-sdk/groq",
}),
package: "@ai-sdk/groq",
options: { name: "custom-groq", apiKey: "test" },
@@ -93,7 +101,7 @@ describe("GroqPlugin", () => {
}),
)
it.effect("uses the default languageModel(api.id) behavior", () =>
it.effect("uses the default languageModel(modelID) behavior", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -104,16 +112,14 @@ describe("GroqPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
api: {
id: ModelV2.ID.make("llama-api"),
type: "aisdk",
package: "@ai-sdk/groq",
},
modelID: ModelV2.ID.make("llama-api"),
aisdk: true,
package: "@ai-sdk/groq",
}),
sdk,
options: { name: "groq", apiKey: "test" },
})
const language = result.language ?? sdk.languageModel(result.model.api.id)
const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
expect(language.modelId).toBe("llama-api")
expect(language.provider).toBe("groq.chat")
}),
+20 -27
View File
@@ -27,22 +27,20 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
provider.request = { headers: { Existing: "value" }, body: {} }
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
provider.headers = { Existing: "value" }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
}),
)
@@ -51,24 +49,20 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty(
"http-referer",
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("x-title")
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("X-Source")
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("http-referer")
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("x-title")
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("X-Source")
}),
)
@@ -77,23 +71,22 @@ describe("KiloPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.kilo.ai/api/gateway",
}
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://api.kilo.ai/api/gateway" }
})
catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
provider.api = { type: "aisdk", package: "kilo" }
provider.aisdk = true
provider.package = "kilo"
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.headers).toBeUndefined()
}),
)
})
@@ -34,23 +34,21 @@ describe("LLMGatewayPlugin", () => {
})
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.llmgateway.io/v1",
}
provider.request = { headers: { Existing: "value" }, body: {} }
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://api.llmgateway.io/v1" }
provider.headers = { Existing: "value" }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-Source": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
}),
)
@@ -64,17 +62,15 @@ describe("LLMGatewayPlugin", () => {
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
provider.disabled = true
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.llmgateway.io/v1",
}
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://api.llmgateway.io/v1" }
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true)
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toBeUndefined()
}),
)
})
@@ -28,7 +28,9 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("mistral-large"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/mistral",
options: { name: "mistral" },
@@ -45,7 +47,9 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("mistral-large"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "mistral" },
@@ -68,7 +72,9 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("mistral-large"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/mistral",
options: { name: "mistral" },
@@ -92,7 +98,9 @@ describe("MistralPlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("mistral-large"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/mistral",
options: { name: "custom-mistral" },
@@ -101,7 +109,7 @@ describe("MistralPlugin", () => {
}),
)
it.effect("leaves Mistral language selection on the default sdk.languageModel(api.id) path", () =>
it.effect("leaves Mistral language selection on the default sdk.languageModel(modelID) path", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -116,12 +124,14 @@ describe("MistralPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("mistral-large"),
aisdk: true,
package: "test-provider",
}),
sdk,
options: {},
})
const language = result.language ?? sdk.languageModel(result.model.api.id)
const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
expect(calls).toEqual(["languageModel:mistral-large"])
expect(language).toBeDefined()
}),
@@ -27,23 +27,21 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
provider.request = { headers: { Existing: "value" }, body: {} }
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://integrate.api.nvidia.com/v1" }
provider.headers = { Existing: "value" }
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
}),
)
@@ -52,16 +50,14 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://integrate.api.nvidia.com/v1" }
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -74,20 +70,15 @@ describe("NvidiaPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://integrate.api.nvidia.com/v1",
}
provider.request = {
headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
body: { baseURL: "https://integrate.api.nvidia.com/v1" },
}
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://integrate.api.nvidia.com/v1" }
provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
@@ -27,7 +27,9 @@ describe("OpenAICompatiblePlugin", () => {
const defaulted = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "custom" },
@@ -35,7 +37,9 @@ describe("OpenAICompatiblePlugin", () => {
const disabled = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "custom", includeUsage: false },
@@ -53,7 +57,9 @@ describe("OpenAICompatiblePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "file:///tmp/@ai-sdk/openai-compatible-provider.js",
options: { name: "custom" },
@@ -76,7 +82,9 @@ describe("OpenAICompatiblePlugin", () => {
yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "custom-provider", baseURL: "https://example.com/v1" },
@@ -96,7 +104,9 @@ describe("OpenAICompatiblePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai" },
@@ -67,7 +67,9 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai",
options: { name: "custom-openai", apiKey: "test" },
@@ -84,7 +86,9 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "openai" },
@@ -102,7 +106,9 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -121,7 +127,9 @@ describe("OpenAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-5"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -137,10 +145,12 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "@ai-sdk/openai" },
aisdk: true,
package: "@ai-sdk/openai",
})
catalog.provider.update(item.id, (draft) => {
draft.api = item.api
draft.aisdk = item.aisdk
draft.package = item.package
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
@@ -159,10 +169,12 @@ describe("OpenAIPlugin", () => {
yield* catalog.transform((catalog) => {
const item = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
api: { type: "aisdk", package: "test-provider" },
aisdk: true,
package: "test-provider",
})
catalog.provider.update(item.id, (draft) => {
draft.api = item.api
draft.aisdk = item.aisdk
draft.package = item.package
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
@@ -133,13 +133,12 @@ describe("OpencodePlugin", () => {
expect(provider).toMatchObject({
name: "Remote",
integrationID: "opencode",
api: {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: `${server.url.origin}/v1`,
},
aisdk: true,
package: "@ai-sdk/openai-compatible",
settings: { baseURL: `${server.url.origin}/v1` },
})
expect(provider.request).toEqual({ headers: { "x-org-id": "org" }, body: { custom: "value" } })
expect(provider.headers).toEqual({ "x-org-id": "org" })
expect(provider.settings).toMatchObject({ custom: "value" })
expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))
@@ -150,12 +149,16 @@ describe("OpencodePlugin", () => {
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 },
})
expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 })
expect(model.settings).toEqual({
baseURL: `${server.url.origin}/v1`,
custom: "value",
temperature: 0.5,
})
expect(model.variants).toEqual([
{
id: ModelV2.VariantID.make("high"),
headers: {},
body: { temperature: 0.2 },
settings: { temperature: 0.2 },
},
])
expect(
@@ -175,11 +178,14 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
aisdk: true,
package: "test-provider",
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("paid"),
aisdk: true,
package: "test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -188,7 +194,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
}),
),
@@ -201,11 +207,14 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
aisdk: true,
package: "test-provider",
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
api: { id: ModelV2.ID.make("free"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("free"),
aisdk: true,
package: "test-provider",
cost: cost(0),
})
catalog.provider.update(provider.id, () => {})
@@ -214,7 +223,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
}),
),
@@ -227,11 +236,14 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
aisdk: true,
package: "test-provider",
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
api: { id: ModelV2.ID.make("output-only"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("output-only"),
aisdk: true,
package: "test-provider",
cost: cost(0, 1),
})
catalog.provider.update(provider.id, () => {})
@@ -240,7 +252,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(
true,
)
@@ -255,11 +267,14 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
aisdk: true,
package: "test-provider",
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("paid"),
aisdk: true,
package: "test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -268,7 +283,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
@@ -288,11 +303,14 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
aisdk: true,
package: "test-provider",
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("paid"),
aisdk: true,
package: "test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -301,7 +319,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
@@ -314,26 +332,28 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
api: { type: "aisdk", package: "test-provider" },
request: {
headers: {},
body: { apiKey: "configured" },
},
aisdk: true,
package: "test-provider",
headers: {},
settings: { apiKey: "configured" },
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("paid"),
aisdk: true,
package: "test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, (draft) => {
draft.request = provider.request
draft.headers = provider.headers
draft.settings = { apiKey: "configured" }
})
catalog.model.update(provider.id, model.id, (draft) => {
draft.cost = [...model.cost]
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured")
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("configured")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
@@ -346,11 +366,14 @@ describe("OpencodePlugin", () => {
yield* catalog.transform((catalog) => {
const provider = ProviderV2.Info.make({
...ProviderV2.Info.empty(ProviderV2.ID.openai),
api: { type: "aisdk", package: "test-provider" },
aisdk: true,
package: "test-provider",
})
const model = ModelV2.Info.make({
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("paid"),
aisdk: true,
package: "test-provider",
cost: cost(1),
})
catalog.provider.update(provider.id, () => {})
@@ -359,7 +382,7 @@ describe("OpencodePlugin", () => {
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
@@ -30,19 +30,21 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
provider.request = { headers: { Existing: "value" }, body: {} }
provider.aisdk = true
provider.package = "@openrouter/ai-sdk-provider"
provider.headers = { Existing: "value" }
provider.settings = {}
})
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toBeUndefined()
}),
)
@@ -55,7 +57,9 @@ describe("OpenRouterPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "openrouter" },
@@ -65,7 +69,9 @@ describe("OpenRouterPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("openai/gpt-5"),
aisdk: true,
package: "test-provider",
}),
package: "@openrouter/ai-sdk-provider",
options: { name: "custom" },
@@ -79,7 +85,8 @@ describe("OpenRouterPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
provider.aisdk = true
provider.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,7 +41,9 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("sonar"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/perplexity",
options: { name: "perplexity" },
@@ -58,7 +60,9 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("sonar"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/perplexity-compatible",
options: { name: "perplexity" },
@@ -75,7 +79,9 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("sonar"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/perplexity",
options: { name: "perplexity" },
@@ -92,7 +98,9 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("sonar"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/perplexity",
options: { name: "custom-perplexity" },
@@ -110,7 +118,9 @@ describe("PerplexityPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("sonar"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -49,7 +49,9 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
function model(providerID: string) {
return ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
api: { id: ModelV2.ID.make("sap-model"), type: "aisdk", package: fixtureProvider },
modelID: ModelV2.ID.make("sap-model"),
aisdk: true,
package: fixtureProvider,
})
}
@@ -57,7 +57,9 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("gpt-4"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai",
options: { name: "openai" },
@@ -75,7 +77,9 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -94,7 +98,9 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -117,7 +123,9 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -136,7 +144,9 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: {
@@ -159,7 +169,9 @@ describe("SnowflakeCortexPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
@@ -41,7 +41,9 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/togetherai",
options: { name: "togetherai" },
@@ -59,7 +61,9 @@ describe("TogetherAIPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "file:///tmp/@ai-sdk/togetherai-provider.js",
options: { name: "togetherai" },
@@ -69,7 +73,9 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/togetherai",
options: { name: "togetherai" },
@@ -87,7 +93,9 @@ describe("TogetherAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/togetherai",
options: { name: "custom-togetherai" },
@@ -110,11 +118,9 @@ describe("TogetherAIPlugin", () => {
ProviderV2.ID.make("togetherai"),
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
),
api: {
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
type: "aisdk",
package: "test-provider",
},
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
aisdk: true,
package: "test-provider",
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: {},
@@ -122,7 +128,9 @@ describe("TogetherAIPlugin", () => {
expect(result.language).toBeUndefined()
expect(calls).toEqual([])
expect(result.language ?? fakeSelectorSdk(calls).languageModel(result.model.api.id)).toBeDefined()
expect(
result.language ?? fakeSelectorSdk(calls).languageModel(result.model.modelID ?? result.model.id),
).toBeDefined()
expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"])
}),
)
@@ -41,7 +41,9 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "venice-ai-sdk-provider",
options: { name: "venice" },
@@ -58,7 +60,9 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "venice-ai-sdk-provider",
options: { name: "custom-venice", apiKey: "test" },
@@ -76,7 +80,9 @@ describe("VenicePlugin", () => {
const similar = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "file:///tmp/venice-ai-sdk-provider.js",
options: { name: "venice" },
@@ -84,7 +90,9 @@ describe("VenicePlugin", () => {
const other = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("model"),
aisdk: true,
package: "test-provider",
}),
package: "@ai-sdk/openai-compatible",
options: { name: "venice" },
@@ -103,7 +111,9 @@ describe("VenicePlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" },
modelID: ModelV2.ID.make("alias"),
aisdk: true,
package: "test-provider",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -25,12 +25,13 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
provider.request.headers.Existing = "1"
provider.aisdk = true
provider.package = "@ai-sdk/vercel"
provider.headers = { ...provider.headers, Existing: "1" }
})
})
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).toEqual({
Existing: "1",
"http-referer": "https://opencode.ai/",
"x-title": "opencode",
@@ -43,14 +44,13 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
provider.aisdk = true
provider.package = "@ai-sdk/vercel"
}),
)
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty(
"HTTP-Referer",
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty("X-Title")
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer")
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title")
}),
)
@@ -62,7 +62,9 @@ describe("VercelPlugin", () => {
const event = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" },
modelID: ModelV2.ID.make("v0-1.0-md"),
aisdk: true,
package: "@ai-sdk/vercel",
}),
package: "@ai-sdk/vercel",
options: { name: "custom-vercel" },
@@ -77,7 +79,7 @@ describe("VercelPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {}))
yield* addPlugin()
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.request.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.headers).toBeUndefined()
}),
)
})
+16 -6
View File
@@ -42,7 +42,9 @@ describe("XAIPlugin", () => {
const ignored = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
modelID: ModelV2.ID.make("grok-4"),
aisdk: true,
package: "@ai-sdk/xai",
}),
package: "@ai-sdk/openai-compatible",
options: {},
@@ -51,7 +53,9 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
modelID: ModelV2.ID.make("grok-4"),
aisdk: true,
package: "@ai-sdk/xai",
}),
package: "@ai-sdk/xai",
options: {},
@@ -71,7 +75,9 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runSDK({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
modelID: ModelV2.ID.make("grok-4"),
aisdk: true,
package: "@ai-sdk/xai",
}),
package: "@ai-sdk/xai",
options: {},
@@ -81,7 +87,7 @@ describe("XAIPlugin", () => {
}),
)
it.effect("uses responses with the model api.id for xAI language models", () =>
it.effect("uses responses with the model modelID for xAI language models", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
@@ -91,7 +97,9 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
modelID: ModelV2.ID.make("grok-4"),
aisdk: true,
package: "@ai-sdk/xai",
}),
sdk: fakeSelectorSdk(calls),
options: {},
@@ -112,7 +120,9 @@ describe("XAIPlugin", () => {
const result = yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
modelID: ModelV2.ID.make("grok-4"),
aisdk: true,
package: "@ai-sdk/xai",
}),
sdk: fakeSelectorSdk(calls),
options: {},

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