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
82 changed files with 1514 additions and 2123 deletions
-9
View File
@@ -57,13 +57,6 @@ The bounded projection of a Core-executed tool result persisted in Session histo
**Managed Tool Output File**:
A temporary file created under OpenCode's shared tool-output directory to retain complete output that was too large for Session history.
**Model Request Options**:
Provider-semantic model settings selected from the Catalog and active Session variant before the LLM protocol adapter encodes them for a provider request.
_Avoid_: Request body, wire options
**Generation Controls**:
Provider-neutral sampling and output controls, partitioned from provider semantics and compatibility wire fields when model metadata enters the Catalog.
**PTY Environment**:
The host-supplied environment overlay applied by the server when creating a PTY, observed for the request Location and resolved PTY working directory.
@@ -114,8 +107,6 @@ The host-supplied environment overlay applied by the server when creating a PTY,
- A **Baseline System Context** durably preserves the exact joined text used for the active provider-cache prefix.
- Completed compaction starts a new **Context Epoch** on the next provider attempt, folding the current complete **System Context** into a fresh baseline and removing earlier **Mid-Conversation System Messages** from active model history.
- A model/provider switch preserves the current **Context Epoch** and chronological conversation history; the new selection applies to the next provider turn.
- **Model Request Options** remain provider-semantic through Catalog resolution. The Session runner maps them into the LLM package's provider-option namespace; the selected protocol adapter alone owns provider wire encoding.
- **Generation Controls**, protocol-semantic **Model Request Options**, and compatibility request body fields are separate Catalog domains. A shared ingestion adapter partitions legacy and models.dev AI-SDK-shaped options before routing.
- The **PTY Environment** is a server concern rather than a Core PTY concern. PTY creation merges caller values, then the host overlay, then Core-forced terminal invariants such as `TERM` and `OPENCODE_TERMINAL`.
- A **PTY Environment** adapter observes plugins in the request Location while passing the resolved PTY working directory to the hook; standalone servers use an empty adapter.
- A **Mid-Conversation System Message** lowers to the provider's native chronological instruction role when supported and to a wrapped chronological fallback otherwise.
+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)
+43 -24
View File
@@ -2,7 +2,6 @@ export * as Catalog from "./catalog"
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
import { ModelV2 } from "./model"
import { ModelRequest } from "./model-request"
import { ProviderV2 } from "./provider"
import { EventV2 } from "./event"
import { Policy } from "./policy"
@@ -71,37 +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 = {
...ModelRequest.merge({ ...provider.request, generation: {}, options: {} }, model.request),
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) => {
@@ -119,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)
@@ -142,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)
@@ -288,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 -26
View File
@@ -4,7 +4,6 @@ import { define } from "../../plugin/internal"
import { Effect } from "effect"
import { Config } from "../../config"
import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider"
export const Plugin = define({
@@ -53,21 +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 }
})
const providerApi = catalog.provider.get(providerID)?.provider.api
const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
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 }
const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
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,
@@ -75,30 +75,20 @@ export const Plugin = define({
output: [...config.capabilities.output],
}
}
if (config.request !== undefined) {
ModelRequest.assign(model.request, {
headers: config.request.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, 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: {},
generation: {},
options: {},
}
model.variants.push(existing)
}
ModelRequest.assign(existing, {
headers: variant.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, 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),
}) {}
-102
View File
@@ -1,102 +0,0 @@
export * as ModelRequest from "./model-request"
import { ModelRequest } from "@opencode-ai/schema/model-request"
export const Generation = ModelRequest.Generation
export type Generation = ModelRequest.Generation
export const Request = ModelRequest.Request
export type Request = ModelRequest.Request
interface MutableRequest {
headers: Record<string, string>
body: Record<string, unknown>
generation?: Record<string, unknown>
options?: Record<string, unknown>
}
const generationKeys = new Map<string, keyof Generation>([
["maxOutputTokens", "maxTokens"],
["maxTokens", "maxTokens"],
["temperature", "temperature"],
["topP", "topP"],
["topK", "topK"],
["frequencyPenalty", "frequencyPenalty"],
["presencePenalty", "presencePenalty"],
["seed", "seed"],
["stopSequences", "stop"],
["stop", "stop"],
])
interface Profile {
readonly namespace: string
readonly semantics: ReadonlyMap<string, string>
}
const profiles = new Map<string, Profile>([
[
"@ai-sdk/openai",
{
namespace: "openai",
semantics: new Map([
["store", "store"],
["promptCacheKey", "promptCacheKey"],
["reasoningEffort", "reasoningEffort"],
["reasoningSummary", "reasoningSummary"],
["include", "include"],
["textVerbosity", "textVerbosity"],
["serviceTier", "serviceTier"],
["service_tier", "serviceTier"],
]),
},
],
[
"@ai-sdk/openai-compatible",
{
namespace: "openai",
semantics: new Map([
["store", "store"],
["promptCacheKey", "promptCacheKey"],
["reasoningEffort", "reasoningEffort"],
["reasoning_effort", "reasoningEffort"],
]),
},
],
["@ai-sdk/anthropic", { namespace: "anthropic", semantics: new Map([["thinking", "thinking"]]) }],
])
export const namespace = (packageName: string) => profiles.get(packageName)?.namespace
export const merge = (base: Request, override: Partial<Request>) => ({
headers: { ...base.headers, ...override.headers },
body: { ...base.body, ...override.body },
generation: { ...base.generation, ...override.generation },
options: { ...base.options, ...override.options },
})
export const assign = (target: MutableRequest, override: Partial<Request>) => {
Object.assign(target.headers, override.headers)
Object.assign(target.body, override.body)
Object.assign((target.generation ??= {}), override.generation)
Object.assign((target.options ??= {}), override.options)
}
/** Partitions AI-SDK-shaped request options before they enter the Catalog. */
export function normalizeAiSdkOptions(packageName: string | undefined, input: Readonly<Record<string, unknown>>) {
const generation: Record<string, number | ReadonlyArray<string>> = {}
const options: Record<string, unknown> = {}
const body: Record<string, unknown> = {}
const semantics = profiles.get(packageName ?? "")?.semantics
for (const [key, value] of Object.entries(input)) {
const generationKey = generationKeys.get(key)
if (generationKey === "stop" && Array.isArray(value) && value.every((item) => typeof item === "string"))
generation[generationKey] = value
else if (generationKey !== undefined && generationKey !== "stop" && typeof value === "number")
generation[generationKey] = value
else if (semantics?.has(key)) options[semantics.get(key)!] = value
else body[key] = value
}
return { generation, options, body }
}
+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("/")
+3 -1
View File
@@ -29,6 +29,7 @@ import { CommandPlugin } from "./command"
import { ModelsDevPlugin } from "./models-dev"
import { ProviderPlugins } from "./provider"
import { SkillPlugin } from "./skill"
import { VariantPlugin } from "./variant"
export type Requirements =
| AgentV2.Service
@@ -107,12 +108,13 @@ export const locationLayer = Layer.effectDiscard(
yield* add(CommandPlugin.Plugin)
yield* add(SkillPlugin.Plugin)
yield* add(ModelsDevPlugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin)
for (const item of ProviderPlugins) yield* add(item)
yield* add(ConfigExternalPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin)
yield* add(VariantPlugin.Plugin)
}).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
}),
).pipe(
+13 -35
View File
@@ -2,7 +2,6 @@ import { define } from "./internal"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
import { ModelRequest } from "../model-request"
import { ModelsDev } from "../models-dev"
import { ProviderV2 } from "../provider"
@@ -38,15 +37,12 @@ function cost(input: ModelsDev.Model["cost"]) {
]
}
function variants(model: ModelsDev.Model, packageName?: string) {
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => {
const request = ModelRequest.normalizeAiSdkOptions(packageName, item.provider?.body ?? {})
return {
id: ModelV2.VariantID.make(id),
headers: { ...(item.provider?.headers ?? {}) },
...request,
}
})
function variants(model: ModelsDev.Model) {
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(item.provider?.headers ?? {}) },
body: { ...(item.provider?.body ?? {}) },
}))
}
export const ModelsDevPlugin = define({
@@ -79,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)) {
@@ -97,25 +85,15 @@ 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 ?? [])],
output: [...(model.modalities?.output ?? [])],
}
draft.variants = variants(model, model.provider?.npm ?? item.npm)
draft.variants = variants(model)
draft.time.released = released(model.release_date)
draft.cost = cost(model.cost)
draft.status = model.status ?? "active"
@@ -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)
}),
)
}),
+19 -22
View File
@@ -8,9 +8,9 @@ import { EventV2 } from "../../event"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider"
import { ConfigProviderV1 } from "../../v1/config/provider"
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options"
import { ConfigV1 } from "../../v1/config/config"
const defaultServer = "https://console.opencode.ai"
@@ -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,30 +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
ModelRequest.assign(model.request, {
headers: config.headers,
...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(config.options)),
})
const lowerer = ConfigProviderOptionsV1.get(packageName)
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 ?? {}) },
...ModelRequest.normalizeAiSdkOptions(packageName, withoutCredentials(options)),
settings: lowerer.model(withoutCredentials(options)),
}))
}
if (config.release_date !== undefined) {
@@ -169,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,
}
})
}
}),
+41
View File
@@ -0,0 +1,41 @@
export * as VariantPlugin from "./variant"
import { Effect } from "effect"
import { ModelV2 } from "../model"
import { define } from "./internal"
export const Plugin = define({
id: "variant",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((catalog) => {
for (const record of catalog.provider.list()) {
for (const model of record.models.values()) {
catalog.model.update(model.providerID, model.id, (draft) => {
const generated = generate(draft as ModelV2.Info, record.provider)
if (generated.length === 0) return
const explicit = new Map((draft.variants ?? []).map((variant) => [variant.id, variant]))
const generatedIDs = new Set<string>(generated.map((variant) => variant.id))
draft.variants = [
...generated.map((variant) => explicit.get(variant.id) ?? variant),
...(draft.variants ?? []).filter((variant) => !generatedIDs.has(variant.id)),
]
})
}
}
})
}),
})
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: ModelV2.VariantID.make(id),
settings: { reasoningEffort: id },
}))
}
+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>
+55 -34
View File
@@ -11,7 +11,6 @@ import { Catalog } from "../../catalog"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request"
import { ProviderV2 } from "../../provider"
import { SessionSchema } from "../schema"
@@ -51,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}`
}
}
@@ -68,7 +67,7 @@ export type Error =
| ModelNotSelectedError
| ModelUnavailableError
| VariantUnavailableError
| UnsupportedApiError
| UnsupportedPackageError
| Integration.AuthorizationError
export interface Interface {
@@ -83,23 +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 options = model.request.options ?? {}
const namespace = model.api.type === "aisdk" ? ModelRequest.namespace(model.api.package) : undefined
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,
generation: model.request.generation,
providerOptions: namespace && Object.keys(options).length > 0 ? { [namespace]: options } : undefined,
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 },
})
@@ -109,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({
@@ -122,52 +117,55 @@ const withVariant = (
return Effect.succeed(
variant
? produce(model, (draft) => {
ModelRequest.assign(draft.request, variant)
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"),
}),
)
}
@@ -176,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(
@@ -217,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
}
+12 -31
View File
@@ -6,7 +6,6 @@ import { ConfigMCPV1 } from "./mcp"
import { ConfigPermissionV1 } from "./permission"
import { ConfigProviderV1 } from "./provider"
import { ConfigProviderOptionsV1 } from "./provider-options"
import { ModelRequest } from "../../model-request"
const keys = new Set([
"logLevel",
@@ -174,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)])),
@@ -192,11 +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 ingest = (options: Readonly<Record<string, unknown>>) => {
const request = ModelRequest.normalizeAiSdkOptions(packageID, options)
return { ...lowerer.request(request.body), ...request.generation, ...request.options }
}
const request = info.options && ingest(info.options)
const settings = info.options && lowerer.model(info.options)
const costs = info.cost && [
{
input: info.cost.input,
@@ -219,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: ingest(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])))
}
+33 -52
View File
@@ -65,14 +65,14 @@ describe("CatalogV2", () => {
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
yield* credentials.create({
integrationID,
label: "Second",
value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }),
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
}).pipe(Effect.provide(layer))
})
@@ -135,114 +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
const options = (model.request.options ??= {})
options.shared = "model"
options.model = true
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 })
expect(model.request.options).toEqual({ shared: "model", model: true })
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, reasoningEffort: "high", serviceTier: "priority" },
},
variants: [{ id: "high", body: { reasoningEffort: "high", reasoningSummary: "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" },
})
},
)
})
+32 -31
View File
@@ -45,17 +45,10 @@ 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", () => {
it.effect("partitions existing model variant bodies without changing config shape", () =>
it.effect("keeps configured model variant bodies unchanged", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.opencode
@@ -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: [
@@ -96,8 +91,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
expect(model.variants).toMatchObject([
{
id: "high",
body: {},
options: {
body: {
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
@@ -107,7 +101,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
}),
)
it.effect("uses the effective provider package across layered config", () =>
it.effect("keeps layered model variant bodies unchanged", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.opencode
@@ -120,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" },
},
},
}),
@@ -145,10 +141,9 @@ 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: {},
options: { reasoningEffort: "high" },
body: { reasoningEffort: "high" },
})
}),
)
@@ -171,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",
@@ -180,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",
@@ -199,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",
@@ -249,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" })
}),
),
)
+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 } },
},
},
-44
View File
@@ -1,44 +0,0 @@
import { describe, expect, test } from "bun:test"
import { ModelRequest } from "@opencode-ai/core/model-request"
describe("ModelRequest", () => {
test("partitions AI SDK model and models.dev mode options", () => {
expect(
ModelRequest.normalizeAiSdkOptions("@ai-sdk/openai", {
maxOutputTokens: 4096,
temperature: 0.2,
reasoningEffort: "high",
serviceTier: "priority",
custom_extension: { enabled: true },
}),
).toEqual({
generation: { maxTokens: 4096, temperature: 0.2 },
options: { reasoningEffort: "high", serviceTier: "priority" },
body: { custom_extension: { enabled: true } },
})
})
test("keeps unknown-provider options as compatibility fields", () => {
expect(ModelRequest.normalizeAiSdkOptions(undefined, { temperature: 0.2, reasoningEffort: "high" })).toEqual({
generation: { temperature: 0.2 },
options: {},
body: { reasoningEffort: "high" },
})
})
test("does not consult inherited package-name properties", () => {
expect(ModelRequest.normalizeAiSdkOptions("__proto__", { reasoningEffort: "high" })).toEqual({
generation: {},
options: {},
body: { reasoningEffort: "high" },
})
})
test("normalizes models.dev wire aliases owned by native protocols", () => {
expect(ModelRequest.normalizeAiSdkOptions("@ai-sdk/openai", { service_tier: "priority" })).toEqual({
generation: {},
options: { serviceTier: "priority" },
body: {},
})
})
})
+11 -22
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,39 +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 },
generation: value.request.generation && {
...value.request.generation,
stop: value.request.generation.stop && [...value.request.generation.stop],
},
options: value.request.options && { ...value.request.options },
},
variants: value.variants.map((variant) => ({
variants: value.variants?.map((variant) => ({
...variant,
headers: { ...variant.headers },
body: { ...variant.body },
generation: variant.generation && {
...variant.generation,
stop: variant.generation.stop && [...variant.generation.stop],
},
options: variant.options && { ...variant.options },
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,15 +149,16 @@ describe("OpencodePlugin", () => {
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 },
})
expect(model.request).toMatchObject({ body: { custom: "value" }, generation: { temperature: 0.5 } })
expect(model.request.body).toEqual({ custom: "value" })
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: {},
generation: { temperature: 0.2 },
options: {},
settings: { temperature: 0.2 },
},
])
expect(
@@ -178,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, () => {})
@@ -191,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)
}),
),
@@ -204,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, () => {})
@@ -217,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)
}),
),
@@ -230,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, () => {})
@@ -243,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,
)
@@ -258,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, () => {})
@@ -271,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)
}),
),
@@ -291,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, () => {})
@@ -304,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)
}),
),
@@ -317,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)
}),
),
@@ -349,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, () => {})
@@ -362,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: {},
@@ -32,17 +32,15 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
})
})
yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
expect(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
expect(result.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
expect(Object.keys(required(result.headers)).sort()).toEqual(["HTTP-Referer", "X-Title"])
}),
)
@@ -51,17 +49,15 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
provider.request.headers.Existing = "value"
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
provider.headers = { ...provider.headers, Existing: "value" }
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@@ -74,17 +70,15 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://zenmux.ai/api/v1",
}
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
provider.aisdk = true
provider.package = "@ai-sdk/openai-compatible"
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
@@ -96,12 +90,12 @@ describe("ZenmuxPlugin", () => {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
})
})
yield* addPlugin()
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
+84
View File
@@ -0,0 +1,84 @@
import { describe, expect } from "bun:test"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { EventV2 } from "@opencode-ai/core/event"
import { Integration } from "@opencode-ai/core/integration"
import { Location } from "@opencode-ai/core/location"
import { ModelV2 } from "@opencode-ai/core/model"
import { Policy } from "@opencode-ai/core/policy"
import { VariantPlugin } from "@opencode-ai/core/plugin/variant"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Effect, Layer } from "effect"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { catalogHost, host } from "./host"
const events = EventV2.defaultLayer
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
)
const connections = Credential.defaultLayer.pipe(Layer.fresh)
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
const catalog = Catalog.layer.pipe(
Layer.provide(
Layer.mergeAll(
events,
locationLayer,
Policy.layer.pipe(Layer.provide(locationLayer)),
connections,
integrations,
),
),
)
const it = testEffect(
Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer),
)
describe("VariantPlugin", () => {
it.effect("adds GLM 5.2 variants after catalog sources", () =>
Effect.gen(function* () {
const service = yield* Catalog.Service
yield* service.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.package = "@ai-sdk/openai-compatible"
provider.aisdk = true
})
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
model.modelID = ModelV2.ID.make("glm-5.2")
model.package = "@ai-sdk/openai-compatible"
model.aisdk = true
})
})
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([
expect.objectContaining({ id: "high", settings: { reasoningEffort: "high" } }),
expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
])
}),
)
it.effect("keeps explicit variants over generated defaults", () =>
Effect.gen(function* () {
const service = yield* Catalog.Service
yield* service.transform((catalog) => {
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
model.modelID = ModelV2.ID.make("glm-5.2")
model.package = "@ai-sdk/openai-compatible"
model.aisdk = true
model.variants = [
{ id: ModelV2.VariantID.make("high"), headers: { custom: "true" } },
]
})
})
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([
expect.objectContaining({ id: "high", headers: { custom: "true" } }),
expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }),
])
}),
)
})
+62 -69
View File
@@ -12,28 +12,23 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { it } from "./lib/effect"
type Api =
| {
readonly type: "aisdk"
readonly package: string
readonly url?: string
readonly settings?: Record<string, unknown>
}
| { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
type Transport = {
readonly package: string
readonly aisdk?: true
readonly settings?: Record<string, unknown>
}
const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
const model = (transport: Transport, variants?: NonNullable<ModelV2.Info["variants"]>) =>
ModelV2.Info.make({
id: ModelV2.ID.make("test-model"),
modelID: ModelV2.ID.make("api-test-model"),
providerID: ProviderV2.ID.make("test-provider"),
name: "Test model",
api: { id: ModelV2.ID.make("api-test-model"), ...api },
...transport,
settings: { apiKey: "secret", ...transport.settings },
capabilities: { tools: true, input: ["text"], output: ["text"] },
request: {
headers: { "x-test": "header" },
body: { apiKey: "secret", custom_extension: { enabled: true } },
generation: { temperature: 0.7 },
options: { store: false, serviceTier: "priority" },
},
headers: { "x-test": "header" },
body: { custom_extension: { enabled: true } },
variants,
time: { released: 0 },
cost: [],
@@ -46,7 +41,7 @@ describe("SessionRunnerModel", () => {
it.effect("maps catalog OpenAI AI SDK models into native Responses routes", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
model({ package: "@ai-sdk/openai", aisdk: true, settings: { baseURL: "https://openai.example/v1" } }),
)
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
@@ -56,8 +51,6 @@ describe("SessionRunnerModel", () => {
defaults: {
headers: { "x-test": "header" },
limits: { context: 100, output: 20 },
generation: { temperature: 0.7 },
providerOptions: { openai: { store: false, serviceTier: "priority" } },
http: { body: { custom_extension: { enabled: true } } },
},
})
@@ -67,7 +60,7 @@ describe("SessionRunnerModel", () => {
it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
model({ package: "@ai-sdk/openai", aisdk: true, settings: { baseURL: "https://openai.example/v1" } }),
)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
@@ -81,12 +74,16 @@ describe("SessionRunnerModel", () => {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://compatible.example/v1",
settings: { apiKey: "settings-secret", compatibility: "strict" },
aisdk: true,
settings: {
apiKey: "settings-secret",
baseURL: "https://compatible.example/v1",
compatibility: "strict",
},
}),
request: { headers: {}, body: {}, generation: {}, options: {} },
headers: {},
body: {},
}),
)
const request = LLM.request({ model: resolved, prompt: "Hello" })
@@ -103,21 +100,20 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("lowers selected OpenAI Session variants into Responses options", () =>
it.effect("overlays selected OpenAI Session variant bodies", () =>
Effect.gen(function* () {
const base = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [
const catalog = model({ package: "@ai-sdk/openai", aisdk: true, settings: { baseURL: "https://openai.example/v1" } }, [
{
id: ModelV2.VariantID.make("high"),
headers: { "x-variant": "high" },
body: {},
generation: { temperature: 0.2 },
options: { reasoningEffort: "high" },
body: {
store: false,
service_tier: "priority",
temperature: 0.2,
reasoning: { effort: "high" },
},
},
])
const catalog = ModelV2.Info.make({
...base,
request: { ...base.request, options: { ...base.request.options, reasoningEffort: "medium" } },
})
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant"),
projectID: ProjectV2.ID.global,
@@ -134,31 +130,31 @@ describe("SessionRunnerModel", () => {
})
const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" })
expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } })
expect(prepared.body).toMatchObject({
expect(resolved.route.defaults.http?.body).toEqual({
custom_extension: { enabled: true },
store: false,
service_tier: "priority",
temperature: 0.2,
reasoning: { effort: "high" },
})
expect(prepared.body).not.toHaveProperty("reasoningEffort")
}),
)
it.effect("lowers selected OpenAI-compatible Session variants into Chat options", () =>
it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
Effect.gen(function* () {
const catalog = model(
{ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" },
{
package: "@ai-sdk/openai-compatible",
aisdk: true,
settings: { baseURL: "https://compatible.example/v1" },
},
[
{
id: ModelV2.VariantID.make("high"),
headers: {},
body: {},
generation: {},
options: { reasoningEffort: "high" },
body: { store: false, reasoning_effort: "high" },
},
],
)
@@ -174,20 +170,18 @@ describe("SessionRunnerModel", () => {
})
const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } })
expect(prepared.body).toMatchObject({
expect(resolved.route.defaults.http?.body).toEqual({
custom_extension: { enabled: true },
store: false,
reasoning_effort: "high",
})
expect(prepared.body).not.toHaveProperty("reasoningEffort")
}),
)
it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" })
const catalog = model({ package: "@ai-sdk/openai", aisdk: true, settings: { baseURL: "https://openai.example/v1" } })
const session = SessionV2.Info.make({
id: SessionV2.ID.make("ses_model_variant_unavailable"),
projectID: ProjectV2.ID.global,
@@ -215,15 +209,13 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("lowers selected Anthropic Session variants into Messages options", () =>
it.effect("overlays selected Anthropic Session variant bodies", () =>
Effect.gen(function* () {
const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [
const catalog = model({ package: "@ai-sdk/anthropic", aisdk: true, settings: { baseURL: "https://anthropic.example/v1" } }, [
{
id: ModelV2.VariantID.make("high"),
headers: {},
body: {},
generation: {},
options: { thinking: { type: "enabled", budgetTokens: 12000 } },
body: { thinking: { type: "enabled", budget_tokens: 12000 } },
},
])
const session = SessionV2.Info.make({
@@ -238,20 +230,18 @@ describe("SessionRunnerModel", () => {
})
const resolved = yield* SessionRunnerModel.resolve(session, catalog)
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true } })
expect(prepared.body).toMatchObject({
expect(resolved.route.defaults.http?.body).toEqual({
custom_extension: { enabled: true },
thinking: { type: "enabled", budget_tokens: 12000 },
})
expect(JSON.stringify(prepared.body)).not.toContain("budgetTokens")
}),
)
it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }),
model({ package: "@ai-sdk/anthropic", aisdk: true, settings: { baseURL: "https://anthropic.example/v1" } }),
)
expect(resolved.route).toMatchObject({
@@ -265,8 +255,9 @@ describe("SessionRunnerModel", () => {
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: {}, generation: {}, options: {} },
...model({ package: "@ai-sdk/openai", aisdk: true, settings: { baseURL: "https://openai.example/v1" } }),
headers: {},
body: {},
}),
Credential.Key.make({ type: "key", key: "secret" }),
)
@@ -288,8 +279,10 @@ describe("SessionRunnerModel", () => {
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
const resolved = yield* SessionRunnerModel.fromCatalogModel(
ModelV2.Info.make({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: { apiKey: "configured-secret" }, generation: {}, options: {} },
...model({ package: "@ai-sdk/openai", aisdk: true, settings: { baseURL: "https://openai.example/v1" } }),
settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
headers: {},
body: {},
}),
credential,
)
@@ -302,23 +295,23 @@ describe("SessionRunnerModel", () => {
})
expect(headers.authorization).toBe("Bearer stored-secret")
expect(resolved.route.defaults.http?.body).toEqual({ tenant: "work" })
expect(resolved.route.defaults.http?.body).toEqual({})
}),
)
it.effect("rejects catalog APIs without a native route", () =>
Effect.gen(function* () {
const failure = yield* SessionRunnerModel.fromCatalogModel(
model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
model({ package: "@ai-sdk/google", aisdk: true, settings: { baseURL: "https://google.example/v1" } }),
).pipe(Effect.flip)
expect(failure).toMatchObject({
_tag: "SessionRunnerModel.UnsupportedApiError",
_tag: "SessionRunnerModel.UnsupportedPackageError",
providerID: "test-provider",
modelID: "test-model",
api: "aisdk:@ai-sdk/google",
package: "aisdk:@ai-sdk/google",
})
expect(failure.message).toBe("Unsupported API for test-provider/test-model: aisdk:@ai-sdk/google")
expect(failure.message).toBe("Unsupported package for test-provider/test-model: aisdk:@ai-sdk/google")
}),
)
@@ -326,15 +319,15 @@ describe("SessionRunnerModel", () => {
Effect.sync(() => {
expect(
SessionRunnerModel.supported(
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
model({ package: "@ai-sdk/openai", aisdk: true, settings: { baseURL: "https://openai.example/v1" } }),
),
).toBe(true)
expect(
SessionRunnerModel.supported(
model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
model({ package: "@ai-sdk/google", aisdk: true, settings: { baseURL: "https://google.example/v1" } }),
),
).toBe(false)
expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false)
expect(SessionRunnerModel.supported(model({ package: "native" }))).toBe(false)
}),
)
})
-9
View File
@@ -19,7 +19,6 @@ import { Credential } from "@opencode-ai/schema/credential"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { Integration } from "@opencode-ai/schema/integration"
import { LLM } from "@opencode-ai/schema/llm"
import { ModelRequest } from "@opencode-ai/schema/model-request"
import { Permission } from "@opencode-ai/schema/permission"
import { Reference } from "@opencode-ai/schema/reference"
import { Skill } from "@opencode-ai/schema/skill"
@@ -35,7 +34,6 @@ test("Core reuses the canonical shared schemas", async () => {
coreIntegration,
coreLocation,
coreLLM,
coreModelRequest,
corePermission,
coreProject,
coreReference,
@@ -53,7 +51,6 @@ test("Core reuses the canonical shared schemas", async () => {
import("@opencode-ai/core/integration"),
import("@opencode-ai/core/location"),
import("@opencode-ai/llm"),
import("@opencode-ai/core/model-request"),
import("@opencode-ai/core/permission"),
import("@opencode-ai/core/project/schema"),
import("@opencode-ai/core/reference"),
@@ -103,16 +100,10 @@ test("Core reuses the canonical shared schemas", async () => {
[ModelV2.Family, Model.Family],
[ModelV2.Capabilities, Model.Capabilities],
[ModelV2.Cost, Model.Cost],
[ModelV2.Api, Model.Api],
[ModelV2.Info, Model.Info],
[ProviderV2.ID, Provider.ID],
[ProviderV2.AISDK, Provider.AISDK],
[ProviderV2.Native, Provider.Native],
[ProviderV2.Api, Provider.Api],
[ProviderV2.Request, Provider.Request],
[ProviderV2.Info, Provider.Info],
[coreModelRequest.Generation, ModelRequest.Generation],
[coreModelRequest.Request, ModelRequest.Request],
[corePermission.Effect, Permission.Effect],
[corePermission.Rule, Permission.Rule],
[corePermission.Ruleset, Permission.Ruleset],
-1
View File
@@ -7,7 +7,6 @@ export { Integration } from "./integration"
export { LLM } from "./llm"
export { Location } from "./location"
export { Model } from "./model"
export { ModelRequest } from "./model-request"
export { Permission } from "./permission"
export { Project } from "./project"
export { Provider } from "./provider"
-31
View File
@@ -1,31 +0,0 @@
export * as ModelRequest from "./model-request"
import { Effect, Schema } from "effect"
import { Provider } from "./provider"
export interface Generation extends Schema.Schema.Type<typeof Generation> {}
export const Generation = Schema.Struct({
maxTokens: Schema.Number.pipe(Schema.optional),
temperature: Schema.Number.pipe(Schema.optional),
topP: Schema.Number.pipe(Schema.optional),
topK: Schema.Number.pipe(Schema.optional),
frequencyPenalty: Schema.Number.pipe(Schema.optional),
presencePenalty: Schema.Number.pipe(Schema.optional),
seed: Schema.Number.pipe(Schema.optional),
stop: Schema.String.pipe(Schema.Array, Schema.mutable, Schema.optional),
})
export interface Request extends Schema.Schema.Type<typeof Request> {}
export const Request = Schema.Struct({
...Provider.Request.fields,
generation: Generation.pipe(
Schema.optionalKey,
Schema.withConstructorDefault(Effect.succeed({})),
Schema.withDecodingDefaultKey(Effect.succeed({})),
),
options: Schema.Record(Schema.String, Schema.Any).pipe(
Schema.optionalKey,
Schema.withConstructorDefault(Effect.succeed({})),
Schema.withDecodingDefaultKey(Effect.succeed({})),
),
})
+10 -24
View File
@@ -1,7 +1,6 @@
export * as Model from "./model"
import { Schema } from "effect"
import { ModelRequest } from "./model-request"
import { Provider } from "./provider"
import { withStatics } from "./schema"
@@ -42,34 +41,24 @@ export const Cost = Schema.Struct({
}),
})
export const Api = Schema.Union([
Schema.Struct({
id: ID,
...Provider.AISDK.fields,
}),
Schema.Struct({
id: ID,
...Provider.Native.fields,
}),
]).pipe(Schema.toTaggedUnion("type"))
export type Api = typeof Api.Type
export interface Variant extends Schema.Schema.Type<typeof Variant> {}
export const Variant = Schema.Struct({
id: VariantID,
...Provider.Overlays,
})
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
id: ID,
modelID: ID.pipe(Schema.optional),
providerID: Provider.ID,
family: Family.pipe(Schema.optional),
name: Schema.String,
api: Api,
package: Schema.String.pipe(Schema.optional),
aisdk: Schema.Literal(true).pipe(Schema.optional),
...Provider.Overlays,
capabilities: Capabilities,
request: Schema.Struct({
...ModelRequest.Request.fields,
variant: Schema.String.pipe(Schema.optional),
}),
variants: Schema.Struct({
id: VariantID,
...ModelRequest.Request.fields,
}).pipe(Schema.Array, Schema.mutable),
variants: Variant.pipe(Schema.Array, Schema.mutable, Schema.optional),
time: Schema.Struct({
released: Schema.Finite,
}),
@@ -90,10 +79,7 @@ export const Info = Schema.Struct({
id: modelID,
providerID,
name: modelID,
api: { id: modelID, type: "native", settings: {} },
capabilities: { tools: false, input: [], output: [] },
request: { headers: {}, body: {}, generation: {}, options: {} },
variants: [],
time: { released: 0 },
cost: [],
status: "active",
+8 -20
View File
@@ -22,23 +22,11 @@ export const ID = Schema.String.pipe(
)
export type ID = typeof ID.Type
export interface AISDK extends Schema.Schema.Type<typeof AISDK> {}
export const AISDK = Schema.Struct({
type: Schema.Literal("aisdk"),
package: Schema.String,
url: Schema.String.pipe(Schema.optional),
export const Overlays = {
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
})
export interface Native extends Schema.Schema.Type<typeof Native> {}
export const Native = Schema.Struct({
type: Schema.Literal("native"),
url: Schema.String.pipe(Schema.optional),
settings: Schema.Record(Schema.String, Schema.Unknown),
})
export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
export type Api = typeof Api.Type
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}
export interface Request extends Schema.Schema.Type<typeof Request> {}
export const Request = Schema.Struct({
@@ -52,8 +40,9 @@ export const Info = Schema.Struct({
integrationID: Integration.ID.pipe(Schema.optional),
name: Schema.String,
disabled: Schema.Boolean.pipe(Schema.optional),
api: Api,
request: Request,
package: Schema.String,
aisdk: Schema.Literal(true).pipe(Schema.optional),
...Overlays,
})
.annotate({ identifier: "ProviderV2.Info" })
.pipe(
@@ -62,8 +51,7 @@ export const Info = Schema.Struct({
schema.make({
id,
name: id,
api: { type: "native", settings: {} },
request: { headers: {}, body: {} },
package: "",
}),
})),
)
+27 -78
View File
@@ -3986,73 +3986,35 @@ export type SessionMessage =
export type ModelV2Info = {
id: string
modelID?: string
providerID: string
family?: string
name: string
api:
| {
id: string
type: "aisdk"
package: string
url?: string
settings?: {
[key: string]: unknown
}
}
| {
id: string
type: "native"
url?: string
settings: {
[key: string]: unknown
}
}
package?: string
aisdk?: true
settings?: {
[key: string]: unknown
}
headers?: {
[key: string]: string
}
body?: {
[key: string]: unknown
}
capabilities: {
tools: boolean
input: Array<string>
output: Array<string>
}
request: {
headers: {
[key: string]: string
}
body: {
[key: string]: unknown
}
generation?: {
maxTokens?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
temperature?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
topP?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
topK?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
frequencyPenalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
presencePenalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
seed?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
stop?: Array<string>
}
options?: {
[key: string]: unknown
}
variant?: string
}
variants: Array<{
variants?: Array<{
id: string
headers: {
[key: string]: string
}
body: {
settings?: {
[key: string]: unknown
}
generation?: {
maxTokens?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
temperature?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
topP?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
topK?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
frequencyPenalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
presencePenalty?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
seed?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
stop?: Array<string>
headers?: {
[key: string]: string
}
options?: {
body?: {
[key: string]: unknown
}
}>
@@ -4085,29 +4047,16 @@ export type ProviderV2Info = {
integrationID?: string
name: string
disabled?: boolean
api:
| {
type: "aisdk"
package: string
url?: string
settings?: {
[key: string]: unknown
}
}
| {
type: "native"
url?: string
settings: {
[key: string]: unknown
}
}
request: {
headers: {
[key: string]: string
}
body: {
[key: string]: unknown
}
package: string
aisdk?: true
settings?: {
[key: string]: unknown
}
headers?: {
[key: string]: string
}
body?: {
[key: string]: unknown
}
}
-352
View File
@@ -26833,182 +26833,6 @@
"body": {
"type": "object"
},
"generation": {
"type": "object",
"properties": {
"maxTokens": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"temperature": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"topP": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"topK": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"frequencyPenalty": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"presencePenalty": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"seed": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"stop": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"options": {
"type": "object"
},
"variant": {
"type": "string"
}
@@ -27032,182 +26856,6 @@
},
"body": {
"type": "object"
},
"generation": {
"type": "object",
"properties": {
"maxTokens": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"temperature": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"topP": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"topK": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"frequencyPenalty": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"presencePenalty": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"seed": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": ["NaN"]
},
{
"type": "string",
"enum": ["Infinity"]
},
{
"type": "string",
"enum": ["-Infinity"]
},
{
"type": "string",
"enum": ["Infinity", "-Infinity", "NaN"]
}
]
},
"stop": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"options": {
"type": "object"
}
},
"required": ["id", "headers", "body"],