feat(core): flatten provider config and load native packages (#35563)
Co-authored-by: Dax Raad <d@ironbay.co>
This commit is contained in:
parent
d603eed5a1
commit
e57d9ca390
107 changed files with 2593 additions and 1984 deletions
|
|
@ -84,7 +84,7 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[
|
||||||
cost: defaultCost(model),
|
cost: defaultCost(model),
|
||||||
limit: model.limit,
|
limit: model.limit,
|
||||||
status: model.status,
|
status: model.status,
|
||||||
variants: Object.fromEntries(model.variants.map((variant) => [variant.id, {}])),
|
variants: Object.fromEntries((model.variants ?? []).map((variant) => [variant.id, {}])),
|
||||||
}
|
}
|
||||||
grouped.set(provider.id, provider)
|
grouped.set(provider.id, provider)
|
||||||
}
|
}
|
||||||
|
|
@ -141,10 +141,7 @@ export async function loadRunCommands(sdk: OpenCodeClient, directory: string): P
|
||||||
sdk.command.list(location(directory)),
|
sdk.command.list(location(directory)),
|
||||||
sdk.skill.list(location(directory)),
|
sdk.skill.list(location(directory)),
|
||||||
])
|
])
|
||||||
return [
|
return [...commands.data.map(runCommand), ...skills.data.filter((skill) => skill.slash !== false).map(runSkill)]
|
||||||
...commands.data.map(runCommand),
|
|
||||||
...skills.data.filter((skill) => skill.slash !== false).map(runSkill),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadRunReferences(sdk: OpenCodeClient, directory: string): Promise<RunReference[]> {
|
export async function loadRunReferences(sdk: OpenCodeClient, directory: string): Promise<RunReference[]> {
|
||||||
|
|
|
||||||
|
|
@ -2009,39 +2009,24 @@ export type ModelListOutput = {
|
||||||
}
|
}
|
||||||
readonly data: ReadonlyArray<{
|
readonly data: ReadonlyArray<{
|
||||||
readonly id: string
|
readonly id: string
|
||||||
|
readonly modelID: string
|
||||||
readonly providerID: string
|
readonly providerID: string
|
||||||
readonly family?: string
|
readonly family?: string
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly api:
|
readonly package?: string
|
||||||
| {
|
readonly settings?: { readonly [x: string]: JsonValue }
|
||||||
readonly id: string
|
readonly headers?: { readonly [x: string]: string }
|
||||||
readonly type: "aisdk"
|
readonly body?: { readonly [x: string]: JsonValue }
|
||||||
readonly package: string
|
|
||||||
readonly url?: string
|
|
||||||
readonly settings?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly type: "native"
|
|
||||||
readonly url?: string
|
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
readonly capabilities: {
|
readonly capabilities: {
|
||||||
readonly tools: boolean
|
readonly tools: boolean
|
||||||
readonly input: ReadonlyArray<string>
|
readonly input: ReadonlyArray<string>
|
||||||
readonly output: ReadonlyArray<string>
|
readonly output: ReadonlyArray<string>
|
||||||
}
|
}
|
||||||
readonly request: {
|
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
|
||||||
readonly headers: { readonly [x: string]: string }
|
|
||||||
readonly body: { readonly [x: string]: JsonValue }
|
|
||||||
readonly variant?: string
|
|
||||||
}
|
|
||||||
readonly variants: ReadonlyArray<{
|
readonly variants: ReadonlyArray<{
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
readonly settings?: { readonly [x: string]: JsonValue }
|
||||||
readonly headers: { readonly [x: string]: string }
|
readonly headers?: { readonly [x: string]: string }
|
||||||
readonly body: { readonly [x: string]: JsonValue }
|
readonly body?: { readonly [x: string]: JsonValue }
|
||||||
}>
|
}>
|
||||||
readonly time: { readonly released: number }
|
readonly time: { readonly released: number }
|
||||||
readonly cost: ReadonlyArray<{
|
readonly cost: ReadonlyArray<{
|
||||||
|
|
@ -2070,39 +2055,24 @@ export type ModelDefaultOutput = {
|
||||||
}
|
}
|
||||||
readonly data: {
|
readonly data: {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
|
readonly modelID: string
|
||||||
readonly providerID: string
|
readonly providerID: string
|
||||||
readonly family?: string
|
readonly family?: string
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly api:
|
readonly package?: string
|
||||||
| {
|
readonly settings?: { readonly [x: string]: JsonValue }
|
||||||
readonly id: string
|
readonly headers?: { readonly [x: string]: string }
|
||||||
readonly type: "aisdk"
|
readonly body?: { readonly [x: string]: JsonValue }
|
||||||
readonly package: string
|
|
||||||
readonly url?: string
|
|
||||||
readonly settings?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly id: string
|
|
||||||
readonly type: "native"
|
|
||||||
readonly url?: string
|
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
readonly capabilities: {
|
readonly capabilities: {
|
||||||
readonly tools: boolean
|
readonly tools: boolean
|
||||||
readonly input: ReadonlyArray<string>
|
readonly input: ReadonlyArray<string>
|
||||||
readonly output: ReadonlyArray<string>
|
readonly output: ReadonlyArray<string>
|
||||||
}
|
}
|
||||||
readonly request: {
|
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
|
||||||
readonly headers: { readonly [x: string]: string }
|
|
||||||
readonly body: { readonly [x: string]: JsonValue }
|
|
||||||
readonly variant?: string
|
|
||||||
}
|
|
||||||
readonly variants: ReadonlyArray<{
|
readonly variants: ReadonlyArray<{
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
readonly settings?: { readonly [x: string]: JsonValue }
|
||||||
readonly headers: { readonly [x: string]: string }
|
readonly headers?: { readonly [x: string]: string }
|
||||||
readonly body: { readonly [x: string]: JsonValue }
|
readonly body?: { readonly [x: string]: JsonValue }
|
||||||
}>
|
}>
|
||||||
readonly time: { readonly released: number }
|
readonly time: { readonly released: number }
|
||||||
readonly cost: ReadonlyArray<{
|
readonly cost: ReadonlyArray<{
|
||||||
|
|
@ -2150,19 +2120,10 @@ export type ProviderListOutput = {
|
||||||
readonly integrationID?: string
|
readonly integrationID?: string
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly disabled?: boolean
|
readonly disabled?: boolean
|
||||||
readonly api:
|
readonly package: string
|
||||||
| {
|
readonly settings?: { readonly [x: string]: JsonValue }
|
||||||
readonly type: "aisdk"
|
readonly headers?: { readonly [x: string]: string }
|
||||||
readonly package: string
|
readonly body?: { readonly [x: string]: JsonValue }
|
||||||
readonly url?: string
|
|
||||||
readonly settings?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
|
|
||||||
readonly request: {
|
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
|
||||||
readonly headers: { readonly [x: string]: string }
|
|
||||||
readonly body: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2184,19 +2145,10 @@ export type ProviderGetOutput = {
|
||||||
readonly integrationID?: string
|
readonly integrationID?: string
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly disabled?: boolean
|
readonly disabled?: boolean
|
||||||
readonly api:
|
readonly package: string
|
||||||
| {
|
readonly settings?: { readonly [x: string]: JsonValue }
|
||||||
readonly type: "aisdk"
|
readonly headers?: { readonly [x: string]: string }
|
||||||
readonly package: string
|
readonly body?: { readonly [x: string]: JsonValue }
|
||||||
readonly url?: string
|
|
||||||
readonly settings?: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
|
|
||||||
readonly request: {
|
|
||||||
readonly settings: { readonly [x: string]: JsonValue }
|
|
||||||
readonly headers: { readonly [x: string]: string }
|
|
||||||
readonly body: { readonly [x: string]: JsonValue }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,43 @@
|
||||||
export * as AISDK from "./aisdk"
|
export * as AISDK from "./aisdk"
|
||||||
|
|
||||||
import { makeLocationNode } from "./effect/app-node"
|
import { makeLocationNode } from "./effect/app-node"
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
import type {
|
||||||
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
|
JSONSchema7,
|
||||||
|
JSONValue,
|
||||||
|
LanguageModelV3,
|
||||||
|
LanguageModelV3CallOptions,
|
||||||
|
LanguageModelV3FunctionTool,
|
||||||
|
LanguageModelV3Message,
|
||||||
|
LanguageModelV3Prompt,
|
||||||
|
LanguageModelV3StreamPart,
|
||||||
|
LanguageModelV3ToolChoice,
|
||||||
|
SharedV3ProviderOptions,
|
||||||
|
} from "@ai-sdk/provider"
|
||||||
|
import {
|
||||||
|
FinishReason,
|
||||||
|
InvalidProviderOutputReason,
|
||||||
|
LLMEvent,
|
||||||
|
LLMError,
|
||||||
|
Model,
|
||||||
|
ProviderID,
|
||||||
|
ProviderMetadata,
|
||||||
|
ToolResultValue,
|
||||||
|
UnknownProviderReason,
|
||||||
|
type ContentPart,
|
||||||
|
type LLMRequest,
|
||||||
|
type ToolDefinition,
|
||||||
|
type UsageInput,
|
||||||
|
} from "@opencode-ai/llm"
|
||||||
|
import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/llm/route"
|
||||||
|
import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
|
||||||
type SDK = any
|
type SDK = any
|
||||||
|
type UserContent = Extract<LanguageModelV3Message, { role: "user" }>["content"]
|
||||||
|
type AssistantContent = Extract<LanguageModelV3Message, { role: "assistant" }>["content"]
|
||||||
|
type ToolResultContent = Extract<AssistantContent[number], { type: "tool-result" }>
|
||||||
|
|
||||||
export interface SDKEvent {
|
export interface SDKEvent {
|
||||||
readonly model: ModelV2.Info
|
readonly model: ModelV2.Info
|
||||||
|
|
@ -74,10 +104,10 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
||||||
function prepareOptions(model: ModelV2.Info, pkg: string) {
|
function prepareOptions(model: ModelV2.Info, pkg: string) {
|
||||||
const options: Record<string, any> = {
|
const options: Record<string, any> = {
|
||||||
name: model.providerID,
|
name: model.providerID,
|
||||||
...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}),
|
...(model.settings ?? {}),
|
||||||
...model.request.body,
|
headers: model.headers,
|
||||||
|
body: model.body,
|
||||||
}
|
}
|
||||||
if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
|
|
||||||
|
|
||||||
const customFetch = options.fetch
|
const customFetch = options.fetch
|
||||||
const chunkTimeout = options.chunkTimeout
|
const chunkTimeout = options.chunkTimeout
|
||||||
|
|
@ -110,6 +140,13 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof opts.body === "string" && model.body !== undefined) {
|
||||||
|
const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
|
||||||
|
if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
|
||||||
|
opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
|
const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, {
|
||||||
...opts,
|
...opts,
|
||||||
timeout: false,
|
timeout: false,
|
||||||
|
|
@ -142,17 +179,29 @@ export interface Interface {
|
||||||
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
|
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
|
||||||
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
|
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
|
||||||
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
|
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
|
||||||
|
readonly model: (model: ModelV2.Info) => Effect.Effect<Model, InitError>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
|
||||||
|
|
||||||
const layer = Layer.effect(
|
export const locationLayer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
|
let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
|
||||||
let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
|
let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
|
||||||
const languages = new Map<string, LanguageModelV3>()
|
const languages = new Map<string, LanguageModelV3>()
|
||||||
const sdks = new Map<string, SDK>()
|
const sdks = new Map<string, SDK>()
|
||||||
|
const functionIDs = new WeakMap<object, number>()
|
||||||
|
let nextFunctionID = 0
|
||||||
|
const cacheKey = (input: unknown) =>
|
||||||
|
JSON.stringify(input, (_key, value: unknown) => {
|
||||||
|
if (typeof value !== "function") return value
|
||||||
|
const existing = functionIDs.get(value)
|
||||||
|
if (existing !== undefined) return `function:${existing}`
|
||||||
|
const id = nextFunctionID++
|
||||||
|
functionIDs.set(value, id)
|
||||||
|
return `function:${id}`
|
||||||
|
}) ?? ""
|
||||||
|
|
||||||
const register = <Event>(
|
const register = <Event>(
|
||||||
hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
|
hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
|
||||||
|
|
@ -196,24 +245,36 @@ const layer = Layer.effect(
|
||||||
runSDK: (event) => run(sdkHooks, event),
|
runSDK: (event) => run(sdkHooks, event),
|
||||||
runLanguage: (event) => run(languageHooks, event),
|
runLanguage: (event) => run(languageHooks, event),
|
||||||
language: Effect.fn("AISDK.language")(function* (model) {
|
language: Effect.fn("AISDK.language")(function* (model) {
|
||||||
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
|
const key = cacheKey({
|
||||||
|
providerID: model.providerID,
|
||||||
|
id: model.id,
|
||||||
|
modelID: model.modelID,
|
||||||
|
package: model.package,
|
||||||
|
settings: model.settings,
|
||||||
|
headers: model.headers,
|
||||||
|
body: model.body,
|
||||||
|
limit: model.limit,
|
||||||
|
})
|
||||||
const existing = languages.get(key)
|
const existing = languages.get(key)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
if (model.api.type !== "aisdk")
|
if (!ProviderV2.isAISDK(model.package))
|
||||||
return yield* new InitError({
|
return yield* new InitError({
|
||||||
providerID: model.providerID,
|
providerID: model.providerID,
|
||||||
cause: new Error(`Unsupported api ${model.api.type}`),
|
cause: new Error(`Unsupported package ${model.package}`),
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = prepareOptions(model, model.api.package)
|
const packageName = ProviderV2.packageName(model.package) ?? ""
|
||||||
const sdkKey = JSON.stringify({
|
const options = prepareOptions(model, packageName)
|
||||||
|
const sdkKey = cacheKey({
|
||||||
providerID: model.providerID,
|
providerID: model.providerID,
|
||||||
api: model.api,
|
package: packageName,
|
||||||
options,
|
settings: model.settings,
|
||||||
|
headers: model.headers,
|
||||||
|
body: model.body,
|
||||||
})
|
})
|
||||||
const sdk =
|
const sdk =
|
||||||
sdks.get(sdkKey) ??
|
sdks.get(sdkKey) ??
|
||||||
(yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
|
(yield* service.runSDK({ model, package: packageName, options }).pipe(initError(model.providerID))).sdk
|
||||||
if (!sdk)
|
if (!sdk)
|
||||||
return yield* new InitError({
|
return yield* new InitError({
|
||||||
providerID: model.providerID,
|
providerID: model.providerID,
|
||||||
|
|
@ -221,15 +282,396 @@ const layer = Layer.effect(
|
||||||
})
|
})
|
||||||
sdks.set(sdkKey, sdk)
|
sdks.set(sdkKey, sdk)
|
||||||
const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
|
const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
|
||||||
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
|
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.modelID ?? model.id)).pipe(
|
||||||
initError(model.providerID),
|
initError(model.providerID),
|
||||||
)
|
)
|
||||||
languages.set(key, language)
|
languages.set(key, language)
|
||||||
return language
|
return language
|
||||||
}),
|
}),
|
||||||
|
model: Effect.fn("AISDK.model")(function* (model) {
|
||||||
|
return modelFromLanguage(model, yield* service.language(model))
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return service
|
return service
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
export const defaultLayer = locationLayer
|
||||||
|
|
||||||
|
function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
|
||||||
|
const settings = requestSettings(info.settings)
|
||||||
|
const optionKey = providerOptionKey(ProviderV2.packageName(info.package), info.providerID)
|
||||||
|
const route: AnyRoute = {
|
||||||
|
id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
|
||||||
|
provider: ProviderID.make(info.providerID),
|
||||||
|
protocol: "ai-sdk",
|
||||||
|
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
|
||||||
|
auth: Auth.none,
|
||||||
|
transport: {
|
||||||
|
id: "ai-sdk",
|
||||||
|
prepare: (input) => Effect.succeed(input.body),
|
||||||
|
frames: () => Stream.empty,
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
headers: info.headers,
|
||||||
|
http:
|
||||||
|
info.body === undefined && info.headers === undefined
|
||||||
|
? undefined
|
||||||
|
: { body: info.body === undefined ? undefined : { ...info.body }, headers: info.headers },
|
||||||
|
limits: { context: info.limit.context, output: info.limit.output },
|
||||||
|
providerOptions: settings === undefined ? undefined : { [optionKey]: settings },
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
schema: Schema.Unknown,
|
||||||
|
from: (request) => Effect.succeed(callOptions(request)),
|
||||||
|
},
|
||||||
|
with: () => route,
|
||||||
|
model: (input) => Model.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
|
||||||
|
prepareTransport: (body) => Effect.succeed(body),
|
||||||
|
streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
|
||||||
|
}
|
||||||
|
return Model.make({ id: info.modelID ?? info.id, provider: info.providerID, route })
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerOptionKey(packageName: string | undefined, providerID: ProviderV2.ID) {
|
||||||
|
if (packageName === "@ai-sdk/google") return "google"
|
||||||
|
if (packageName === "@ai-sdk/google-vertex") return "vertex"
|
||||||
|
if (packageName === "@ai-sdk/google-vertex/anthropic") return "anthropic"
|
||||||
|
if (packageName === "@ai-sdk/amazon-bedrock" || packageName === "@ai-sdk/amazon-bedrock/mantle") return "bedrock"
|
||||||
|
if (packageName === "@ai-sdk/azure") return "azure"
|
||||||
|
if (packageName === "@openrouter/ai-sdk-provider") return "openrouter"
|
||||||
|
if (packageName?.startsWith("@ai-sdk/")) return packageName.slice("@ai-sdk/".length)
|
||||||
|
return providerID
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSettings(settings: Readonly<Record<string, unknown>> | undefined) {
|
||||||
|
if (settings === undefined) return undefined
|
||||||
|
const result = Object.fromEntries(
|
||||||
|
Object.entries(settings).filter(
|
||||||
|
([key]) => !["apiKey", "authToken", "baseURL", "chunkTimeout", "fetch", "timeout"].includes(key),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Object.keys(result).length === 0 ? undefined : result
|
||||||
|
}
|
||||||
|
|
||||||
|
function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
|
||||||
|
return {
|
||||||
|
prompt: prompt(request),
|
||||||
|
maxOutputTokens: request.generation?.maxTokens ?? request.model.route.defaults.limits?.output,
|
||||||
|
temperature: request.generation?.temperature,
|
||||||
|
stopSequences: request.generation?.stop === undefined ? undefined : [...request.generation.stop],
|
||||||
|
topP: request.generation?.topP,
|
||||||
|
topK: request.generation?.topK,
|
||||||
|
presencePenalty: request.generation?.presencePenalty,
|
||||||
|
frequencyPenalty: request.generation?.frequencyPenalty,
|
||||||
|
seed: request.generation?.seed,
|
||||||
|
responseFormat: responseFormat(request),
|
||||||
|
tools: request.tools.map(tool),
|
||||||
|
toolChoice: toolChoice(request.toolChoice),
|
||||||
|
headers: request.http?.headers,
|
||||||
|
providerOptions: providerOptions(request.providerOptions),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prompt(request: LLMRequest): LanguageModelV3Prompt {
|
||||||
|
const system = request.system
|
||||||
|
.map((part) => part.text)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n")
|
||||||
|
const messages = request.messages.flatMap(message)
|
||||||
|
if (!system.length) return messages
|
||||||
|
return [{ role: "system", content: system }, ...messages]
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(input: LLMRequest["messages"][number]): LanguageModelV3Message[] {
|
||||||
|
switch (input.role) {
|
||||||
|
case "system":
|
||||||
|
return [{ role: "system", content: input.content.flatMap(text).join("\n\n") }]
|
||||||
|
case "user":
|
||||||
|
return [{ role: "user", content: input.content.flatMap(userPart) }]
|
||||||
|
case "assistant":
|
||||||
|
return [{ role: "assistant", content: input.content.flatMap(assistantPart) }]
|
||||||
|
case "tool": {
|
||||||
|
const content = input.content.flatMap(toolResultPart)
|
||||||
|
return content.length ? [{ role: "tool", content }] : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(part: ContentPart) {
|
||||||
|
return part.type === "text" ? [part.text] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function userPart(part: ContentPart): UserContent {
|
||||||
|
if (part.type === "text") return [{ type: "text", text: part.text }]
|
||||||
|
if (part.type === "media")
|
||||||
|
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function assistantPart(part: ContentPart): AssistantContent {
|
||||||
|
switch (part.type) {
|
||||||
|
case "text":
|
||||||
|
return [{ type: "text", text: part.text }]
|
||||||
|
case "media":
|
||||||
|
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
|
||||||
|
case "reasoning":
|
||||||
|
return [{ type: "reasoning", text: part.text }]
|
||||||
|
case "tool-call":
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: part.id,
|
||||||
|
toolName: part.name,
|
||||||
|
input: part.input,
|
||||||
|
providerExecuted: part.providerExecuted,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
case "tool-result":
|
||||||
|
return toolResultPart(part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolResultPart(part: ContentPart): ToolResultContent[] {
|
||||||
|
if (part.type !== "tool-result") return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "tool-result",
|
||||||
|
toolCallId: part.id,
|
||||||
|
toolName: part.name,
|
||||||
|
output: toolOutput(part.result),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolOutput(result: ToolResultValue) {
|
||||||
|
switch (result.type) {
|
||||||
|
case "text":
|
||||||
|
case "error":
|
||||||
|
return { type: "text" as const, value: messageValue(result.value) }
|
||||||
|
}
|
||||||
|
return { type: "json" as const, value: jsonValue(result.value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function tool(input: ToolDefinition): LanguageModelV3FunctionTool {
|
||||||
|
return {
|
||||||
|
type: "function",
|
||||||
|
name: input.name,
|
||||||
|
description: input.description,
|
||||||
|
inputSchema: input.inputSchema as JSONSchema7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolChoice(input: LLMRequest["toolChoice"]): LanguageModelV3ToolChoice | undefined {
|
||||||
|
if (!input) return undefined
|
||||||
|
if (input.type === "tool") return input.name === undefined ? undefined : { type: "tool", toolName: input.name }
|
||||||
|
return { type: input.type }
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseFormat(request: LLMRequest): LanguageModelV3CallOptions["responseFormat"] {
|
||||||
|
if (request.responseFormat?.type === "json")
|
||||||
|
return { type: "json", schema: request.responseFormat.schema as JSONSchema7 }
|
||||||
|
if (request.responseFormat) return { type: "text" }
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerOptions(input: LLMRequest["providerOptions"]): SharedV3ProviderOptions | undefined {
|
||||||
|
if (!input) return undefined
|
||||||
|
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions) {
|
||||||
|
const state = { step: 0, toolNames: {} as Record<string, string> }
|
||||||
|
return Stream.concat(
|
||||||
|
Stream.make(LLMEvent.stepStart({ index: state.step })),
|
||||||
|
Stream.unwrap(
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: () => language.doStream(options),
|
||||||
|
catch: (error) => llmError("doStream", error),
|
||||||
|
}).pipe(
|
||||||
|
Effect.map((result) =>
|
||||||
|
Stream.fromReadableStream({
|
||||||
|
evaluate: () => result.stream,
|
||||||
|
onError: (error) => llmError("readStream", error),
|
||||||
|
}).pipe(
|
||||||
|
Stream.mapEffect((event) => streamPartEvents(state, event)),
|
||||||
|
Stream.flatMap((events) => Stream.fromIterable(events)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function streamPartEvents(
|
||||||
|
state: { step: number; toolNames: Record<string, string> },
|
||||||
|
event: LanguageModelV3StreamPart,
|
||||||
|
): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> {
|
||||||
|
switch (event.type) {
|
||||||
|
case "stream-start":
|
||||||
|
case "response-metadata":
|
||||||
|
case "raw":
|
||||||
|
case "file":
|
||||||
|
case "source":
|
||||||
|
case "tool-approval-request":
|
||||||
|
return Effect.succeed([])
|
||||||
|
case "text-start":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.textStart({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
|
||||||
|
])
|
||||||
|
case "text-delta":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.textDelta({
|
||||||
|
id: event.id,
|
||||||
|
text: event.delta,
|
||||||
|
providerMetadata: providerMetadata(event.providerMetadata),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
case "text-end":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.textEnd({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
|
||||||
|
])
|
||||||
|
case "reasoning-start":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.reasoningStart({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
|
||||||
|
])
|
||||||
|
case "reasoning-delta":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.reasoningDelta({
|
||||||
|
id: event.id,
|
||||||
|
text: event.delta,
|
||||||
|
providerMetadata: providerMetadata(event.providerMetadata),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
case "reasoning-end":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.reasoningEnd({ id: event.id, providerMetadata: providerMetadata(event.providerMetadata) }),
|
||||||
|
])
|
||||||
|
case "tool-input-start":
|
||||||
|
state.toolNames[event.id] = event.toolName
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.toolInputStart({
|
||||||
|
id: event.id,
|
||||||
|
name: event.toolName,
|
||||||
|
providerMetadata: providerMetadata(event.providerMetadata),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
case "tool-input-delta":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.toolInputDelta({ id: event.id, name: state.toolNames[event.id] ?? "unknown", text: event.delta }),
|
||||||
|
])
|
||||||
|
case "tool-input-end":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.toolInputEnd({
|
||||||
|
id: event.id,
|
||||||
|
name: state.toolNames[event.id] ?? "unknown",
|
||||||
|
providerMetadata: providerMetadata(event.providerMetadata),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
case "tool-call":
|
||||||
|
state.toolNames[event.toolCallId] = event.toolName
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.toolCall({
|
||||||
|
id: event.toolCallId,
|
||||||
|
name: event.toolName,
|
||||||
|
input: parseToolInput(event.input),
|
||||||
|
providerExecuted: event.providerExecuted,
|
||||||
|
providerMetadata: providerMetadata(event.providerMetadata),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
case "tool-result":
|
||||||
|
delete state.toolNames[event.toolCallId]
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.toolResult({
|
||||||
|
id: event.toolCallId,
|
||||||
|
name: event.toolName,
|
||||||
|
result: ToolResultValue.make(event.result, event.isError ? "error" : "json"),
|
||||||
|
providerExecuted: true,
|
||||||
|
providerMetadata: providerMetadata(event.providerMetadata),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
case "finish":
|
||||||
|
return Effect.succeed([
|
||||||
|
LLMEvent.stepFinish({
|
||||||
|
index: state.step++,
|
||||||
|
reason: finishReason(event.finishReason),
|
||||||
|
usage: usage(event.usage),
|
||||||
|
providerMetadata: providerMetadata(event.providerMetadata),
|
||||||
|
}),
|
||||||
|
LLMEvent.finish({
|
||||||
|
reason: finishReason(event.finishReason),
|
||||||
|
usage: usage(event.usage),
|
||||||
|
providerMetadata: providerMetadata(event.providerMetadata),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
case "error":
|
||||||
|
return Effect.fail(llmError("stream", event.error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["usage"]): UsageInput | undefined {
|
||||||
|
const output = {
|
||||||
|
inputTokens: input.inputTokens.total,
|
||||||
|
nonCachedInputTokens: input.inputTokens.noCache,
|
||||||
|
cacheReadInputTokens: input.inputTokens.cacheRead,
|
||||||
|
cacheWriteInputTokens: input.inputTokens.cacheWrite,
|
||||||
|
outputTokens: input.outputTokens.total,
|
||||||
|
reasoningTokens: input.outputTokens.reasoning,
|
||||||
|
totalTokens:
|
||||||
|
input.inputTokens.total === undefined || input.outputTokens.total === undefined
|
||||||
|
? undefined
|
||||||
|
: input.inputTokens.total + input.outputTokens.total,
|
||||||
|
}
|
||||||
|
return Object.values(output).some((value) => value !== undefined) ? output : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishReason(value: unknown): FinishReason {
|
||||||
|
return Schema.is(FinishReason)(value) ? value : "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerMetadata(value: unknown) {
|
||||||
|
return Schema.is(ProviderMetadata)(value) ? value : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseToolInput(value: string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as unknown
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonObject(input: Record<string, unknown>) {
|
||||||
|
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonValue(value)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonValue(input: unknown): JSONValue {
|
||||||
|
try {
|
||||||
|
const encoded = JSON.stringify(input)
|
||||||
|
return encoded === undefined ? null : (JSON.parse(encoded) as JSONValue)
|
||||||
|
} catch {
|
||||||
|
return messageValue(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageValue(input: unknown) {
|
||||||
|
if (typeof input === "string") return input
|
||||||
|
try {
|
||||||
|
return JSON.stringify(input) ?? String(input)
|
||||||
|
} catch {
|
||||||
|
return String(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function llmError(method: string, error: unknown) {
|
||||||
|
const reason =
|
||||||
|
error instanceof LLMError
|
||||||
|
? new InvalidProviderOutputReason({ message: error.message })
|
||||||
|
: new UnknownProviderReason({ message: error instanceof Error ? error.message : String(error) })
|
||||||
|
return new LLMError({
|
||||||
|
module: "AISDK",
|
||||||
|
method,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })
|
||||||
|
|
|
||||||
|
|
@ -66,39 +66,21 @@ const layer = Layer.effect(
|
||||||
|
|
||||||
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
|
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
|
||||||
if (provider.disabled) return false
|
if (provider.disabled) return false
|
||||||
if (typeof provider.request.body.apiKey === "string") return true
|
if (typeof provider.settings?.apiKey === "string") return true
|
||||||
if (integration?.connections.length) return true
|
if (integration?.connections.length) return true
|
||||||
return provider.integrationID === undefined && !integration
|
return provider.integrationID === undefined && !integration
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
|
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
|
||||||
const api =
|
|
||||||
model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0
|
|
||||||
? { ...provider.api, id: model.api.id }
|
|
||||||
: model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
|
|
||||||
? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } }
|
|
||||||
: model.api.type === "aisdk" && provider.api.type === "aisdk"
|
|
||||||
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
|
|
||||||
: model.api
|
|
||||||
const request = {
|
|
||||||
settings: { ...provider.request.settings, ...model.request.settings },
|
|
||||||
headers: { ...provider.request.headers, ...model.request.headers },
|
|
||||||
body: { ...provider.request.body, ...model.request.body },
|
|
||||||
variant: model.request.variant,
|
|
||||||
}
|
|
||||||
return ModelV2.Info.make({
|
return ModelV2.Info.make({
|
||||||
...model,
|
...model,
|
||||||
api,
|
package: model.package ?? provider.package,
|
||||||
request,
|
settings: ProviderV2.mergeOverlay(provider.settings, model.settings),
|
||||||
|
headers: ProviderV2.mergeHeaders(provider.headers, model.headers),
|
||||||
|
body: ProviderV2.mergeOverlay(provider.body, model.body),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => {
|
|
||||||
if (typeof item.request.body.baseURL !== "string") return
|
|
||||||
item.api.url = item.request.body.baseURL
|
|
||||||
delete item.request.body.baseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = State.create<Data, Draft>({
|
const state = State.create<Data, Draft>({
|
||||||
initial: () => ({ providers: new Map() }),
|
initial: () => ({ providers: new Map() }),
|
||||||
draft: (draft) => {
|
draft: (draft) => {
|
||||||
|
|
@ -116,7 +98,6 @@ const layer = Layer.effect(
|
||||||
draft.providers.set(providerID, current)
|
draft.providers.set(providerID, current)
|
||||||
}
|
}
|
||||||
fn(current.provider)
|
fn(current.provider)
|
||||||
normalizeApi(current.provider)
|
|
||||||
},
|
},
|
||||||
remove: (providerID) => {
|
remove: (providerID) => {
|
||||||
draft.providers.delete(providerID)
|
draft.providers.delete(providerID)
|
||||||
|
|
@ -139,7 +120,6 @@ const layer = Layer.effect(
|
||||||
fn(model)
|
fn(model)
|
||||||
model.id = modelID
|
model.id = modelID
|
||||||
model.providerID = providerID
|
model.providerID = providerID
|
||||||
normalizeApi(model)
|
|
||||||
},
|
},
|
||||||
remove: (providerID, modelID) => {
|
remove: (providerID, modelID) => {
|
||||||
draft.providers.get(providerID)?.models.delete(modelID)
|
draft.providers.get(providerID)?.models.delete(modelID)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { ConfigCommand } from "./config/command"
|
||||||
import { ConfigFormatter } from "./config/formatter"
|
import { ConfigFormatter } from "./config/formatter"
|
||||||
import { ConfigLSP } from "./config/lsp"
|
import { ConfigLSP } from "./config/lsp"
|
||||||
import { ConfigMCP } from "./config/mcp"
|
import { ConfigMCP } from "./config/mcp"
|
||||||
|
import { ConfigModel } from "./config/model"
|
||||||
import { ConfigPlugin } from "./config/plugin"
|
import { ConfigPlugin } from "./config/plugin"
|
||||||
import { ConfigProvider } from "./config/provider"
|
import { ConfigProvider } from "./config/provider"
|
||||||
import { ConfigReference } from "./config/reference"
|
import { ConfigReference } from "./config/reference"
|
||||||
|
|
@ -35,7 +36,7 @@ export class Info extends Schema.Class<Info>("Config.Info")({
|
||||||
shell: Schema.String.pipe(Schema.optional).annotate({
|
shell: Schema.String.pipe(Schema.optional).annotate({
|
||||||
description: "Default shell to use for terminal and shell tool execution",
|
description: "Default shell to use for terminal and shell tool execution",
|
||||||
}),
|
}),
|
||||||
model: Schema.String.pipe(Schema.optional).annotate({
|
model: ConfigModel.Selection.pipe(Schema.optional).annotate({
|
||||||
description: "Default model to use when no session or agent model is selected",
|
description: "Default model to use when no session or agent model is selected",
|
||||||
}),
|
}),
|
||||||
default_agent: Schema.String.pipe(Schema.optional).annotate({
|
default_agent: Schema.String.pipe(Schema.optional).annotate({
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export * as ConfigAgent from "./agent"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { Permission } from "@opencode-ai/schema/permission"
|
import { Permission } from "@opencode-ai/schema/permission"
|
||||||
import { ConfigProvider } from "./provider"
|
import { ConfigProvider } from "./provider"
|
||||||
|
import { ConfigModel } from "./model"
|
||||||
import { PositiveInt } from "../schema"
|
import { PositiveInt } from "../schema"
|
||||||
|
|
||||||
export const Color = Schema.Union([
|
export const Color = Schema.Union([
|
||||||
|
|
@ -11,8 +12,7 @@ export const Color = Schema.Union([
|
||||||
])
|
])
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
|
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
|
||||||
model: Schema.String.pipe(Schema.optional),
|
model: ConfigModel.Selection.pipe(Schema.optional),
|
||||||
variant: Schema.String.pipe(Schema.optional),
|
|
||||||
request: ConfigProvider.Request.pipe(Schema.optional),
|
request: ConfigProvider.Request.pipe(Schema.optional),
|
||||||
system: Schema.String.pipe(Schema.optional),
|
system: Schema.String.pipe(Schema.optional),
|
||||||
description: Schema.String.pipe(Schema.optional),
|
description: Schema.String.pipe(Schema.optional),
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
export * as ConfigCommand from "./command"
|
export * as ConfigCommand from "./command"
|
||||||
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
|
import { ConfigModel } from "./model"
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("ConfigV2.Command")({
|
export class Info extends Schema.Class<Info>("ConfigV2.Command")({
|
||||||
template: Schema.String,
|
template: Schema.String,
|
||||||
description: Schema.String.pipe(Schema.optional),
|
description: Schema.String.pipe(Schema.optional),
|
||||||
agent: Schema.String.pipe(Schema.optional),
|
agent: Schema.String.pipe(Schema.optional),
|
||||||
model: Schema.String.pipe(Schema.optional),
|
model: ConfigModel.Selection.pipe(Schema.optional),
|
||||||
variant: Schema.String.pipe(Schema.optional),
|
|
||||||
subtask: Schema.Boolean.pipe(Schema.optional),
|
subtask: Schema.Boolean.pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
|
||||||
38
packages/core/src/config/model.ts
Normal file
38
packages/core/src/config/model.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
export * as ConfigModel from "./model"
|
||||||
|
|
||||||
|
import { Schema, SchemaGetter } from "effect"
|
||||||
|
import { Model } from "@opencode-ai/schema/model"
|
||||||
|
import { Provider } from "@opencode-ai/schema/provider"
|
||||||
|
|
||||||
|
const ProviderID = Provider.ID.check(Schema.isPattern(/^[^/#]+$/))
|
||||||
|
const ModelID = Model.ID.check(Schema.isPattern(/^[^#]+$/))
|
||||||
|
const VariantID = Model.VariantID.check(Schema.isPattern(/^[^#]+$/))
|
||||||
|
|
||||||
|
const Explicit = Schema.Struct({
|
||||||
|
providerID: ProviderID,
|
||||||
|
model: ModelID,
|
||||||
|
variant: VariantID.pipe(Schema.optional),
|
||||||
|
})
|
||||||
|
|
||||||
|
const Short = Schema.String.check(Schema.isPattern(/^[^/#]+\/[^#]+(?:#[^#]+)?$/))
|
||||||
|
|
||||||
|
export interface Selection extends Schema.Schema.Type<typeof Explicit> {}
|
||||||
|
export const Selection = Schema.Union([Short, Explicit])
|
||||||
|
.pipe(
|
||||||
|
Schema.decodeTo(Explicit, {
|
||||||
|
decode: SchemaGetter.transform((input) => (typeof input === "string" ? parse(input) : input)),
|
||||||
|
encode: SchemaGetter.passthrough({ strict: false }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.annotate({ identifier: "Config.ModelSelection" })
|
||||||
|
|
||||||
|
function parse(input: string): Selection {
|
||||||
|
const providerEnd = input.indexOf("/")
|
||||||
|
const variantStart = input.lastIndexOf("#")
|
||||||
|
const hasVariant = variantStart > providerEnd
|
||||||
|
return {
|
||||||
|
providerID: Provider.ID.make(input.slice(0, providerEnd)),
|
||||||
|
model: Model.ID.make(input.slice(providerEnd + 1, hasVariant ? variantStart : undefined)),
|
||||||
|
...(hasVariant ? { variant: Model.VariantID.make(input.slice(variantStart + 1)) } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,6 @@ import { Config } from "../../config"
|
||||||
import { ConfigAgent } from "../agent"
|
import { ConfigAgent } from "../agent"
|
||||||
import { ConfigMarkdown } from "../markdown"
|
import { ConfigMarkdown } from "../markdown"
|
||||||
import { FSUtil } from "../../fs-util"
|
import { FSUtil } from "../../fs-util"
|
||||||
import { ModelV2 } from "../../model"
|
|
||||||
import { ConfigAgentV1 } from "../../v1/config/agent"
|
import { ConfigAgentV1 } from "../../v1/config/agent"
|
||||||
import { ConfigMigrateV1 } from "../../v1/config/migrate"
|
import { ConfigMigrateV1 } from "../../v1/config/migrate"
|
||||||
|
|
||||||
|
|
@ -76,13 +75,12 @@ export const Plugin = define({
|
||||||
const exists = draft.get(agentID) !== undefined
|
const exists = draft.get(agentID) !== undefined
|
||||||
draft.update(agentID, (agent) => {
|
draft.update(agentID, (agent) => {
|
||||||
if (!exists) agent.permissions.push(...global)
|
if (!exists) agent.permissions.push(...global)
|
||||||
if (item.model !== undefined) {
|
if (item.model !== undefined)
|
||||||
const model = ModelV2.parse(item.model)
|
agent.model = {
|
||||||
agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
|
id: item.model.model,
|
||||||
}
|
providerID: item.model.providerID,
|
||||||
if (item.variant !== undefined && agent.model !== undefined) {
|
...(item.model.variant === undefined ? {} : { variant: item.model.variant }),
|
||||||
agent.model.variant = ModelV2.VariantID.make(item.variant)
|
}
|
||||||
}
|
|
||||||
if (item.request !== undefined) {
|
if (item.request !== undefined) {
|
||||||
Object.assign(agent.request.headers, item.request.headers ?? {})
|
Object.assign(agent.request.headers, item.request.headers ?? {})
|
||||||
Object.assign(agent.request.body, item.request.body ?? {})
|
Object.assign(agent.request.body, item.request.body ?? {})
|
||||||
|
|
@ -134,14 +132,16 @@ function decode(file: { directory: string; filepath: string; primary: boolean },
|
||||||
.replace(/\.md$/, "")
|
.replace(/\.md$/, "")
|
||||||
const body = markdown.content.trim()
|
const body = markdown.content.trim()
|
||||||
const legacy = Object.keys(markdown.data).some((key) => !agentKeys.has(key))
|
const legacy = Object.keys(markdown.data).some((key) => !agentKeys.has(key))
|
||||||
const agent = Option.getOrUndefined(
|
const agent = legacy
|
||||||
legacy
|
? Option.getOrUndefined(
|
||||||
? Option.map(
|
Option.map(
|
||||||
decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }),
|
decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }),
|
||||||
ConfigMigrateV1.migrateAgent,
|
ConfigMigrateV1.migrateAgent,
|
||||||
)
|
),
|
||||||
: decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }),
|
)
|
||||||
)
|
: Option.getOrUndefined(
|
||||||
|
decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }),
|
||||||
|
)
|
||||||
if (!agent) return
|
if (!agent) return
|
||||||
const info = Option.getOrUndefined(
|
const info = Option.getOrUndefined(
|
||||||
decodeConfig({
|
decodeConfig({
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { Effect, Option, Schema, Stream } from "effect"
|
||||||
import { CommandV2 } from "../../command"
|
import { CommandV2 } from "../../command"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { FSUtil } from "../../fs-util"
|
import { FSUtil } from "../../fs-util"
|
||||||
import { ModelV2 } from "../../model"
|
|
||||||
import { ConfigCommand } from "../command"
|
import { ConfigCommand } from "../command"
|
||||||
import { ConfigMarkdown } from "../markdown"
|
import { ConfigMarkdown } from "../markdown"
|
||||||
|
|
||||||
|
|
@ -35,13 +34,12 @@ export const Plugin = define({
|
||||||
item.template = command.template
|
item.template = command.template
|
||||||
if (command.description !== undefined) item.description = command.description
|
if (command.description !== undefined) item.description = command.description
|
||||||
if (command.agent !== undefined) item.agent = command.agent
|
if (command.agent !== undefined) item.agent = command.agent
|
||||||
if (command.model !== undefined) {
|
if (command.model !== undefined)
|
||||||
const model = ModelV2.parse(command.model)
|
item.model = {
|
||||||
item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
|
id: command.model.model,
|
||||||
}
|
providerID: command.model.providerID,
|
||||||
if (command.variant !== undefined && item.model !== undefined) {
|
...(command.model.variant === undefined ? {} : { variant: command.model.variant }),
|
||||||
item.model.variant = ModelV2.VariantID.make(command.variant)
|
}
|
||||||
}
|
|
||||||
if (command.subtask !== undefined) item.subtask = command.subtask
|
if (command.subtask !== undefined) item.subtask = command.subtask
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "opencode.config.provider",
|
id: "opencode.config.provider",
|
||||||
|
|
@ -39,27 +40,29 @@ export const Plugin = define({
|
||||||
yield* ctx.catalog.transform((catalog) => {
|
yield* ctx.catalog.transform((catalog) => {
|
||||||
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
|
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
|
||||||
const configuredDefault = Config.latest(loaded.entries, "model")
|
const configuredDefault = Config.latest(loaded.entries, "model")
|
||||||
if (configuredDefault !== undefined) {
|
if (configuredDefault !== undefined)
|
||||||
const model = ModelV2.parse(configuredDefault)
|
catalog.model.default.set(configuredDefault.providerID, configuredDefault.model)
|
||||||
catalog.model.default.set(model.providerID, model.modelID)
|
|
||||||
}
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
||||||
const providerID = id
|
const providerID = id
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
if (item.name !== undefined) provider.name = item.name
|
if (item.name !== undefined) provider.name = item.name
|
||||||
if (item.api !== undefined) provider.api = { ...item.api }
|
if (item.package !== undefined) provider.package = item.package
|
||||||
if (item.request !== undefined) {
|
if (item.settings !== undefined)
|
||||||
Object.assign(provider.request.settings, item.request.settings)
|
provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings)
|
||||||
Object.assign(provider.request.headers, item.request.headers)
|
if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers)
|
||||||
Object.assign(provider.request.body, item.request.body)
|
if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||||
catalog.model.update(providerID, id, (model) => {
|
catalog.model.update(providerID, id, (model) => {
|
||||||
if (config.family !== undefined) model.family = config.family
|
if (config.family !== undefined) model.family = config.family
|
||||||
if (config.name !== undefined) model.name = config.name
|
if (config.name !== undefined) model.name = config.name
|
||||||
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
|
if (config.modelID !== undefined) model.modelID = config.modelID
|
||||||
|
if (config.package !== undefined) model.package = config.package
|
||||||
|
if (config.settings !== undefined)
|
||||||
|
model.settings = ProviderV2.mergeOverlay(model.settings, config.settings)
|
||||||
|
if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers)
|
||||||
|
if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body)
|
||||||
if (config.capabilities !== undefined) {
|
if (config.capabilities !== undefined) {
|
||||||
model.capabilities = {
|
model.capabilities = {
|
||||||
tools: config.capabilities.tools,
|
tools: config.capabilities.tools,
|
||||||
|
|
@ -67,27 +70,19 @@ export const Plugin = define({
|
||||||
output: [...config.capabilities.output],
|
output: [...config.capabilities.output],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (config.request !== undefined) {
|
|
||||||
Object.assign(model.request.settings, config.request.settings)
|
|
||||||
Object.assign(model.request.headers, config.request.headers)
|
|
||||||
Object.assign(model.request.body, config.request.body)
|
|
||||||
if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
|
||||||
}
|
|
||||||
if (config.variants !== undefined) {
|
if (config.variants !== undefined) {
|
||||||
|
model.variants ??= []
|
||||||
for (const variant of config.variants) {
|
for (const variant of config.variants) {
|
||||||
let existing = model.variants.find((item) => item.id === variant.id)
|
let existing = model.variants.find((item) => item.id === variant.id)
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
existing = {
|
existing = { id: variant.id }
|
||||||
id: variant.id,
|
|
||||||
settings: {},
|
|
||||||
headers: {},
|
|
||||||
body: {},
|
|
||||||
}
|
|
||||||
model.variants.push(existing)
|
model.variants.push(existing)
|
||||||
}
|
}
|
||||||
Object.assign(existing.settings, variant.settings)
|
if (variant.settings !== undefined)
|
||||||
Object.assign(existing.headers, variant.headers)
|
existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings)
|
||||||
Object.assign(existing.body, variant.body)
|
if (variant.headers !== undefined)
|
||||||
|
existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers)
|
||||||
|
if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (config.cost !== undefined) {
|
if (config.cost !== undefined) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
export * as ConfigProvider from "./provider"
|
export * as ConfigProvider from "./provider"
|
||||||
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { ProviderV2 } from "../provider"
|
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
|
|
||||||
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
|
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
|
||||||
settings: ProviderV2.Settings.pipe(Schema.optional),
|
|
||||||
|
export const Overlays = {
|
||||||
|
settings: JsonRecord.pipe(Schema.optional),
|
||||||
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||||
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
body: JsonRecord.pipe(Schema.optional),
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
|
||||||
|
headers: Overlays.headers,
|
||||||
|
body: Overlays.body,
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
|
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
|
||||||
|
|
@ -31,32 +37,16 @@ class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
|
||||||
output: Schema.Int.pipe(Schema.optional),
|
output: Schema.Int.pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
const ModelApi = Schema.Union([
|
|
||||||
Schema.Struct({
|
|
||||||
id: ModelV2.ID.pipe(Schema.optional),
|
|
||||||
...ProviderV2.AISDK.fields,
|
|
||||||
}),
|
|
||||||
Schema.Struct({
|
|
||||||
id: ModelV2.ID.pipe(Schema.optional),
|
|
||||||
...ProviderV2.Native.fields,
|
|
||||||
}),
|
|
||||||
Schema.Struct({
|
|
||||||
id: ModelV2.ID,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
class Model extends Schema.Class<Model>("ConfigV2.Model")({
|
class Model extends Schema.Class<Model>("ConfigV2.Model")({
|
||||||
|
modelID: ModelV2.ID.pipe(Schema.optional),
|
||||||
family: ModelV2.Family.pipe(Schema.optional),
|
family: ModelV2.Family.pipe(Schema.optional),
|
||||||
name: Schema.String.pipe(Schema.optional),
|
name: Schema.String.pipe(Schema.optional),
|
||||||
api: ModelApi.pipe(Schema.optional),
|
package: Schema.String.pipe(Schema.optional),
|
||||||
|
...Overlays,
|
||||||
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
|
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
|
||||||
request: Schema.Struct({
|
|
||||||
...Request.fields,
|
|
||||||
variant: Schema.String.pipe(Schema.optional),
|
|
||||||
}).pipe(Schema.optional),
|
|
||||||
variants: Schema.Struct({
|
variants: Schema.Struct({
|
||||||
id: ModelV2.VariantID,
|
id: ModelV2.VariantID,
|
||||||
...Request.fields,
|
...Overlays,
|
||||||
}).pipe(Schema.Array, Schema.optional),
|
}).pipe(Schema.Array, Schema.optional),
|
||||||
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
|
cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional),
|
||||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||||
|
|
@ -66,7 +56,7 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
|
||||||
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
|
export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
|
||||||
name: Schema.String.pipe(Schema.optional),
|
name: Schema.String.pipe(Schema.optional),
|
||||||
env: Schema.String.pipe(Schema.Array, Schema.optional),
|
env: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||||
api: ProviderV2.Api.pipe(Schema.optional),
|
package: Schema.String.pipe(Schema.optional),
|
||||||
request: Request.pipe(Schema.optional),
|
...Overlays,
|
||||||
models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
|
models: Schema.Record(Schema.String, Model).pipe(Schema.optional),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Types } from "effect"
|
|
||||||
import { Model } from "@opencode-ai/schema/model"
|
import { Model } from "@opencode-ai/schema/model"
|
||||||
import { ProviderV2 } from "./provider"
|
import { ProviderV2 } from "./provider"
|
||||||
|
import type { DeepMutable } from "./schema"
|
||||||
|
|
||||||
export const ID = Model.ID
|
export const ID = Model.ID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
|
@ -20,20 +20,10 @@ export const Cost = Model.Cost
|
||||||
export const Ref = Model.Ref
|
export const Ref = Model.Ref
|
||||||
export type Ref = typeof Ref.Type
|
export type Ref = typeof Ref.Type
|
||||||
|
|
||||||
export const Api = Model.Api
|
|
||||||
export type Api = Model.Api
|
|
||||||
|
|
||||||
export const Info = Model.Info
|
export const Info = Model.Info
|
||||||
export type Info = Model.Info
|
export type Info = Model.Info
|
||||||
|
|
||||||
export type MutableRequest = ProviderV2.MutableRequest & { variant?: string }
|
export type MutableInfo = DeepMutable<Info>
|
||||||
export type MutableVariant = ProviderV2.MutableRequest & { id: VariantID }
|
|
||||||
|
|
||||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request" | "variants"> & {
|
|
||||||
api: ProviderV2.MutableApi<Api>
|
|
||||||
request: MutableRequest
|
|
||||||
variants: MutableVariant[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
||||||
const [providerID, ...modelID] = input.split("/")
|
const [providerID, ...modelID] = input.split("/")
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"]
|
||||||
|
|
||||||
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||||
|
|
||||||
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): ModelV2Info["variants"] {
|
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelV2Info["variants"]> {
|
||||||
const npm = model.provider?.npm ?? provider.npm
|
const npm = model.provider?.npm ?? provider.npm
|
||||||
const options = model.reasoning_options ?? []
|
const options = model.reasoning_options ?? []
|
||||||
const effort = options.find((option) => option.type === "effort")
|
const effort = options.find((option) => option.type === "effort")
|
||||||
|
|
@ -82,7 +82,7 @@ function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model)
|
||||||
const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
|
const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
|
||||||
if (id === undefined) return []
|
if (id === undefined) return []
|
||||||
const settings = settingsForEffort(npm, id)
|
const settings = settingsForEffort(npm, id)
|
||||||
return settings ? [{ id, settings, headers: {}, body: {} }] : []
|
return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,15 +117,18 @@ function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.
|
||||||
function budgetVariants(
|
function budgetVariants(
|
||||||
npm: string | undefined,
|
npm: string | undefined,
|
||||||
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
|
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
|
||||||
): ModelV2Info["variants"] {
|
): NonNullable<ModelV2Info["variants"]> {
|
||||||
const max = option.max
|
const max = option.max
|
||||||
const high = option.max === undefined ? Math.max(option.min ?? 0, 16_000) : Math.min(Math.max(option.min ?? 0, 16_000), option.max)
|
const high =
|
||||||
|
option.max === undefined
|
||||||
|
? Math.max(option.min ?? 0, 16_000)
|
||||||
|
: Math.min(Math.max(option.min ?? 0, 16_000), option.max)
|
||||||
return [
|
return [
|
||||||
{ id: "high", budget: high },
|
{ id: "high", budget: high },
|
||||||
...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
|
...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
|
||||||
].flatMap((item) => {
|
].flatMap((item) => {
|
||||||
const settings = settingsForBudget(npm, item.budget)
|
const settings = settingsForBudget(npm, item.budget)
|
||||||
return settings ? [{ id: item.id, settings, headers: {}, body: {} }] : []
|
return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,12 +146,13 @@ function modeName(model: ModelsDev.Model, mode: string) {
|
||||||
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
|
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeVariants(model: ModelV2Info, next: ModelV2Info["variants"]) {
|
function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["variants"]>) {
|
||||||
const existing = new Map(model.variants.map((variant) => [variant.id, variant]))
|
const variants = model.variants ?? []
|
||||||
|
const existing = new Map(variants.map((variant) => [variant.id, variant]))
|
||||||
const nextIDs = new Set(next.map((variant) => variant.id))
|
const nextIDs = new Set(next.map((variant) => variant.id))
|
||||||
model.variants = [
|
model.variants = [
|
||||||
...next.map((variant) => existing.get(variant.id) ?? variant),
|
...next.map((variant) => existing.get(variant.id) ?? variant),
|
||||||
...model.variants.filter((variant) => !nextIDs.has(variant.id)),
|
...variants.filter((variant) => !nextIDs.has(variant.id)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,24 +163,14 @@ function applyModel(
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
readonly cost?: ModelV2Info["cost"]
|
readonly cost?: ModelV2Info["cost"]
|
||||||
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
|
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
|
||||||
readonly variants?: ModelV2Info["variants"]
|
readonly variants?: NonNullable<ModelV2Info["variants"]>
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
draft.name = input.name ?? model.name
|
draft.name = input.name ?? model.name
|
||||||
|
draft.modelID = model.id
|
||||||
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
|
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
|
||||||
draft.api = model.provider?.npm
|
draft.package = model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined
|
||||||
? {
|
draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
|
||||||
id: ModelV2.ID.make(model.id),
|
|
||||||
type: "aisdk",
|
|
||||||
package: model.provider.npm,
|
|
||||||
url: model.provider.api,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
id: ModelV2.ID.make(model.id),
|
|
||||||
type: "native",
|
|
||||||
url: model.provider?.api,
|
|
||||||
settings: {},
|
|
||||||
}
|
|
||||||
draft.capabilities = {
|
draft.capabilities = {
|
||||||
tools: model.tool_call,
|
tools: model.tool_call,
|
||||||
input: [...(model.modalities?.input ?? [])],
|
input: [...(model.modalities?.input ?? [])],
|
||||||
|
|
@ -184,7 +178,11 @@ function applyModel(
|
||||||
}
|
}
|
||||||
mergeVariants(draft, input.variants ?? [])
|
mergeVariants(draft, input.variants ?? [])
|
||||||
draft.time.released = released(model.release_date)
|
draft.time.released = released(model.release_date)
|
||||||
draft.cost = input.cost ?? cost(model.cost)
|
draft.cost = (input.cost ?? cost(model.cost)).map((item) => ({
|
||||||
|
...item,
|
||||||
|
tier: item.tier && { ...item.tier },
|
||||||
|
cache: { ...item.cache },
|
||||||
|
}))
|
||||||
draft.status = model.status ?? "active"
|
draft.status = model.status ?? "active"
|
||||||
draft.enabled = true
|
draft.enabled = true
|
||||||
draft.limit = {
|
draft.limit = {
|
||||||
|
|
@ -192,8 +190,8 @@ function applyModel(
|
||||||
input: model.limit.input,
|
input: model.limit.input,
|
||||||
output: model.limit.output,
|
output: model.limit.output,
|
||||||
}
|
}
|
||||||
Object.assign(draft.request.headers, input.request?.headers ?? {})
|
draft.headers = { ...draft.headers, ...input.request?.headers }
|
||||||
Object.assign(draft.request.body, input.request?.body ?? {})
|
draft.body = { ...draft.body, ...input.request?.body }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelsDevPlugin = define({
|
export const ModelsDevPlugin = define({
|
||||||
|
|
@ -222,25 +220,18 @@ export const ModelsDevPlugin = define({
|
||||||
const providerID = ProviderV2.ID.make(item.id)
|
const providerID = ProviderV2.ID.make(item.id)
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.name = item.name
|
provider.name = item.name
|
||||||
provider.api = item.npm
|
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
|
||||||
? {
|
provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
|
||||||
type: "aisdk",
|
|
||||||
package: item.npm,
|
|
||||||
url: item.api,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: "native",
|
|
||||||
url: item.api,
|
|
||||||
settings: {},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const model of Object.values(item.models)) {
|
for (const model of Object.values(item.models)) {
|
||||||
const baseCost = cost(model.cost)
|
const baseCost = cost(model.cost)
|
||||||
const variants = reasoningVariants(item, model)
|
const variants = reasoningVariants(item, model)
|
||||||
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
|
catalog.model.update(providerID, ModelV2.ID.make(model.id), (draft) =>
|
||||||
|
applyModel(draft, model, { cost: baseCost, variants }),
|
||||||
|
)
|
||||||
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
|
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
|
||||||
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
|
catalog.model.update(providerID, ModelV2.ID.make(`${model.id}-${mode}`), (draft) =>
|
||||||
applyModel(draft, model, {
|
applyModel(draft, model, {
|
||||||
name: modeName(model, mode),
|
name: modeName(model, mode),
|
||||||
cost: mergeCost(baseCost, options.cost),
|
cost: mergeCost(baseCost, options.cost),
|
||||||
|
|
|
||||||
|
|
@ -64,15 +64,14 @@ export const AmazonBedrockPlugin = define({
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
if (provider.api.type !== "aisdk") return
|
if (typeof provider.settings?.endpoint !== "string") return
|
||||||
if (typeof provider.request.body.endpoint !== "string") return
|
|
||||||
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
|
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
|
||||||
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
|
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
|
||||||
provider.api.url = provider.request.body.endpoint
|
provider.settings.baseURL = provider.settings.endpoint
|
||||||
delete provider.request.body.endpoint
|
delete provider.settings.endpoint
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -112,12 +111,15 @@ export const AmazonBedrockPlugin = define({
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||||
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
|
if (
|
||||||
evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
|
ProviderV2.isAISDK(evt.model.package) &&
|
||||||
|
ProviderV2.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
|
||||||
|
) {
|
||||||
|
evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
|
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
|
||||||
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
|
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.modelID ?? evt.model.id, region))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const AnthropicPlugin = define({
|
export const AnthropicPlugin = define({
|
||||||
id: "opencode.provider.anthropic",
|
id: "opencode.provider.anthropic",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["anthropic-beta"] =
|
provider.headers = {
|
||||||
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
...provider.headers,
|
||||||
|
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@ export const AzurePlugin = define({
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/azure") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue
|
||||||
const configured = item.provider.request.body.resourceName
|
const configured = item.provider.settings?.resourceName
|
||||||
const resourceName =
|
const resourceName =
|
||||||
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
|
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
|
||||||
if (!resourceName) continue
|
if (!resourceName) continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.body.resourceName = resourceName
|
provider.settings = { ...provider.settings, resourceName }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -33,7 +33,7 @@ export const AzurePlugin = define({
|
||||||
if (
|
if (
|
||||||
!evt.options.resourceName &&
|
!evt.options.resourceName &&
|
||||||
!evt.options.baseURL &&
|
!evt.options.baseURL &&
|
||||||
(evt.model.api.type !== "aisdk" || !evt.model.api.url)
|
(!ProviderV2.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string")
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
|
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
|
||||||
|
|
@ -47,7 +47,11 @@ export const AzurePlugin = define({
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
||||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
evt.language = selectLanguage(
|
||||||
|
evt.sdk,
|
||||||
|
evt.model.modelID ?? evt.model.id,
|
||||||
|
Boolean(evt.options.useCompletionUrls),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
@ -60,18 +64,25 @@ export const AzureCognitiveServicesPlugin = define({
|
||||||
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||||
if (!resourceName) return
|
if (!resourceName) return
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||||
if (!item.provider.id.includes("azure-cognitive-services")) continue
|
if (!item.provider.id.includes("azure-cognitive-services")) continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
|
provider.settings = {
|
||||||
|
...provider.settings,
|
||||||
|
baseURL: `https://${resourceName}.cognitiveservices.azure.com/openai`,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
evt.language = selectLanguage(
|
||||||
|
evt.sdk,
|
||||||
|
evt.model.modelID ?? evt.model.id,
|
||||||
|
Boolean(evt.options.useCompletionUrls),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const CerebrasPlugin = define({
|
export const CerebrasPlugin = define({
|
||||||
id: "opencode.provider.cerebras",
|
id: "opencode.provider.cerebras",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
|
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ export const CloudflareWorkersAIPlugin = define({
|
||||||
const item = evt.provider.get(providerID)
|
const item = evt.provider.get(providerID)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
if (provider.api.type !== "aisdk") return
|
if (!ProviderV2.isAISDK(provider.package)) return
|
||||||
if (provider.api.url) return
|
if (typeof provider.settings?.baseURL === "string") return
|
||||||
const accountId = resolveAccountId(provider.request.body)
|
const accountId = resolveAccountId(provider.settings ?? {})
|
||||||
if (accountId) provider.api.url = workersEndpoint(accountId)
|
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* ctx.aisdk.sdk(
|
yield* ctx.aisdk.sdk(
|
||||||
|
|
@ -25,7 +25,7 @@ export const CloudflareWorkersAIPlugin = define({
|
||||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||||
|
|
||||||
const accountId = resolveAccountId(evt.options)
|
const accountId = resolveAccountId(evt.options)
|
||||||
if (!hasWorkersEndpoint(evt.model.api) && !accountId) return
|
if (!hasWorkersEndpoint(evt.model) && !accountId) return
|
||||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
||||||
evt.sdk = mod.createOpenAICompatible(
|
evt.sdk = mod.createOpenAICompatible(
|
||||||
sdkOptions({
|
sdkOptions({
|
||||||
|
|
@ -38,7 +38,7 @@ export const CloudflareWorkersAIPlugin = define({
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== providerID) return
|
if (evt.model.providerID !== providerID) return
|
||||||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
@ -52,8 +52,11 @@ function workersEndpoint(accountId: string) {
|
||||||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
|
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasWorkersEndpoint(api: ProviderV2.Api) {
|
function hasWorkersEndpoint(model: {
|
||||||
return api.type === "aisdk" && Boolean(api.url)
|
readonly package?: string
|
||||||
|
readonly settings?: Readonly<Record<string, unknown>>
|
||||||
|
}) {
|
||||||
|
return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
function sdkOptions(options: Record<string, any>) {
|
function sdkOptions(options: Record<string, any>) {
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,11 @@ export const GithubCopilotPlugin = define({
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||||
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
||||||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
evt.language = shouldUseResponses(evt.model.api.id)
|
const id = evt.model.modelID ?? evt.model.id
|
||||||
? evt.sdk.responses(evt.model.api.id)
|
evt.language = shouldUseResponses(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
|
||||||
: evt.sdk.chat(evt.model.api.id)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -36,26 +36,24 @@ export const GitLabPlugin = define({
|
||||||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||||
const featureFlags =
|
const featureFlags =
|
||||||
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
|
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
|
||||||
if (evt.model.api.id.startsWith("duo-workflow-")) {
|
const id = evt.model.modelID ?? evt.model.id
|
||||||
|
if (id.startsWith("duo-workflow-")) {
|
||||||
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
|
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
|
||||||
const workflowRef =
|
const workflowRef =
|
||||||
typeof evt.model.request.body.workflowRef === "string" ? evt.model.request.body.workflowRef : undefined
|
typeof evt.model.settings?.workflowRef === "string" ? evt.model.settings.workflowRef : undefined
|
||||||
const workflowDefinition =
|
const workflowDefinition =
|
||||||
typeof evt.model.request.body.workflowDefinition === "string"
|
typeof evt.model.settings?.workflowDefinition === "string"
|
||||||
? evt.model.request.body.workflowDefinition
|
? evt.model.settings.workflowDefinition
|
||||||
: undefined
|
: undefined
|
||||||
const language = evt.sdk.workflowChat(
|
const language = evt.sdk.workflowChat(gitlab.isWorkflowModel(id) ? id : "duo-workflow", {
|
||||||
gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow",
|
featureFlags,
|
||||||
{
|
workflowDefinition,
|
||||||
featureFlags,
|
})
|
||||||
workflowDefinition,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (workflowRef) language.selectedModelRef = workflowRef
|
if (workflowRef) language.selectedModelRef = workflowRef
|
||||||
evt.language = language
|
evt.language = language
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
evt.language = evt.sdk.agenticChat(evt.model.api.id, {
|
evt.language = evt.sdk.agenticChat(id, {
|
||||||
aiGatewayHeaders: evt.options.aiGatewayHeaders,
|
aiGatewayHeaders: evt.options.aiGatewayHeaders,
|
||||||
featureFlags,
|
featureFlags,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -59,25 +59,28 @@ export const GoogleVertexPlugin = define({
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (
|
if (
|
||||||
item.provider.api.package !== "@ai-sdk/google-vertex" &&
|
ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
|
||||||
!(
|
!(
|
||||||
item.provider.id === ProviderV2.ID.googleVertex &&
|
item.provider.id === ProviderV2.ID.googleVertex &&
|
||||||
item.provider.api.package.includes("@ai-sdk/openai-compatible")
|
ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
const project = resolveProject(item.provider.request.body)
|
const project = resolveProject(item.provider.settings ?? {})
|
||||||
const location = String(resolveLocation(item.provider.request.body))
|
const location = String(resolveLocation(item.provider.settings ?? {}))
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
if (project) provider.request.body.project = project
|
provider.settings = {
|
||||||
provider.request.body.location = location
|
...provider.settings,
|
||||||
if (provider.api.type === "aisdk" && provider.api.url) {
|
...(project ? { project } : {}),
|
||||||
provider.api.url = replaceVertexVars(provider.api.url, project, location)
|
location,
|
||||||
}
|
...(typeof provider.settings?.baseURL === "string"
|
||||||
if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) {
|
? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
|
||||||
provider.request.body.fetch = authFetch(provider.request.body.fetch)
|
: {}),
|
||||||
|
...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
|
||||||
|
? { fetch: authFetch(provider.settings?.fetch) }
|
||||||
|
: {}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +107,7 @@ export const GoogleVertexPlugin = define({
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
||||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
@ -115,21 +118,20 @@ export const GoogleVertexAnthropicPlugin = define({
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
|
||||||
const project =
|
const project =
|
||||||
item.provider.request.body.project ??
|
item.provider.settings?.project ??
|
||||||
process.env.GOOGLE_CLOUD_PROJECT ??
|
process.env.GOOGLE_CLOUD_PROJECT ??
|
||||||
process.env.GCP_PROJECT ??
|
process.env.GCP_PROJECT ??
|
||||||
process.env.GCLOUD_PROJECT
|
process.env.GCLOUD_PROJECT
|
||||||
const location =
|
const location =
|
||||||
item.provider.request.body.location ??
|
item.provider.settings?.location ??
|
||||||
process.env.GOOGLE_CLOUD_LOCATION ??
|
process.env.GOOGLE_CLOUD_LOCATION ??
|
||||||
process.env.VERTEX_LOCATION ??
|
process.env.VERTEX_LOCATION ??
|
||||||
"global"
|
"global"
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
if (project) provider.request.body.project = project
|
provider.settings = { ...provider.settings, ...(project ? { project } : {}), location }
|
||||||
provider.request.body.location = location
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -162,7 +164,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const KiloPlugin = define({
|
export const KiloPlugin = define({
|
||||||
id: "opencode.provider.kilo",
|
id: "opencode.provider.kilo",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||||
if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue
|
if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
|
||||||
provider.request.headers["X-Title"] = "opencode"
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Integration } from "../../integration"
|
import { Integration } from "../../integration"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const LLMGatewayPlugin = define({
|
export const LLMGatewayPlugin = define({
|
||||||
id: "opencode.provider.llmgateway",
|
id: "opencode.provider.llmgateway",
|
||||||
|
|
@ -10,14 +11,17 @@ export const LLMGatewayPlugin = define({
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.disabled) continue
|
if (item.provider.disabled) continue
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||||
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
|
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
|
||||||
if (!configured.has(Integration.ID.make(item.provider.id))) continue
|
if (!configured.has(Integration.ID.make(item.provider.id))) continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
provider.headers = {
|
||||||
provider.request.headers["X-Title"] = "opencode"
|
...provider.headers,
|
||||||
provider.request.headers["X-Source"] = "opencode"
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
|
"X-Title": "opencode",
|
||||||
|
"X-Source": "opencode",
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const NvidiaPlugin = define({
|
export const NvidiaPlugin = define({
|
||||||
id: "opencode.provider.nvidia",
|
id: "opencode.provider.nvidia",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||||
if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue
|
if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
provider.headers = {
|
||||||
provider.request.headers["X-Title"] = "opencode"
|
...provider.headers,
|
||||||
provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode"
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
|
"X-Title": "opencode",
|
||||||
|
"X-BILLING-INVOKE-ORIGIN": provider.headers?.["X-BILLING-INVOKE-ORIGIN"] ?? "OpenCode",
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -178,8 +178,8 @@ export const OpenAIPlugin = define({
|
||||||
})
|
})
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") continue
|
||||||
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
|
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
|
||||||
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
|
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
|
||||||
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
|
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
|
||||||
|
|
@ -194,7 +194,7 @@ export const OpenAIPlugin = define({
|
||||||
// ChatGPT-plan tokens only authorize codex-eligible models, and the
|
// ChatGPT-plan tokens only authorize codex-eligible models, and the
|
||||||
// subscription covers usage, so hide the rest and zero the cost.
|
// subscription covers usage, so hide the rest and zero the cost.
|
||||||
evt.model.update(item.provider.id, model.id, (draft) => {
|
evt.model.update(item.provider.id, model.id, (draft) => {
|
||||||
if (!OpenAICodex.eligible(draft.api.id)) {
|
if (!OpenAICodex.eligible(draft.modelID ?? draft.id)) {
|
||||||
draft.enabled = false
|
draft.enabled = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +220,7 @@ export const OpenAIPlugin = define({
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -112,45 +112,43 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.integrationID = Integration.ID.make("opencode")
|
provider.integrationID = Integration.ID.make("opencode")
|
||||||
if (item.name !== undefined) provider.name = item.name
|
if (item.name !== undefined) provider.name = item.name
|
||||||
provider.api = item.npm
|
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
|
||||||
? { type: "aisdk", package: item.npm, url: item.api }
|
provider.settings = {
|
||||||
: { type: "native", url: item.api, settings: {} }
|
...provider.settings,
|
||||||
Object.assign(provider.request.headers, item.options?.headers)
|
...withoutCredentials(item.options),
|
||||||
Object.assign(provider.request.body, withoutCredentials(item.options))
|
...(item.api ? { baseURL: item.api } : {}),
|
||||||
|
}
|
||||||
|
provider.headers = { ...provider.headers, ...item.options?.headers }
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const [modelID, config] of Object.entries(item.models ?? {})) {
|
for (const [modelID, config] of Object.entries(item.models ?? {})) {
|
||||||
catalog.model.update(providerID, modelID, (model) => {
|
catalog.model.update(providerID, modelID, (model) => {
|
||||||
if (config.family !== undefined) model.family = config.family
|
if (config.family !== undefined) model.family = config.family
|
||||||
if (config.name !== undefined) model.name = config.name
|
if (config.name !== undefined) model.name = config.name
|
||||||
if (config.id !== undefined) model.api.id = config.id
|
if (config.id !== undefined) model.modelID = config.id
|
||||||
if (config.provider !== undefined) {
|
if (config.provider !== undefined) {
|
||||||
model.api = config.provider.npm
|
model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined
|
||||||
? {
|
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
|
||||||
id: model.api.id,
|
|
||||||
type: "aisdk",
|
|
||||||
package: config.provider.npm,
|
|
||||||
url: config.provider.api,
|
|
||||||
}
|
|
||||||
: { id: model.api.id, type: "native", url: config.provider.api, settings: {} }
|
|
||||||
}
|
}
|
||||||
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
|
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
|
||||||
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
|
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
|
||||||
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
|
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
|
||||||
const packageName = config.provider?.npm ?? item.npm
|
model.headers = { ...model.headers, ...config.headers }
|
||||||
const lowerer = ConfigProviderOptionsV1.get(packageName)
|
model.settings = { ...model.settings, ...ConfigProviderOptionsV1.model(withoutCredentials(config.options)) }
|
||||||
Object.assign(model.request.headers, config.headers)
|
|
||||||
Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options)))
|
|
||||||
if (config.variants !== undefined) {
|
if (config.variants !== undefined) {
|
||||||
|
model.variants ??= []
|
||||||
for (const [id, options] of Object.entries(config.variants)) {
|
for (const [id, options] of Object.entries(config.variants)) {
|
||||||
const variantID = ModelV2.VariantID.make(id)
|
const variantID = ModelV2.VariantID.make(id)
|
||||||
let existing = model.variants.find((item) => item.id === variantID)
|
let existing = model.variants.find((item) => item.id === variantID)
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
existing = { id: variantID, settings: {}, headers: {}, body: {} }
|
existing = { id: variantID }
|
||||||
model.variants.push(existing)
|
model.variants.push(existing)
|
||||||
}
|
}
|
||||||
Object.assign(existing.headers, options.headers)
|
existing.headers = { ...existing.headers, ...options.headers }
|
||||||
Object.assign(existing.body, lowerer.request(withoutCredentials(options)))
|
existing.settings = {
|
||||||
|
...existing.settings,
|
||||||
|
...ConfigProviderOptionsV1.model(withoutCredentials(options)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (config.release_date !== undefined) {
|
if (config.release_date !== undefined) {
|
||||||
|
|
@ -169,9 +167,9 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
||||||
|
|
||||||
const item = catalog.provider.get(ProviderV2.ID.opencode)
|
const item = catalog.provider.get(ProviderV2.ID.opencode)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey)
|
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
|
||||||
catalog.provider.update(item.provider.id, (provider) => {
|
catalog.provider.update(item.provider.id, (provider) => {
|
||||||
if (!hasKey) provider.request.body.apiKey = "public"
|
if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
|
||||||
})
|
})
|
||||||
if (hasKey) return
|
if (hasKey) return
|
||||||
for (const model of item.models.values()) {
|
for (const model of item.models.values()) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
|
||||||
export const OpenRouterPlugin = define({
|
export const OpenRouterPlugin = define({
|
||||||
|
|
@ -7,11 +8,10 @@ export const OpenRouterPlugin = define({
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
|
||||||
provider.request.headers["X-Title"] = "opencode"
|
|
||||||
})
|
})
|
||||||
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
|
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
|
||||||
if (!item.models.has(modelID)) continue
|
if (!item.models.has(modelID)) continue
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export const SapAICorePlugin = define({
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||||
evt.language = evt.sdk(evt.model.api.id)
|
evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const VercelPlugin = define({
|
export const VercelPlugin = define({
|
||||||
id: "opencode.provider.vercel",
|
id: "opencode.provider.vercel",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/vercel") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["http-referer"] = "https://opencode.ai/"
|
provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
|
||||||
provider.request.headers["x-title"] = "opencode"
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export const XAIPlugin = define({
|
||||||
yield* ctx.aisdk.language(
|
yield* ctx.aisdk.language(
|
||||||
Effect.fn(function* (evt) {
|
Effect.fn(function* (evt) {
|
||||||
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
||||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
export const ZenmuxPlugin = define({
|
export const ZenmuxPlugin = define({
|
||||||
id: "opencode.provider.zenmux",
|
id: "opencode.provider.zenmux",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
yield* ctx.catalog.transform((evt) => {
|
yield* ctx.catalog.transform((evt) => {
|
||||||
for (const item of evt.provider.list()) {
|
for (const item of evt.provider.list()) {
|
||||||
if (item.provider.api.type !== "aisdk") continue
|
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||||
if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue
|
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
|
||||||
evt.provider.update(item.provider.id, (provider) => {
|
evt.provider.update(item.provider.id, (provider) => {
|
||||||
provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/"
|
provider.headers = {
|
||||||
provider.request.headers["X-Title"] ??= "opencode"
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
|
"X-Title": "opencode",
|
||||||
|
...provider.headers,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
export * as VariantPlugin from "./variant"
|
export * as VariantPlugin from "./variant"
|
||||||
|
|
||||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
|
import { ModelV2 } from "../model"
|
||||||
|
import { ProviderV2 } from "../provider"
|
||||||
|
|
||||||
export const Plugin = define({
|
export const Plugin = define({
|
||||||
id: "opencode.variant",
|
id: "opencode.variant",
|
||||||
|
|
@ -11,14 +12,15 @@ export const Plugin = define({
|
||||||
for (const record of catalog.provider.list()) {
|
for (const record of catalog.provider.list()) {
|
||||||
for (const model of record.models.values()) {
|
for (const model of record.models.values()) {
|
||||||
catalog.model.update(model.providerID, model.id, (draft) => {
|
catalog.model.update(model.providerID, model.id, (draft) => {
|
||||||
const generated = generate(draft)
|
const generated = generate(draft, record.provider)
|
||||||
if (generated.length === 0) return
|
if (generated.length === 0) return
|
||||||
|
|
||||||
const explicit = new Map(draft.variants.map((variant) => [variant.id, variant]))
|
const variants = draft.variants ?? []
|
||||||
const generatedIDs = new Set(generated.map((variant) => variant.id))
|
const explicit = new Map(variants.map((variant) => [variant.id, variant]))
|
||||||
|
const generatedIDs = new Set<string>(generated.map((variant) => variant.id))
|
||||||
draft.variants = [
|
draft.variants = [
|
||||||
...generated.map((variant) => explicit.get(variant.id) ?? variant),
|
...generated.map((variant) => explicit.get(variant.id) ?? variant),
|
||||||
...draft.variants.filter((variant) => !generatedIDs.has(variant.id)),
|
...variants.filter((variant) => !generatedIDs.has(variant.id)),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -27,14 +29,16 @@ export const Plugin = define({
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
|
export function generate(
|
||||||
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
|
model: { readonly id: string; readonly modelID?: string; readonly package?: string },
|
||||||
const ids = `${model.id} ${model.api.id}`.toLowerCase()
|
provider?: { readonly package: string },
|
||||||
|
): NonNullable<ModelV2.Info["variants"]> {
|
||||||
|
const packageName = model.package ?? provider?.package
|
||||||
|
if (!ProviderV2.isAISDK(packageName) || ProviderV2.packageName(packageName) !== "@ai-sdk/openai-compatible") return []
|
||||||
|
const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase()
|
||||||
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
|
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
|
||||||
return ["high", "max"].map((id) => ({
|
return ["high", "max"].map((id) => ({
|
||||||
id,
|
id: ModelV2.VariantID.make(id),
|
||||||
settings: { reasoningEffort: id },
|
settings: { reasoningEffort: id },
|
||||||
headers: {},
|
|
||||||
body: {},
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,106 @@
|
||||||
export * as ProviderV2 from "./provider"
|
export * as ProviderV2 from "./provider"
|
||||||
|
|
||||||
import { Types } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
|
import { pathToFileURL } from "url"
|
||||||
import { Provider } from "@opencode-ai/schema/provider"
|
import { Provider } from "@opencode-ai/schema/provider"
|
||||||
|
import type { ProviderPackageDefinition } from "@opencode-ai/llm"
|
||||||
|
import { Npm } from "./npm"
|
||||||
|
import type { DeepMutable } from "./schema"
|
||||||
|
|
||||||
export const ID = Provider.ID
|
export const ID = Provider.ID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
export const AISDK = Provider.AISDK
|
export const AISDK_PREFIX = "aisdk:"
|
||||||
|
export const isAISDK = (value: string | undefined) => value?.startsWith(AISDK_PREFIX) ?? false
|
||||||
|
export const aisdk = (value: string) => (isAISDK(value) ? value : `${AISDK_PREFIX}${value}`)
|
||||||
|
export const packageName = (value: string | undefined) => {
|
||||||
|
if (value === undefined || !isAISDK(value)) return value
|
||||||
|
return value.slice(AISDK_PREFIX.length)
|
||||||
|
}
|
||||||
|
|
||||||
export const Native = Provider.Native
|
type Json = Schema.Schema.Type<typeof Schema.Json>
|
||||||
|
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
|
||||||
|
const decodeJsonRecord = Schema.decodeUnknownSync(JsonRecord)
|
||||||
|
|
||||||
export const Api = Provider.Api
|
export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.LoadError", {
|
||||||
export type Api = Provider.Api
|
package: Schema.String,
|
||||||
export type MutableApi<T extends Api = Api> = T extends Api
|
cause: Schema.Defect(),
|
||||||
? Omit<Types.DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any })
|
}) {}
|
||||||
: never
|
export type ProviderPackage = ProviderPackageDefinition
|
||||||
|
|
||||||
|
const packages = new Map<string, Promise<unknown>>()
|
||||||
|
|
||||||
|
export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
|
||||||
|
const resolved = yield* Effect.sync(() => {
|
||||||
|
if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")) return specifier
|
||||||
|
try {
|
||||||
|
return import.meta.resolve(specifier)
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (resolved) return yield* importPackage(specifier, resolved)
|
||||||
|
if (!npm) {
|
||||||
|
return yield* new LoadError({
|
||||||
|
package: specifier,
|
||||||
|
cause: new Error(`Provider package ${specifier} is not installed`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const parts = specifier.split("/")
|
||||||
|
const root = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? specifier)
|
||||||
|
const installed = yield* npm.add(root).pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
|
||||||
|
const entrypoint = yield* Effect.try({
|
||||||
|
try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
|
||||||
|
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||||
|
})
|
||||||
|
return yield* importPackage(specifier, entrypoint)
|
||||||
|
})
|
||||||
|
|
||||||
|
export function mergeOverlay(
|
||||||
|
base: Readonly<Record<string, unknown>> | undefined,
|
||||||
|
overlay: Readonly<Record<string, unknown>> | undefined,
|
||||||
|
): Record<string, Json> | undefined {
|
||||||
|
if (base === undefined) return overlay && decodeJsonRecord({ ...overlay })
|
||||||
|
if (overlay === undefined) return decodeJsonRecord({ ...base })
|
||||||
|
return decodeJsonRecord(
|
||||||
|
Object.fromEntries(
|
||||||
|
new Set([...Object.keys(base), ...Object.keys(overlay)]).values().map((key): [string, unknown] => {
|
||||||
|
const left = base[key]
|
||||||
|
const right = overlay[key]
|
||||||
|
if (right === undefined) return [key, left]
|
||||||
|
if (
|
||||||
|
typeof left === "object" &&
|
||||||
|
left !== null &&
|
||||||
|
!Array.isArray(left) &&
|
||||||
|
typeof right === "object" &&
|
||||||
|
right !== null &&
|
||||||
|
!Array.isArray(right)
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
key,
|
||||||
|
mergeOverlay(left as Readonly<Record<string, unknown>>, right as Readonly<Record<string, unknown>>) ?? {},
|
||||||
|
]
|
||||||
|
return [key, right]
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeHeaders(
|
||||||
|
base: Readonly<Record<string, string>> | undefined,
|
||||||
|
overlay: Readonly<Record<string, string>> | undefined,
|
||||||
|
) {
|
||||||
|
if (base === undefined) return overlay && { ...overlay }
|
||||||
|
if (overlay === undefined) return { ...base }
|
||||||
|
return Object.fromEntries(
|
||||||
|
[...Object.entries(base), ...Object.entries(overlay)]
|
||||||
|
.reduce((result, entry) => {
|
||||||
|
result.set(entry[0].toLowerCase(), entry)
|
||||||
|
return result
|
||||||
|
}, new Map<string, [string, string]>())
|
||||||
|
.values(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const Request = Provider.Request
|
export const Request = Provider.Request
|
||||||
export type Request = Provider.Request
|
export type Request = Provider.Request
|
||||||
|
|
@ -25,9 +111,24 @@ export type Settings = Provider.Settings
|
||||||
export const Info = Provider.Info
|
export const Info = Provider.Info
|
||||||
export type Info = Provider.Info
|
export type Info = Provider.Info
|
||||||
|
|
||||||
export type MutableRequest = Types.DeepMutable<Request>
|
export type MutableInfo = DeepMutable<Info>
|
||||||
|
|
||||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request"> & {
|
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) {
|
||||||
api: MutableApi
|
const module = yield* Effect.tryPromise({
|
||||||
request: MutableRequest
|
try: () => {
|
||||||
}
|
const existing = packages.get(entrypoint)
|
||||||
|
if (existing) return existing
|
||||||
|
const loaded = import(entrypoint)
|
||||||
|
packages.set(entrypoint, loaded)
|
||||||
|
return loaded
|
||||||
|
},
|
||||||
|
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||||
|
})
|
||||||
|
if (typeof module !== "object" || module === null || typeof (module as { model?: unknown }).model !== "function") {
|
||||||
|
return yield* new LoadError({
|
||||||
|
package: specifier,
|
||||||
|
cause: new Error(`Provider package ${specifier} does not export model(modelID, settings)`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return module as ProviderPackageDefinition
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,10 @@ const layer = Layer.effect(
|
||||||
]
|
]
|
||||||
.filter((part): part is string => part !== undefined && part.length > 0)
|
.filter((part): part is string => part !== undefined && part.length > 0)
|
||||||
.map(SystemPart.make),
|
.map(SystemPart.make),
|
||||||
messages: [...toLLMMessages(context, model), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])],
|
messages: [
|
||||||
|
...toLLMMessages(context, resolved.ref),
|
||||||
|
...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []),
|
||||||
|
],
|
||||||
tools: toolMaterialization?.definitions ?? [],
|
tools: toolMaterialization?.definitions ?? [],
|
||||||
toolChoice: isLastStep ? "none" : undefined,
|
toolChoice: isLastStep ? "none" : undefined,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export * as SessionRunnerModel from "./model"
|
export * as SessionRunnerModel from "./model"
|
||||||
|
|
||||||
import { makeLocationNode } from "../../effect/app-node"
|
import { makeLocationNode } from "../../effect/app-node"
|
||||||
import { type Model } from "@opencode-ai/llm"
|
import { Model } from "@opencode-ai/llm"
|
||||||
// ast-grep-ignore: no-star-import
|
// ast-grep-ignore: no-star-import
|
||||||
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
|
||||||
// ast-grep-ignore: no-star-import
|
// ast-grep-ignore: no-star-import
|
||||||
|
|
@ -11,10 +11,12 @@ import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
|
||||||
import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
|
import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { produce } from "immer"
|
import { produce } from "immer"
|
||||||
|
import { AISDK } from "../../aisdk"
|
||||||
import { Catalog } from "../../catalog"
|
import { Catalog } from "../../catalog"
|
||||||
import { Credential } from "../../credential"
|
import { Credential } from "../../credential"
|
||||||
import { Integration } from "../../integration"
|
import { Integration } from "../../integration"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
import { Npm } from "../../npm"
|
||||||
import { OpenAICodex } from "../../plugin/provider/openai-codex"
|
import { OpenAICodex } from "../../plugin/provider/openai-codex"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
import { SessionSchema } from "../schema"
|
import { SessionSchema } from "../schema"
|
||||||
|
|
@ -55,16 +57,16 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()(
|
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
|
||||||
"SessionRunnerModel.UnsupportedApiError",
|
"SessionRunnerModel.UnsupportedPackageError",
|
||||||
{
|
{
|
||||||
providerID: ProviderV2.ID,
|
providerID: ProviderV2.ID,
|
||||||
modelID: ModelV2.ID,
|
modelID: ModelV2.ID,
|
||||||
api: Schema.String,
|
package: Schema.String,
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
override get message() {
|
override get message() {
|
||||||
return `Unsupported API for ${this.providerID}/${this.modelID}: ${this.api}`
|
return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +74,7 @@ export type Error =
|
||||||
| ModelNotSelectedError
|
| ModelNotSelectedError
|
||||||
| ModelUnavailableError
|
| ModelUnavailableError
|
||||||
| VariantUnavailableError
|
| VariantUnavailableError
|
||||||
| UnsupportedApiError
|
| UnsupportedPackageError
|
||||||
| Integration.AuthorizationError
|
| Integration.AuthorizationError
|
||||||
|
|
||||||
export interface Resolved {
|
export interface Resolved {
|
||||||
|
|
@ -104,41 +106,50 @@ export const resolved = (model: Model, variant?: ModelV2.VariantID): Resolved =>
|
||||||
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
||||||
if (credential?.type === "key") return Auth.value(credential.key)
|
if (credential?.type === "key") return Auth.value(credential.key)
|
||||||
if (credential?.type === "oauth") return Auth.value(credential.access)
|
if (credential?.type === "oauth") return Auth.value(credential.access)
|
||||||
const value = model.request.body.apiKey ?? model.api.settings?.apiKey
|
const value = model.settings?.apiKey
|
||||||
if (typeof value === "string") return Auth.value(value)
|
if (typeof value === "string") return Auth.value(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
|
const withDefaults = (model: ModelV2.Info, route: AnyRoute) =>
|
||||||
const body = model.request.body
|
route.with({
|
||||||
const httpBody = Object.hasOwn(body, "apiKey")
|
|
||||||
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
|
|
||||||
: body
|
|
||||||
return route.with({
|
|
||||||
provider: model.providerID,
|
provider: model.providerID,
|
||||||
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
|
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
|
||||||
headers: model.request.headers,
|
headers: providerHeaders(model),
|
||||||
providerOptions: providerOptions(model),
|
providerOptions: providerOptions(model),
|
||||||
http: { body: httpBody },
|
http: model.body === undefined ? undefined : { body: model.body },
|
||||||
limits: { context: model.limit.context, output: model.limit.output },
|
limits: { context: model.limit.context, output: model.limit.output },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const providerHeaders = (model: ModelV2.Info) => {
|
||||||
|
const packageName = ProviderV2.packageName(model.package)
|
||||||
|
const generated = new Map<string, string>()
|
||||||
|
if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string")
|
||||||
|
generated.set("OpenAI-Organization", model.settings.organization)
|
||||||
|
if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string")
|
||||||
|
generated.set("OpenAI-Project", model.settings.project)
|
||||||
|
if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string")
|
||||||
|
generated.set("Authorization", `Bearer ${model.settings.authToken}`)
|
||||||
|
return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerOptions = (
|
const providerOptions = (
|
||||||
model: ModelV2.Info,
|
model: ModelV2.Info,
|
||||||
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
|
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
|
||||||
if (Object.keys(model.request.settings).length === 0) return undefined
|
if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined
|
||||||
if (model.api.type !== "aisdk") return undefined
|
const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings
|
||||||
if (model.api.package === "@ai-sdk/openai") return { openai: model.request.settings }
|
if (Object.keys(settings).length === 0) return undefined
|
||||||
if (model.api.package === "@ai-sdk/anthropic") return { anthropic: model.request.settings }
|
const packageName = ProviderV2.packageName(model.package)
|
||||||
if (model.api.package === "@ai-sdk/openai-compatible") return { openai: model.request.settings }
|
if (packageName === "@ai-sdk/openai") return { openai: settings }
|
||||||
|
if (packageName === "@ai-sdk/anthropic") return { anthropic: settings }
|
||||||
|
if (packageName === "@ai-sdk/openai-compatible") return { openai: settings }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const withVariant = (
|
export const withVariant = (
|
||||||
model: ModelV2.Info,
|
model: ModelV2.Info,
|
||||||
variantID: ModelV2.VariantID | undefined,
|
variantID: ModelV2.VariantID | undefined,
|
||||||
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
|
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
|
||||||
const id = variantID === "default" || variantID === undefined ? model.request.variant : variantID
|
const id = variantID === "default" ? undefined : variantID
|
||||||
const variant = model.variants.find((item) => item.id === id)
|
const variant = model.variants?.find((item) => item.id === id)
|
||||||
if (!variant && variantID !== undefined && variantID !== "default")
|
if (!variant && variantID !== undefined && variantID !== "default")
|
||||||
return Effect.fail(
|
return Effect.fail(
|
||||||
new VariantUnavailableError({
|
new VariantUnavailableError({
|
||||||
|
|
@ -150,81 +161,140 @@ export const withVariant = (
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
variant
|
variant
|
||||||
? produce(model, (draft) => {
|
? produce(model, (draft) => {
|
||||||
Object.assign(draft.request.settings, variant.settings)
|
draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings)
|
||||||
Object.assign(draft.request.headers, variant.headers)
|
draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers)
|
||||||
Object.assign(draft.request.body, variant.body)
|
draft.body = ProviderV2.mergeOverlay(draft.body, variant.body)
|
||||||
})
|
})
|
||||||
: model,
|
: model,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiName = (model: ModelV2.Info) =>
|
export interface Dependencies {
|
||||||
model.api.type === "aisdk" ? `${model.api.type}:${model.api.package}` : model.api.type
|
readonly loadPackage?: (specifier: string) => Effect.Effect<ProviderV2.ProviderPackage, ProviderV2.LoadError>
|
||||||
|
readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect<Model, AISDK.InitError>
|
||||||
|
}
|
||||||
|
|
||||||
export const fromCatalogModel = (
|
export const fromCatalogModel = (
|
||||||
model: ModelV2.Info,
|
model: ModelV2.Info,
|
||||||
credential?: Credential.Value,
|
credential?: Credential.Value,
|
||||||
): Effect.Effect<Model, UnsupportedApiError> => {
|
dependencies: Dependencies = {},
|
||||||
|
): Effect.Effect<Model, UnsupportedPackageError> => {
|
||||||
const resolved =
|
const resolved =
|
||||||
credential?.type !== "key" || credential.metadata === undefined
|
credential?.type !== "key" || credential.metadata === undefined
|
||||||
? model
|
? model
|
||||||
: produce(model, (draft) => {
|
: produce(model, (draft) => {
|
||||||
Object.assign(draft.request.body, credential.metadata)
|
draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata)
|
||||||
})
|
})
|
||||||
|
const packageName = ProviderV2.packageName(resolved.package)
|
||||||
const key = apiKey(resolved, credential)
|
const key = apiKey(resolved, credential)
|
||||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
|
|
||||||
// ChatGPT-plan OAuth tokens are not API-key credentials: the public API rejects
|
if (
|
||||||
// them, so requests must target the codex backend with the account header.
|
OpenAICodex.isChatGPT(credential) &&
|
||||||
if (OpenAICodex.isChatGPT(credential)) {
|
!ProviderV2.isAISDK(resolved.package) &&
|
||||||
const account = OpenAICodex.accountID(credential)
|
isNativeOpenAI(resolved.package)
|
||||||
return Effect.succeed(
|
) {
|
||||||
withDefaults(resolved, OpenAIResponses.route)
|
return Effect.succeed(codexModel(resolved, credential, key))
|
||||||
.with({
|
}
|
||||||
endpoint: { baseURL: OpenAICodex.baseURL },
|
|
||||||
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen(
|
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
|
||||||
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }),
|
if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key))
|
||||||
),
|
|
||||||
})
|
|
||||||
.model({ id: resolved.api.id }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
withDefaults(resolved, OpenAIResponses.route)
|
withDefaults(resolved, OpenAIResponses.route)
|
||||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||||
.model({ id: resolved.api.id }),
|
.model({ id: resolved.modelID ?? resolved.id }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/anthropic") {
|
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
withDefaults(resolved, AnthropicMessages.route)
|
withDefaults(resolved, AnthropicMessages.route)
|
||||||
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
|
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
|
||||||
.model({ id: resolved.api.id }),
|
.model({ id: resolved.modelID ?? resolved.id }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai-compatible" && resolved.api.url) {
|
if (
|
||||||
|
ProviderV2.isAISDK(resolved.package) &&
|
||||||
|
packageName === "@ai-sdk/openai-compatible" &&
|
||||||
|
typeof resolved.settings?.baseURL === "string"
|
||||||
|
) {
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
withDefaults(resolved, OpenAICompatibleChat.route)
|
withDefaults(resolved, OpenAICompatibleChat.route)
|
||||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||||
.model({ id: resolved.api.id }),
|
.model({ id: resolved.modelID ?? resolved.id }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return Effect.fail(
|
if (ProviderV2.isAISDK(resolved.package)) {
|
||||||
new UnsupportedApiError({
|
if (!dependencies.loadAISDK) return Effect.fail(unsupported(resolved))
|
||||||
providerID: resolved.providerID,
|
const runtime = produce(resolved, (draft) => {
|
||||||
modelID: resolved.id,
|
draft.settings = ProviderV2.mergeOverlay(draft.settings, {
|
||||||
api: apiName(resolved),
|
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
|
||||||
}),
|
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
|
||||||
)
|
...credential?.metadata,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved)))
|
||||||
|
}
|
||||||
|
if (!resolved.package) return Effect.fail(unsupported(resolved))
|
||||||
|
|
||||||
|
const specifier = resolved.package
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const module = yield* (dependencies.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe(
|
||||||
|
Effect.mapError(() => unsupported(resolved)),
|
||||||
|
)
|
||||||
|
const settings = {
|
||||||
|
...resolved.settings,
|
||||||
|
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
|
||||||
|
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
|
||||||
|
...credential?.metadata,
|
||||||
|
headers: resolved.headers,
|
||||||
|
body: resolved.body,
|
||||||
|
limits: { context: resolved.limit.context, output: resolved.limit.output },
|
||||||
|
}
|
||||||
|
return yield* Effect.try({
|
||||||
|
try: () =>
|
||||||
|
Model.update(module.model(resolved.modelID ?? resolved.id, settings), { provider: resolved.providerID }),
|
||||||
|
catch: () => unsupported(resolved),
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, credential?: Credential.Value) =>
|
const isNativeOpenAI = (packageName: string | undefined) =>
|
||||||
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
|
packageName === "@opencode-ai/llm/providers/openai" ||
|
||||||
|
packageName?.startsWith("@opencode-ai/llm/providers/openai/") === true
|
||||||
|
|
||||||
export const supported = (model: ModelV2.Info) =>
|
const codexModel = (
|
||||||
model.api.type === "aisdk" &&
|
model: ModelV2.Info,
|
||||||
(model.api.package === "@ai-sdk/openai" ||
|
credential: Credential.Value | undefined,
|
||||||
model.api.package === "@ai-sdk/anthropic" ||
|
key: ReturnType<typeof Auth.value> | undefined,
|
||||||
(model.api.package === "@ai-sdk/openai-compatible" && model.api.url !== undefined))
|
) => {
|
||||||
|
const account = OpenAICodex.accountID(credential)
|
||||||
|
return withDefaults(model, OpenAIResponses.route)
|
||||||
|
.with({
|
||||||
|
endpoint: { baseURL: OpenAICodex.baseURL },
|
||||||
|
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen(
|
||||||
|
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.model({ id: model.modelID ?? model.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsupported = (model: ModelV2.Info) =>
|
||||||
|
new UnsupportedPackageError({
|
||||||
|
providerID: model.providerID,
|
||||||
|
modelID: model.id,
|
||||||
|
package: model.package ?? "unknown",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const resolve = (
|
||||||
|
session: SessionSchema.Info,
|
||||||
|
model: ModelV2.Info,
|
||||||
|
credential?: Credential.Value,
|
||||||
|
dependencies?: Dependencies,
|
||||||
|
) =>
|
||||||
|
withVariant(model, session.model?.variant).pipe(
|
||||||
|
Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const supported = (model: ModelV2.Info) => Boolean(model.package)
|
||||||
|
|
||||||
/** Resolves models from the catalog belonging to the current Location runtime. */
|
/** Resolves models from the catalog belonging to the current Location runtime. */
|
||||||
const layer = Layer.effect(
|
const layer = Layer.effect(
|
||||||
|
|
@ -232,6 +302,8 @@ const layer = Layer.effect(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const integrations = yield* Integration.Service
|
const integrations = yield* Integration.Service
|
||||||
|
const npm = yield* Npm.Service
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
return Service.of({
|
return Service.of({
|
||||||
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
||||||
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
||||||
|
|
@ -257,6 +329,10 @@ const layer = Layer.effect(
|
||||||
session,
|
session,
|
||||||
selected,
|
selected,
|
||||||
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
||||||
|
{
|
||||||
|
loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm),
|
||||||
|
loadAISDK: (model) => aisdk.model(model),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
model,
|
model,
|
||||||
|
|
@ -271,4 +347,8 @@ const layer = Layer.effect(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, Integration.node] })
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [Catalog.node, Integration.node, Npm.node, AISDK.node],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import {
|
||||||
ToolOutput,
|
ToolOutput,
|
||||||
ToolResultPart,
|
ToolResultPart,
|
||||||
type ContentPart,
|
type ContentPart,
|
||||||
type Model,
|
|
||||||
type ProviderMetadata,
|
type ProviderMetadata,
|
||||||
} from "@opencode-ai/llm"
|
} from "@opencode-ai/llm"
|
||||||
import { Option, Schema } from "effect"
|
import { Option, Schema } from "effect"
|
||||||
|
import type { ModelV2 } from "../../model"
|
||||||
import { SessionMessage } from "../message"
|
import { SessionMessage } from "../message"
|
||||||
import type { FileAttachment } from "@opencode-ai/schema/prompt"
|
import type { FileAttachment } from "@opencode-ai/schema/prompt"
|
||||||
|
|
||||||
|
|
@ -88,9 +88,9 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
|
||||||
const sameModel =
|
const sameModel =
|
||||||
String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id)
|
String(message.model.providerID) === String(model.providerID) && String(message.model.id) === String(model.id)
|
||||||
const reuseProviderMetadata = sameModel && message.error === undefined
|
const reuseProviderMetadata = sameModel && message.error === undefined
|
||||||
const content = message.content.flatMap((item): ContentPart[] => {
|
const content = message.content.flatMap((item): ContentPart[] => {
|
||||||
if (item.type === "text") return [{ type: "text", text: item.text }]
|
if (item.type === "text") return [{ type: "text", text: item.text }]
|
||||||
|
|
@ -133,7 +133,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function toLLMMessage(message: SessionMessage.Message, model: Model): Message[] {
|
function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Message[] {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "agent-switched":
|
case "agent-switched":
|
||||||
case "model-switched":
|
case "model-switched":
|
||||||
|
|
@ -197,5 +197,5 @@ ${message.recent}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Translate projected V2 Session history into canonical @opencode-ai/llm context. */
|
/** Translate projected V2 Session history into canonical @opencode-ai/llm context. */
|
||||||
export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: Model) =>
|
export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: ModelV2.Ref) =>
|
||||||
messages.flatMap((message) => toLLMMessage(message, model))
|
messages.flatMap((message) => toLLMMessage(message, model))
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@ export * as ConfigMigrateV1 from "./migrate"
|
||||||
|
|
||||||
import { ConfigV1 } from "./config"
|
import { ConfigV1 } from "./config"
|
||||||
import { ConfigAgentV1 } from "./agent"
|
import { ConfigAgentV1 } from "./agent"
|
||||||
|
import { ConfigCommandV1 } from "./command"
|
||||||
import { ConfigMCPV1 } from "./mcp"
|
import { ConfigMCPV1 } from "./mcp"
|
||||||
import { ConfigPermissionV1 } from "./permission"
|
import { ConfigPermissionV1 } from "./permission"
|
||||||
import { ConfigProviderV1 } from "./provider"
|
import { ConfigProviderV1 } from "./provider"
|
||||||
import { ConfigProviderOptionsV1 } from "./provider-options"
|
import { ConfigProviderOptionsV1 } from "./provider-options"
|
||||||
|
import { ProviderV2 } from "../../provider"
|
||||||
|
|
||||||
const keys = new Set([
|
const keys = new Set([
|
||||||
"logLevel",
|
"logLevel",
|
||||||
|
|
@ -48,7 +50,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
|
||||||
return {
|
return {
|
||||||
$schema: info.$schema,
|
$schema: info.$schema,
|
||||||
shell: info.shell,
|
shell: info.shell,
|
||||||
model: info.model,
|
model: modelSelection(info.model),
|
||||||
default_agent: info.default_agent,
|
default_agent: info.default_agent,
|
||||||
autoupdate: info.autoupdate,
|
autoupdate: info.autoupdate,
|
||||||
share: info.share ?? (info.autoshare ? "auto" : undefined),
|
share: info.share ?? (info.autoshare ? "auto" : undefined),
|
||||||
|
|
@ -72,7 +74,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
|
||||||
buffer: info.compaction.reserved,
|
buffer: info.compaction.reserved,
|
||||||
},
|
},
|
||||||
skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])],
|
skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])],
|
||||||
commands: info.command,
|
commands: commands(info.command),
|
||||||
instructions: info.instructions,
|
instructions: info.instructions,
|
||||||
references: info.references ?? info.reference,
|
references: info.references ?? info.reference,
|
||||||
plugins: info.plugin?.map((plugin) =>
|
plugins: info.plugin?.map((plugin) =>
|
||||||
|
|
@ -126,8 +128,7 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
|
||||||
...(info.top_p === undefined ? {} : { top_p: info.top_p }),
|
...(info.top_p === undefined ? {} : { top_p: info.top_p }),
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
model: info.model,
|
model: modelSelection(info.model, info.variant),
|
||||||
variant: info.variant,
|
|
||||||
request: Object.keys(body).length ? { body } : undefined,
|
request: Object.keys(body).length ? { body } : undefined,
|
||||||
system: info.prompt,
|
system: info.prompt,
|
||||||
description: info.description,
|
description: info.description,
|
||||||
|
|
@ -140,6 +141,32 @@ export function migrateAgent(info: ConfigAgentV1.Info) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function commands(info?: Readonly<Record<string, ConfigCommandV1.Info>>) {
|
||||||
|
if (!info) return undefined
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(info).map(([id, command]) => [
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
template: command.template,
|
||||||
|
description: command.description,
|
||||||
|
agent: command.agent,
|
||||||
|
model: modelSelection(command.model, command.variant),
|
||||||
|
subtask: command.subtask,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelSelection(input?: string, variant?: string) {
|
||||||
|
if (input === undefined || !/^[^/#]+\/[^#]+$/.test(input)) return undefined
|
||||||
|
const separator = input.indexOf("/")
|
||||||
|
return {
|
||||||
|
providerID: input.slice(0, separator),
|
||||||
|
model: input.slice(separator + 1),
|
||||||
|
...(variant === undefined || variant.length === 0 || variant.includes("#") ? {} : { variant }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mcp(info: typeof ConfigV1.Info.Type) {
|
function mcp(info: typeof ConfigV1.Info.Type) {
|
||||||
const servers = Object.fromEntries(
|
const servers = Object.fromEntries(
|
||||||
Object.entries(info.mcp ?? {}).flatMap(([name, server]) =>
|
Object.entries(info.mcp ?? {}).flatMap(([name, server]) =>
|
||||||
|
|
@ -184,31 +211,22 @@ function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateProvider(info: ConfigProviderV1.Info) {
|
function migrateProvider(info: ConfigProviderV1.Info) {
|
||||||
const lowerer = ConfigProviderOptionsV1.get(info.npm)
|
const options = ConfigProviderOptionsV1.provider(info.options ?? {})
|
||||||
const options = lowerer.provider(info.options ?? {})
|
|
||||||
const url = info.api ?? options.url
|
|
||||||
return {
|
return {
|
||||||
name: info.name,
|
name: info.name,
|
||||||
env: info.env,
|
env: info.env,
|
||||||
api: info.npm
|
package: info.npm ? ProviderV2.aisdk(info.npm) : undefined,
|
||||||
? {
|
settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings,
|
||||||
type: "aisdk" as const,
|
headers: info.options && options.headers,
|
||||||
package: info.npm,
|
body: info.options && options.body,
|
||||||
...(url === undefined ? {} : { url }),
|
|
||||||
settings: options.settings ?? {},
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
request: info.options && { headers: options.headers, body: options.body },
|
|
||||||
models:
|
models:
|
||||||
info.models &&
|
info.models &&
|
||||||
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model, info.npm)])),
|
Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model)])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: string) {
|
function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
|
||||||
const packageID = info.provider?.npm ?? packageName
|
const settings = info.options && ConfigProviderOptionsV1.model(info.options)
|
||||||
const lowerer = ConfigProviderOptionsV1.get(packageID)
|
|
||||||
const request = info.options && lowerer.request(info.options)
|
|
||||||
const costs = info.cost && [
|
const costs = info.cost && [
|
||||||
{
|
{
|
||||||
input: info.cost.input,
|
input: info.cost.input,
|
||||||
|
|
@ -231,29 +249,18 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
|
||||||
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
|
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
|
||||||
: undefined
|
: undefined
|
||||||
return {
|
return {
|
||||||
|
modelID: info.id,
|
||||||
family: info.family,
|
family: info.family,
|
||||||
name: info.name,
|
name: info.name,
|
||||||
api: info.provider?.npm
|
package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined,
|
||||||
? {
|
settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
|
||||||
...(info.id === undefined ? {} : { id: info.id }),
|
|
||||||
type: "aisdk" as const,
|
|
||||||
package: info.provider.npm,
|
|
||||||
...(info.provider.api === undefined ? {} : { url: info.provider.api }),
|
|
||||||
settings: {},
|
|
||||||
}
|
|
||||||
: info.id === undefined
|
|
||||||
? undefined
|
|
||||||
: { id: info.id },
|
|
||||||
capabilities,
|
capabilities,
|
||||||
request: (info.headers || request) && {
|
headers: info.headers,
|
||||||
headers: info.headers,
|
|
||||||
body: request,
|
|
||||||
},
|
|
||||||
variants:
|
variants:
|
||||||
info.variants &&
|
info.variants &&
|
||||||
Object.entries(info.variants).map(([id, options]) => ({
|
Object.entries(info.variants).map(([id, options]) => ({
|
||||||
id,
|
id,
|
||||||
body: lowerer.request(options),
|
settings: ConfigProviderOptionsV1.model(options),
|
||||||
})),
|
})),
|
||||||
cost: costs,
|
cost: costs,
|
||||||
disabled: info.status === "deprecated" ? true : undefined,
|
disabled: info.status === "deprecated" ? true : undefined,
|
||||||
|
|
|
||||||
|
|
@ -3,225 +3,29 @@ export * as ConfigProviderOptionsV1 from "./provider-options"
|
||||||
type Options = Readonly<Record<string, unknown>>
|
type Options = Readonly<Record<string, unknown>>
|
||||||
|
|
||||||
export interface ProviderResult {
|
export interface ProviderResult {
|
||||||
|
readonly settings: Record<string, unknown>
|
||||||
readonly headers?: Record<string, string>
|
readonly headers?: Record<string, string>
|
||||||
readonly body?: Record<string, unknown>
|
readonly body?: Record<string, unknown>
|
||||||
readonly url?: string
|
|
||||||
readonly settings?: Record<string, unknown>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Lowerer {
|
export function provider(options: Options): ProviderResult {
|
||||||
readonly provider: (options: Options) => ProviderResult
|
const headers = options.headers
|
||||||
readonly request: (options: Options) => Record<string, unknown>
|
const body = options.body
|
||||||
}
|
const settings = Object.fromEntries(Object.entries(options).filter(([key]) => key !== "headers" && key !== "body"))
|
||||||
|
const headerOverlay =
|
||||||
export function get(packageName?: string): Lowerer {
|
typeof headers === "object" && headers !== null && !Array.isArray(headers)
|
||||||
const key = packageName ?? ""
|
? Object.fromEntries(
|
||||||
return Object.hasOwn(lowerers, key) ? lowerers[key]! : raw
|
Object.entries(headers).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
||||||
}
|
)
|
||||||
|
: undefined
|
||||||
const raw: Lowerer = {
|
const bodyOverlay = typeof body === "object" && body !== null && !Array.isArray(body) ? { ...body } : undefined
|
||||||
provider(options) {
|
|
||||||
return { body: clone(options) }
|
|
||||||
},
|
|
||||||
request: clone,
|
|
||||||
}
|
|
||||||
|
|
||||||
const openai: Lowerer = {
|
|
||||||
provider(options) {
|
|
||||||
return {
|
|
||||||
url: string(options.baseURL),
|
|
||||||
headers: compact({
|
|
||||||
Authorization: bearer(options.apiKey),
|
|
||||||
"OpenAI-Organization": string(options.organization),
|
|
||||||
"OpenAI-Project": string(options.project),
|
|
||||||
...headers(options.headers),
|
|
||||||
}),
|
|
||||||
body: body(options.body),
|
|
||||||
settings: omit(options, ["apiKey", "baseURL", "organization", "project", "headers", "body"]),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
request(options) {
|
|
||||||
const result = snake(options)
|
|
||||||
if (options.reasoningEffort !== undefined || options.reasoningSummary !== undefined) {
|
|
||||||
result.reasoning = {
|
|
||||||
...(isRecord(result.reasoning) ? result.reasoning : {}),
|
|
||||||
...(options.reasoningEffort !== undefined ? { effort: options.reasoningEffort } : {}),
|
|
||||||
...(options.reasoningSummary !== undefined ? { summary: options.reasoningSummary } : {}),
|
|
||||||
}
|
|
||||||
delete result.reasoning_effort
|
|
||||||
delete result.reasoning_summary
|
|
||||||
}
|
|
||||||
if (options.textVerbosity !== undefined) {
|
|
||||||
result.text = { ...(isRecord(result.text) ? result.text : {}), verbosity: options.textVerbosity }
|
|
||||||
delete result.text_verbosity
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const anthropic: Lowerer = {
|
|
||||||
provider(options) {
|
|
||||||
return {
|
|
||||||
url: string(options.baseURL),
|
|
||||||
headers: compact({
|
|
||||||
"x-api-key": string(options.apiKey),
|
|
||||||
Authorization: options.authToken ? bearer(options.authToken) : undefined,
|
|
||||||
...headers(options.headers),
|
|
||||||
}),
|
|
||||||
body: body(options.body),
|
|
||||||
settings: omit(options, ["apiKey", "authToken", "baseURL", "headers", "body"]),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
request(options) {
|
|
||||||
const result = snake(options)
|
|
||||||
if (options.effort !== undefined || options.taskBudget !== undefined) {
|
|
||||||
result.output_config = compactUnknown({ effort: options.effort, task_budget: options.taskBudget })
|
|
||||||
delete result.effort
|
|
||||||
delete result.task_budget
|
|
||||||
}
|
|
||||||
if (isRecord(options.metadata) && options.metadata.userId !== undefined) {
|
|
||||||
result.metadata = { ...(isRecord(result.metadata) ? result.metadata : {}), user_id: options.metadata.userId }
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const google: Lowerer = {
|
|
||||||
provider(options) {
|
|
||||||
return {
|
|
||||||
url: string(options.baseURL),
|
|
||||||
headers: compact({ "x-goog-api-key": string(options.apiKey), ...headers(options.headers) }),
|
|
||||||
body: body(options.body),
|
|
||||||
settings: omit(options, ["apiKey", "baseURL", "headers", "body"]),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
request(options) {
|
|
||||||
const generationConfig = pick(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"])
|
|
||||||
return {
|
|
||||||
...omit(options, ["thinkingConfig", "responseModalities", "mediaResolution", "imageConfig"]),
|
|
||||||
...(Object.keys(generationConfig).length ? { generationConfig } : {}),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const azure: Lowerer = {
|
|
||||||
provider(options) {
|
|
||||||
return {
|
|
||||||
url: string(options.baseURL),
|
|
||||||
headers: compact({ "api-key": string(options.apiKey), ...headers(options.headers) }),
|
|
||||||
body: body(options.body),
|
|
||||||
settings: omit(options, ["apiKey", "baseURL", "headers", "body"]),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
request: openai.request,
|
|
||||||
}
|
|
||||||
|
|
||||||
const bedrock: Lowerer = {
|
|
||||||
provider(options) {
|
|
||||||
return direct(options)
|
|
||||||
},
|
|
||||||
request(options) {
|
|
||||||
return { additionalModelRequestFields: clone(options) }
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const openaiCompatible: Lowerer = {
|
|
||||||
provider(options) {
|
|
||||||
return { ...direct(options, ["baseURL"]), url: string(options.baseURL) }
|
|
||||||
},
|
|
||||||
request(options) {
|
|
||||||
const result = clone(options)
|
|
||||||
if (options.reasoningEffort !== undefined) {
|
|
||||||
result.reasoning_effort = options.reasoningEffort
|
|
||||||
delete result.reasoningEffort
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowerers: Readonly<Record<string, Lowerer>> = {
|
|
||||||
"@ai-sdk/openai": openai,
|
|
||||||
"@ai-sdk/anthropic": anthropic,
|
|
||||||
"@ai-sdk/google-vertex/anthropic": anthropic,
|
|
||||||
"@ai-sdk/google": google,
|
|
||||||
"@ai-sdk/google-vertex": google,
|
|
||||||
"@ai-sdk/azure": azure,
|
|
||||||
"@ai-sdk/amazon-bedrock": bedrock,
|
|
||||||
"@ai-sdk/openai-compatible": openaiCompatible,
|
|
||||||
"@ai-sdk/cerebras": openaiCompatible,
|
|
||||||
"@ai-sdk/deepinfra": openaiCompatible,
|
|
||||||
"@ai-sdk/groq": openaiCompatible,
|
|
||||||
"@ai-sdk/mistral": openaiCompatible,
|
|
||||||
"@ai-sdk/togetherai": openaiCompatible,
|
|
||||||
"@ai-sdk/xai": openaiCompatible,
|
|
||||||
"@openrouter/ai-sdk-provider": openaiCompatible,
|
|
||||||
"ai-gateway-provider": openaiCompatible,
|
|
||||||
"venice-ai-sdk-provider": openaiCompatible,
|
|
||||||
}
|
|
||||||
|
|
||||||
function direct(options: Options, extraKeys: ReadonlyArray<string> = []): ProviderResult {
|
|
||||||
return {
|
return {
|
||||||
headers: headers(options.headers),
|
settings,
|
||||||
body: body(options.body),
|
headers: headerOverlay,
|
||||||
settings: omit(options, ["headers", "body", ...extraKeys]),
|
body: bodyOverlay,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function body(input: unknown) {
|
export function model(options: Options) {
|
||||||
if (!isRecord(input)) return undefined
|
|
||||||
return { ...input }
|
|
||||||
}
|
|
||||||
|
|
||||||
function snake(options: Options) {
|
|
||||||
return Object.fromEntries(Object.entries(options).map(([key, value]) => [snakeKey(key), snakeValue(value)]))
|
|
||||||
}
|
|
||||||
|
|
||||||
function snakeValue(value: unknown): unknown {
|
|
||||||
if (Array.isArray(value)) return value.map(snakeValue)
|
|
||||||
if (!isRecord(value)) return value
|
|
||||||
return Object.fromEntries(Object.entries(value).map(([key, value]) => [snakeKey(key), snakeValue(value)]))
|
|
||||||
}
|
|
||||||
|
|
||||||
function snakeKey(key: string) {
|
|
||||||
return key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
function clone(options: Options) {
|
|
||||||
return { ...options }
|
return { ...options }
|
||||||
}
|
}
|
||||||
|
|
||||||
function omit(options: Options, keys: ReadonlyArray<string>) {
|
|
||||||
return Object.fromEntries(Object.entries(options).filter(([key]) => !keys.includes(key)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function pick(options: Options, keys: ReadonlyArray<string>) {
|
|
||||||
return Object.fromEntries(Object.entries(options).filter(([key]) => keys.includes(key)))
|
|
||||||
}
|
|
||||||
|
|
||||||
function headers(input: unknown) {
|
|
||||||
if (!isRecord(input)) return undefined
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(input).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function compact(input: Record<string, string | undefined>) {
|
|
||||||
const entries = Object.entries(input).filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
||||||
return entries.length ? Object.fromEntries(entries) : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function compactUnknown(input: Record<string, unknown>) {
|
|
||||||
return Object.fromEntries(Object.entries(input).filter((entry) => entry[1] !== undefined))
|
|
||||||
}
|
|
||||||
|
|
||||||
function string(input: unknown) {
|
|
||||||
return typeof input === "string" && input ? input : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function bearer(input: unknown) {
|
|
||||||
return typeof input === "string" && input ? `Bearer ${input}` : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(input: unknown): input is Record<string, unknown> {
|
|
||||||
return typeof input === "object" && input !== null && !Array.isArray(input)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
85
packages/core/test/aisdk.test.ts
Normal file
85
packages/core/test/aisdk.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import type { LanguageModelV3CallOptions } from "@ai-sdk/provider"
|
||||||
|
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||||
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
import { LLM } from "@opencode-ai/llm"
|
||||||
|
import { LLMClient } from "@opencode-ai/llm/route"
|
||||||
|
import { expect } from "bun:test"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
|
const it = testEffect(AISDK.locationLayer)
|
||||||
|
type Fetch = (input: Parameters<typeof fetch>[0], init?: RequestInit) => Promise<Response>
|
||||||
|
|
||||||
|
const model = (packageName: string, settings: Record<string, unknown> = {}) =>
|
||||||
|
ModelV2.Info.make({
|
||||||
|
...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")),
|
||||||
|
modelID: ModelV2.ID.make("api-model"),
|
||||||
|
package: ProviderV2.aisdk(packageName),
|
||||||
|
settings,
|
||||||
|
limit: { context: 100, output: 20 },
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("keys language models by package and flattened overlays", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
|
const loaded: string[] = []
|
||||||
|
yield* aisdk.hook.sdk((event) => {
|
||||||
|
loaded.push(event.package)
|
||||||
|
event.sdk = { languageModel: () => ({ package: event.package }) }
|
||||||
|
})
|
||||||
|
|
||||||
|
const first = yield* aisdk.language(model("first", { region: "us-east-1" }))
|
||||||
|
const second = yield* aisdk.language(model("second", { region: "us-east-1" }))
|
||||||
|
const third = yield* aisdk.language(
|
||||||
|
model("second", { region: "us-east-1", fetch: async () => new Response("ok") }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(first).not.toBe(second)
|
||||||
|
expect(second).not.toBe(third)
|
||||||
|
expect(loaded).toEqual(["first", "second", "second"])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("projects request settings, headers, and raw body overlays", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
|
let wrappedFetch: Fetch | undefined
|
||||||
|
let body: unknown
|
||||||
|
const customFetch: Fetch = async (_input, init) => {
|
||||||
|
body = init?.body
|
||||||
|
return new Response("ok")
|
||||||
|
}
|
||||||
|
yield* aisdk.hook.sdk((event) => {
|
||||||
|
wrappedFetch = event.options.fetch
|
||||||
|
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolved = yield* aisdk.model(
|
||||||
|
ModelV2.Info.make({
|
||||||
|
...model("@ai-sdk/google", {
|
||||||
|
apiKey: "secret",
|
||||||
|
fetch: customFetch,
|
||||||
|
thinkingConfig: { thinkingBudget: 1024 },
|
||||||
|
}),
|
||||||
|
headers: { "x-test": "header" },
|
||||||
|
body: { safety_setting: "strict" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||||
|
LLM.request({ model: resolved, prompt: "Hello" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.providerOptions).toEqual({
|
||||||
|
google: { thinkingConfig: { thinkingBudget: 1024 } },
|
||||||
|
})
|
||||||
|
expect(prepared.body.headers).toEqual({ "x-test": "header" })
|
||||||
|
expect(wrappedFetch).toBeFunction()
|
||||||
|
if (wrappedFetch === undefined) return yield* Effect.die("Expected wrapped fetch")
|
||||||
|
const fetchRequest = wrappedFetch
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
fetchRequest("https://provider.example", { method: "POST", body: JSON.stringify({ model: "api-model" }) }),
|
||||||
|
)
|
||||||
|
expect(JSON.parse(String(body))).toEqual({ model: "api-model", safety_setting: "strict" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
@ -61,14 +61,14 @@ describe("CatalogV2", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
|
||||||
yield* credentials.create({
|
yield* credentials.create({
|
||||||
integrationID,
|
integrationID,
|
||||||
label: "Second",
|
label: "Second",
|
||||||
value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }),
|
value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }),
|
||||||
})
|
})
|
||||||
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined()
|
||||||
}).pipe(Effect.provide(localCatalogLayer))
|
}).pipe(Effect.provide(localCatalogLayer))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -130,111 +130,91 @@ describe("CatalogV2", () => {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("normalizes provider baseURL into api url", () =>
|
it.effect("stores provider package settings", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://override.example.com" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://default.example.com",
|
|
||||||
}
|
|
||||||
provider.request.body.baseURL = "https://override.example.com"
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(required(yield* catalog.provider.get(providerID)).api).toEqual({
|
expect(required(yield* catalog.provider.get(providerID))).toMatchObject({
|
||||||
type: "aisdk",
|
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
|
||||||
package: "@ai-sdk/openai-compatible",
|
settings: { baseURL: "https://override.example.com" },
|
||||||
url: "https://override.example.com",
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("normalizes model baseURL into api url", () =>
|
it.effect("uses model package settings over provider settings", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
const modelID = ModelV2.ID.make("model")
|
const modelID = ModelV2.ID.make("model")
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://provider.example.com" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://provider.example.com",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
catalog.model.update(providerID, modelID, (model) => {
|
catalog.model.update(providerID, modelID, (model) => {
|
||||||
model.api = {
|
model.modelID = ModelV2.ID.make("upstream-model")
|
||||||
id: modelID,
|
model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
model.settings = { baseURL: "https://override.example.com" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://model.example.com",
|
|
||||||
}
|
|
||||||
model.request.body.baseURL = "https://override.example.com"
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
|
expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
|
||||||
id: modelID,
|
modelID: ModelV2.ID.make("upstream-model"),
|
||||||
type: "aisdk",
|
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
|
||||||
package: "@ai-sdk/openai-compatible",
|
settings: { baseURL: "https://override.example.com" },
|
||||||
url: "https://override.example.com",
|
|
||||||
settings: {},
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("resolves default model api from provider api", () =>
|
it.effect("resolves default model package settings from the provider", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
const modelID = ModelV2.ID.make("model")
|
const modelID = ModelV2.ID.make("model")
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://provider.example.com" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://provider.example.com",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
catalog.model.update(providerID, modelID, () => {})
|
catalog.model.update(providerID, modelID, () => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
|
expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({
|
||||||
id: modelID,
|
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
|
||||||
type: "aisdk",
|
settings: { baseURL: "https://provider.example.com" },
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://provider.example.com",
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("resolves provider and model request merges", () =>
|
it.effect("resolves provider and model overlay merges", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const providerID = ProviderV2.ID.make("test")
|
const providerID = ProviderV2.ID.make("test")
|
||||||
const modelID = ModelV2.ID.make("model")
|
const modelID = ModelV2.ID.make("model")
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(providerID, (provider) => {
|
catalog.provider.update(providerID, (provider) => {
|
||||||
provider.request.headers.provider = "provider"
|
provider.settings = { provider: true, shared: "provider" }
|
||||||
provider.request.headers.shared = "provider"
|
provider.headers = { provider: "provider", shared: "provider" }
|
||||||
provider.request.body.provider = true
|
provider.body = { provider: true, shared: "provider" }
|
||||||
})
|
})
|
||||||
catalog.model.update(providerID, modelID, (model) => {
|
catalog.model.update(providerID, modelID, (model) => {
|
||||||
model.request.headers.model = "model"
|
model.settings = { model: true, shared: "model" }
|
||||||
model.request.headers.shared = "model"
|
model.headers = { model: "model", shared: "model" }
|
||||||
model.request.body.model = true
|
model.body = { model: true, shared: "model" }
|
||||||
model.request.body.request = true
|
|
||||||
model.request.body.shared = "model"
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
const model = required(yield* catalog.model.get(providerID, modelID))
|
||||||
expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
|
expect(model.settings).toEqual({ provider: true, shared: "model", model: true })
|
||||||
expect(model.request.body).toEqual({ provider: true, model: true, request: true, shared: "model" })
|
expect(model.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
|
||||||
|
expect(model.body).toEqual({ provider: true, shared: "model", model: true })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
||||||
info: decode({
|
info: decode({
|
||||||
permissions: [{ action: "read", resource: "*", effect: "allow" }],
|
permissions: [{ action: "read", resource: "*", effect: "allow" }],
|
||||||
agents: {
|
agents: {
|
||||||
reviewer: { variant: "high", hidden: true },
|
reviewer: { model: "openrouter/openai/gpt-5#high", hidden: true },
|
||||||
removed: { disabled: true },
|
removed: { disabled: true },
|
||||||
late: {
|
late: {
|
||||||
permissions: [{ action: "edit", resource: "*", effect: "allow" }],
|
permissions: [{ action: "edit", resource: "*", effect: "allow" }],
|
||||||
|
|
@ -170,7 +170,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
||||||
hidden: true,
|
hidden: true,
|
||||||
color: "warning",
|
color: "warning",
|
||||||
steps: 12,
|
steps: 12,
|
||||||
model: { providerID: "anthropic", id: "claude-sonnet", variant: undefined },
|
model: { providerID: "anthropic", id: "claude-sonnet" },
|
||||||
})
|
})
|
||||||
expect(reviewer.request).toEqual({
|
expect(reviewer.request).toEqual({
|
||||||
settings: {},
|
settings: {},
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,7 @@ describe("ConfigCommandPlugin.Plugin", () => {
|
||||||
`---
|
`---
|
||||||
description: File review
|
description: File review
|
||||||
agent: reviewer
|
agent: reviewer
|
||||||
model: anthropic/claude
|
model: anthropic/claude#high
|
||||||
variant: high
|
|
||||||
subtask: true
|
subtask: true
|
||||||
---
|
---
|
||||||
Review files`,
|
Review files`,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { describe, expect } from "bun:test"
|
||||||
import { Effect, Fiber, Layer, PubSub, Schema, Stream } from "effect"
|
import { Effect, Fiber, Layer, PubSub, Schema, Stream } from "effect"
|
||||||
import { FastCheck } from "effect/testing"
|
import { FastCheck } from "effect/testing"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
|
import { ConfigModel } from "@opencode-ai/core/config/model"
|
||||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||||
import { ConfigProvider } from "@opencode-ai/core/config/provider"
|
import { ConfigProvider } from "@opencode-ai/core/config/provider"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
|
@ -16,12 +17,14 @@ import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/core/project"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { location } from "../fixture/location"
|
import { location } from "../fixture/location"
|
||||||
import { tmpdir } from "../fixture/tmpdir"
|
import { tmpdir } from "../fixture/tmpdir"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Layer.empty)
|
const it = testEffect(Layer.empty)
|
||||||
|
const selection = Schema.decodeUnknownSync(ConfigModel.Selection)
|
||||||
|
|
||||||
function testLayer(
|
function testLayer(
|
||||||
directory: string,
|
directory: string,
|
||||||
|
|
@ -47,11 +50,10 @@ function testLayer(
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
api: { type: "native", settings: {} },
|
package: "native",
|
||||||
request: {
|
settings: {},
|
||||||
headers: {},
|
headers: {},
|
||||||
body: {},
|
body: {},
|
||||||
},
|
|
||||||
models: {},
|
models: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,13 +107,19 @@ describe("Config", () => {
|
||||||
it.effect("returns the latest defined scalar from priority-ordered documents", () =>
|
it.effect("returns the latest defined scalar from priority-ordered documents", () =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const entries = [
|
const entries = [
|
||||||
new Config.Document({ type: "document", info: new Config.Info({ model: "openrouter/openai/gpt-5" }) }),
|
new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
info: new Config.Info({ model: selection("openrouter/openai/gpt-5") }),
|
||||||
|
}),
|
||||||
new Config.Directory({ type: "directory", path: AbsolutePath.make("/skills") }),
|
new Config.Directory({ type: "directory", path: AbsolutePath.make("/skills") }),
|
||||||
new Config.Document({ type: "document", info: new Config.Info({}) }),
|
new Config.Document({ type: "document", info: new Config.Info({}) }),
|
||||||
new Config.Document({ type: "document", info: new Config.Info({ model: "openrouter/openai/gpt-5.5" }) }),
|
new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
info: new Config.Info({ model: selection("openrouter/openai/gpt-5.5") }),
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(Config.latest(entries, "model")).toBe("openrouter/openai/gpt-5.5")
|
expect(Config.latest(entries, "model")).toEqual(selection("openrouter/openai/gpt-5.5"))
|
||||||
expect(Config.latest(entries, "default_agent")).toBeUndefined()
|
expect(Config.latest(entries, "default_agent")).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -142,7 +150,12 @@ describe("Config", () => {
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
FastCheck.assert(
|
FastCheck.assert(
|
||||||
FastCheck.property(Schema.toArbitrary(ConfigV1.Info), (info) => {
|
FastCheck.property(Schema.toArbitrary(ConfigV1.Info), (info) => {
|
||||||
Schema.decodeUnknownSync(Config.Info)(ConfigMigrateV1.migrate(info), { errors: "all" })
|
const parsed = Schema.decodeUnknownSync(ConfigV1.Info)(
|
||||||
|
Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(
|
||||||
|
Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(info),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Schema.decodeUnknownSync(Config.Info)(ConfigMigrateV1.migrate(parsed), { errors: "all" })
|
||||||
}),
|
}),
|
||||||
{ numRuns: 100 },
|
{ numRuns: 100 },
|
||||||
)
|
)
|
||||||
|
|
@ -165,12 +178,9 @@ describe("Config", () => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(migrated.providers?.bedrock?.api).toEqual({
|
expect(migrated.providers?.bedrock).toMatchObject({
|
||||||
type: "aisdk",
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
|
||||||
settings: { region: "us-east-1", profile: "dev" },
|
settings: { region: "us-east-1", profile: "dev" },
|
||||||
})
|
|
||||||
expect(migrated.providers?.bedrock?.request).toEqual({
|
|
||||||
headers: { "x-test": "1" },
|
headers: { "x-test": "1" },
|
||||||
body: { trace: true },
|
body: { trace: true },
|
||||||
})
|
})
|
||||||
|
|
@ -197,8 +207,7 @@ describe("Config", () => {
|
||||||
template: "Review changes",
|
template: "Review changes",
|
||||||
description: "Review code",
|
description: "Review code",
|
||||||
agent: "reviewer",
|
agent: "reviewer",
|
||||||
model: "anthropic/claude",
|
model: { providerID: "anthropic", model: "claude", variant: "high" },
|
||||||
variant: "high",
|
|
||||||
subtask: true,
|
subtask: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -431,8 +440,7 @@ describe("Config", () => {
|
||||||
],
|
],
|
||||||
agents: {
|
agents: {
|
||||||
reviewer: {
|
reviewer: {
|
||||||
model: "openrouter/openai/gpt-5",
|
model: "openrouter/openai/gpt-5#high",
|
||||||
variant: "high",
|
|
||||||
request: {
|
request: {
|
||||||
headers: { "x-agent": "reviewer" },
|
headers: { "x-agent": "reviewer" },
|
||||||
body: { reasoningEffort: "high" },
|
body: { reasoningEffort: "high" },
|
||||||
|
|
@ -505,7 +513,7 @@ describe("Config", () => {
|
||||||
|
|
||||||
expect(documents).toHaveLength(1)
|
expect(documents).toHaveLength(1)
|
||||||
expect(documents[0]?.info.shell).toBe("/bin/bash")
|
expect(documents[0]?.info.shell).toBe("/bin/bash")
|
||||||
expect(documents[0]?.info.model).toBe("anthropic/claude")
|
expect(documents[0]?.info.model).toEqual(selection("anthropic/claude"))
|
||||||
expect(documents[0]?.info.default_agent).toBe("reviewer")
|
expect(documents[0]?.info.default_agent).toBe("reviewer")
|
||||||
expect(documents[0]?.info.autoupdate).toBe("notify")
|
expect(documents[0]?.info.autoupdate).toBe("notify")
|
||||||
expect(documents[0]?.info.share).toBe("disabled")
|
expect(documents[0]?.info.share).toBe("disabled")
|
||||||
|
|
@ -516,8 +524,7 @@ describe("Config", () => {
|
||||||
{ action: "bash", resource: "git status", effect: "allow" },
|
{ action: "bash", resource: "git status", effect: "allow" },
|
||||||
])
|
])
|
||||||
const reviewer = documents[0]?.info.agents?.reviewer
|
const reviewer = documents[0]?.info.agents?.reviewer
|
||||||
expect(reviewer?.model).toBe("openrouter/openai/gpt-5")
|
expect(reviewer?.model).toEqual(selection("openrouter/openai/gpt-5#high"))
|
||||||
expect(reviewer?.variant).toBe("high")
|
|
||||||
expect(reviewer?.request).toEqual({
|
expect(reviewer?.request).toEqual({
|
||||||
headers: { "x-agent": "reviewer" },
|
headers: { "x-agent": "reviewer" },
|
||||||
body: { reasoningEffort: "high" },
|
body: { reasoningEffort: "high" },
|
||||||
|
|
@ -748,34 +755,32 @@ describe("Config", () => {
|
||||||
})
|
})
|
||||||
expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
|
expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
|
||||||
expect(documents[0]?.info.providers?.custom).toMatchObject({
|
expect(documents[0]?.info.providers?.custom).toMatchObject({
|
||||||
request: { body: { apiKey: "secret" } },
|
settings: { apiKey: "secret" },
|
||||||
models: {
|
models: {
|
||||||
model: {
|
model: {
|
||||||
request: { body: { reasoningEffort: "high" } },
|
settings: { reasoningEffort: "high" },
|
||||||
variants: [{ id: "fast", body: { temperature: 0.2 } }],
|
variants: [{ id: "fast", settings: { temperature: 0.2 } }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(documents[0]?.info.providers?.openai).toMatchObject({
|
expect(documents[0]?.info.providers?.openai).toMatchObject({
|
||||||
api: { settings: {} },
|
package: ProviderV2.aisdk("@ai-sdk/openai"),
|
||||||
request: { headers: { Authorization: "Bearer secret", "OpenAI-Organization": "org" } },
|
settings: { apiKey: "secret", organization: "org" },
|
||||||
models: {
|
models: {
|
||||||
model: {
|
model: {
|
||||||
request: {
|
settings: { temperature: 0.3, reasoningEffort: "high", serviceTier: "priority" },
|
||||||
body: { temperature: 0.3, reasoning: { effort: "high" }, service_tier: "priority" },
|
variants: [{ id: "high", settings: { reasoningEffort: "high", reasoningSummary: "auto" } }],
|
||||||
},
|
|
||||||
variants: [{ id: "high", body: { reasoning: { effort: "high", summary: "auto" } } }],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(documents[0]?.info.providers?.anthropic).toMatchObject({
|
expect(documents[0]?.info.providers?.anthropic).toMatchObject({
|
||||||
|
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
|
||||||
models: {
|
models: {
|
||||||
model: {
|
model: {
|
||||||
request: {
|
settings: {
|
||||||
body: {
|
effort: "high",
|
||||||
output_config: { effort: "high", task_budget: 4096 },
|
taskBudget: 4096,
|
||||||
metadata: { user_id: "user-1" },
|
metadata: { userId: "user-1" },
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
28
packages/core/test/config/model.test.ts
Normal file
28
packages/core/test/config/model.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { ConfigModel } from "@opencode-ai/core/config/model"
|
||||||
|
import { Model } from "@opencode-ai/schema/model"
|
||||||
|
import { Provider } from "@opencode-ai/schema/provider"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
|
const decode = Schema.decodeUnknownSync(ConfigModel.Selection)
|
||||||
|
|
||||||
|
describe("ConfigModel.Selection", () => {
|
||||||
|
test("normalizes short and explicit model selections", () => {
|
||||||
|
expect(decode("openrouter/openai/gpt-5#high")).toEqual({
|
||||||
|
providerID: Provider.ID.make("openrouter"),
|
||||||
|
model: Model.ID.make("openai/gpt-5"),
|
||||||
|
variant: Model.VariantID.make("high"),
|
||||||
|
})
|
||||||
|
expect(decode({ providerID: "anthropic", model: "claude-sonnet", variant: "high" })).toEqual({
|
||||||
|
providerID: Provider.ID.make("anthropic"),
|
||||||
|
model: Model.ID.make("claude-sonnet"),
|
||||||
|
variant: Model.VariantID.make("high"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects malformed selections and reserved fragments", () => {
|
||||||
|
expect(() => decode("gpt-5")).toThrow()
|
||||||
|
expect(() => decode("openai/gpt-5#")).toThrow()
|
||||||
|
expect(() => decode({ providerID: "openai", model: "gpt-5#high" })).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -2,223 +2,47 @@ import { describe, expect, test } from "bun:test"
|
||||||
import { ConfigProviderOptionsV1 } from "@opencode-ai/core/v1/config/provider-options"
|
import { ConfigProviderOptionsV1 } from "@opencode-ai/core/v1/config/provider-options"
|
||||||
|
|
||||||
describe("ConfigProviderOptionsV1", () => {
|
describe("ConfigProviderOptionsV1", () => {
|
||||||
test("keeps raw provider and request options unchanged", () => {
|
test("splits provider overlays without changing package settings", () => {
|
||||||
const lowerer = ConfigProviderOptionsV1.get("custom-provider")
|
|
||||||
|
|
||||||
expect(lowerer.provider({ apiKey: "secret", headers: { "x-test": "1" }, nested: { camelCase: true } })).toEqual({
|
|
||||||
body: { apiKey: "secret", headers: { "x-test": "1" }, nested: { camelCase: true } },
|
|
||||||
})
|
|
||||||
expect(lowerer.request({ nested: { camelCase: true } })).toEqual({ nested: { camelCase: true } })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("falls back to raw lowering for prototype property package names", () => {
|
|
||||||
expect(ConfigProviderOptionsV1.get("toString").provider({ enabled: true })).toEqual({ body: { enabled: true } })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("lowers OpenAI provider and request options", () => {
|
|
||||||
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/openai")
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
lowerer.provider({
|
ConfigProviderOptionsV1.provider({
|
||||||
apiKey: "secret",
|
apiKey: "secret",
|
||||||
baseURL: "https://openai.example/v1",
|
baseURL: "https://openai.example/v1",
|
||||||
organization: "org",
|
organization: "org",
|
||||||
project: "project",
|
headers: { "x-test": "1", invalid: true },
|
||||||
headers: { "x-test": "1" },
|
|
||||||
body: { store: true },
|
body: { store: true },
|
||||||
timeout: 1000,
|
nested: { camelCase: true },
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
url: "https://openai.example/v1",
|
settings: {
|
||||||
headers: {
|
apiKey: "secret",
|
||||||
Authorization: "Bearer secret",
|
baseURL: "https://openai.example/v1",
|
||||||
"OpenAI-Organization": "org",
|
organization: "org",
|
||||||
"OpenAI-Project": "project",
|
nested: { camelCase: true },
|
||||||
"x-test": "1",
|
|
||||||
},
|
},
|
||||||
|
headers: { "x-test": "1" },
|
||||||
body: { store: true },
|
body: { store: true },
|
||||||
settings: { timeout: 1000 },
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps model and variant options unchanged", () => {
|
||||||
expect(
|
expect(
|
||||||
lowerer.request({
|
ConfigProviderOptionsV1.model({
|
||||||
reasoningEffort: "high",
|
reasoningEffort: "high",
|
||||||
reasoningSummary: "auto",
|
|
||||||
reasoning: { encryptedContent: true },
|
|
||||||
textVerbosity: "low",
|
|
||||||
text: { outputFormat: "plain" },
|
|
||||||
nestedValue: { camelCase: true },
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
reasoning: { encrypted_content: true, effort: "high", summary: "auto" },
|
|
||||||
text: { output_format: "plain", verbosity: "low" },
|
|
||||||
nested_value: { camel_case: true },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("lowers Anthropic provider and request options", () => {
|
|
||||||
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/anthropic")
|
|
||||||
|
|
||||||
expect(
|
|
||||||
lowerer.provider({
|
|
||||||
apiKey: "secret",
|
|
||||||
authToken: "token",
|
|
||||||
baseURL: "https://anthropic.example",
|
|
||||||
headers: { "x-test": "1" },
|
|
||||||
body: { beta: true },
|
|
||||||
generateId: "custom",
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
url: "https://anthropic.example",
|
|
||||||
headers: { "x-api-key": "secret", Authorization: "Bearer token", "x-test": "1" },
|
|
||||||
body: { beta: true },
|
|
||||||
settings: { generateId: "custom" },
|
|
||||||
})
|
|
||||||
expect(
|
|
||||||
lowerer.request({
|
|
||||||
effort: "high",
|
|
||||||
taskBudget: 1024,
|
taskBudget: 1024,
|
||||||
metadata: { userId: "user", traceId: "trace" },
|
metadata: { userId: "user" },
|
||||||
nestedValue: { camelCase: true },
|
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
output_config: { effort: "high", task_budget: 1024 },
|
reasoningEffort: "high",
|
||||||
metadata: { user_id: "user", trace_id: "trace" },
|
taskBudget: 1024,
|
||||||
nested_value: { camel_case: true },
|
metadata: { userId: "user" },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("lowers Google provider and request options", () => {
|
test("uses mechanical lowering for custom provider options", () => {
|
||||||
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/google")
|
expect(ConfigProviderOptionsV1.provider({ enabled: true })).toEqual({
|
||||||
|
settings: { enabled: true },
|
||||||
expect(
|
|
||||||
lowerer.provider({
|
|
||||||
apiKey: "secret",
|
|
||||||
baseURL: "https://google.example",
|
|
||||||
headers: { "x-test": "1" },
|
|
||||||
body: { trace: true },
|
|
||||||
project: "project",
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
url: "https://google.example",
|
|
||||||
headers: { "x-goog-api-key": "secret", "x-test": "1" },
|
|
||||||
body: { trace: true },
|
|
||||||
settings: { project: "project" },
|
|
||||||
})
|
|
||||||
expect(
|
|
||||||
lowerer.request({
|
|
||||||
thinkingConfig: { thinkingBudget: 1024 },
|
|
||||||
responseModalities: ["TEXT"],
|
|
||||||
mediaResolution: "high",
|
|
||||||
imageConfig: { aspectRatio: "16:9" },
|
|
||||||
safetySettings: ["safe"],
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
safetySettings: ["safe"],
|
|
||||||
generationConfig: {
|
|
||||||
thinkingConfig: { thinkingBudget: 1024 },
|
|
||||||
responseModalities: ["TEXT"],
|
|
||||||
mediaResolution: "high",
|
|
||||||
imageConfig: { aspectRatio: "16:9" },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("lowers Azure provider options and uses OpenAI request lowering", () => {
|
|
||||||
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/azure")
|
|
||||||
|
|
||||||
expect(
|
|
||||||
lowerer.provider({
|
|
||||||
apiKey: "secret",
|
|
||||||
baseURL: "https://azure.example",
|
|
||||||
headers: { "x-test": "1" },
|
|
||||||
body: { trace: true },
|
|
||||||
resourceName: "resource",
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
url: "https://azure.example",
|
|
||||||
headers: { "api-key": "secret", "x-test": "1" },
|
|
||||||
body: { trace: true },
|
|
||||||
settings: { resourceName: "resource" },
|
|
||||||
})
|
|
||||||
expect(lowerer.request({ reasoningEffort: "high", reasoningSummary: "auto", textVerbosity: "low" })).toEqual({
|
|
||||||
reasoning: { effort: "high", summary: "auto" },
|
|
||||||
text: { verbosity: "low" },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("lowers Amazon Bedrock provider and request options", () => {
|
|
||||||
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/amazon-bedrock")
|
|
||||||
|
|
||||||
expect(
|
|
||||||
lowerer.provider({
|
|
||||||
headers: { "x-test": "1" },
|
|
||||||
body: { trace: true },
|
|
||||||
region: "us-east-1",
|
|
||||||
profile: "dev",
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
headers: { "x-test": "1" },
|
|
||||||
body: { trace: true },
|
|
||||||
settings: { region: "us-east-1", profile: "dev" },
|
|
||||||
})
|
|
||||||
expect(lowerer.request({ temperature: 0.2 })).toEqual({
|
|
||||||
additionalModelRequestFields: { temperature: 0.2 },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("lowers OpenAI-compatible provider and request options", () => {
|
|
||||||
const lowerer = ConfigProviderOptionsV1.get("@ai-sdk/openai-compatible")
|
|
||||||
|
|
||||||
expect(
|
|
||||||
lowerer.provider({
|
|
||||||
baseURL: "https://compatible.example/v1",
|
|
||||||
headers: { "x-test": "1" },
|
|
||||||
body: { trace: true },
|
|
||||||
apiKey: "secret",
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
url: "https://compatible.example/v1",
|
|
||||||
headers: { "x-test": "1" },
|
|
||||||
body: { trace: true },
|
|
||||||
settings: { apiKey: "secret" },
|
|
||||||
})
|
|
||||||
expect(lowerer.request({ reasoningEffort: "high", serviceTier: "priority" })).toEqual({
|
|
||||||
reasoning_effort: "high",
|
|
||||||
serviceTier: "priority",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test.each([
|
|
||||||
"@ai-sdk/cerebras",
|
|
||||||
"@ai-sdk/deepinfra",
|
|
||||||
"@ai-sdk/groq",
|
|
||||||
"@ai-sdk/mistral",
|
|
||||||
"@ai-sdk/togetherai",
|
|
||||||
"@ai-sdk/xai",
|
|
||||||
"@openrouter/ai-sdk-provider",
|
|
||||||
"ai-gateway-provider",
|
|
||||||
"venice-ai-sdk-provider",
|
|
||||||
])("uses OpenAI-compatible lowering for %s", (packageName) => {
|
|
||||||
const lowerer = ConfigProviderOptionsV1.get(packageName)
|
|
||||||
|
|
||||||
expect(lowerer.provider({ baseURL: "https://example.test", apiKey: "secret" })).toEqual({
|
|
||||||
url: "https://example.test",
|
|
||||||
headers: undefined,
|
headers: undefined,
|
||||||
body: undefined,
|
body: undefined,
|
||||||
settings: { apiKey: "secret" },
|
|
||||||
})
|
})
|
||||||
expect(lowerer.request({ reasoningEffort: "high" })).toEqual({ reasoning_effort: "high" })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test.each(["@ai-sdk/google-vertex", "@ai-sdk/google-vertex/anthropic"])(
|
|
||||||
"uses provider family lowering for %s",
|
|
||||||
(packageName) => {
|
|
||||||
const lowerer = ConfigProviderOptionsV1.get(packageName)
|
|
||||||
|
|
||||||
expect(lowerer.provider({ baseURL: "https://example.test", profile: "dev" })).toMatchObject({
|
|
||||||
url: "https://example.test",
|
|
||||||
settings: { profile: "dev" },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,6 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function request(headers: Record<string, string>, variant?: string) {
|
|
||||||
return {
|
|
||||||
headers,
|
|
||||||
variant,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
|
|
||||||
describe("ConfigProviderPlugin.Plugin", () => {
|
describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
|
|
@ -68,7 +61,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
info: decode({
|
info: decode({
|
||||||
providers: {
|
providers: {
|
||||||
opencode: {
|
opencode: {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
|
package: "aisdk:@ai-sdk/openai",
|
||||||
|
settings: { baseURL: "https://opencode.test/v1" },
|
||||||
models: {
|
models: {
|
||||||
"alpha-gpt-next": {
|
"alpha-gpt-next": {
|
||||||
variants: [
|
variants: [
|
||||||
|
|
@ -119,7 +113,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
info: decode({
|
info: decode({
|
||||||
providers: {
|
providers: {
|
||||||
opencode: {
|
opencode: {
|
||||||
api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" },
|
package: "aisdk:@ai-sdk/openai",
|
||||||
|
settings: { baseURL: "https://opencode.test/v1" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -144,7 +139,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
yield* addPlugin(config)
|
yield* addPlugin(config)
|
||||||
|
|
||||||
const model = required(yield* catalog.model.get(providerID, modelID))
|
const model = required(yield* catalog.model.get(providerID, modelID))
|
||||||
expect(model.variants[0]).toMatchObject({
|
expect(model.variants?.[0]).toMatchObject({
|
||||||
id: "high",
|
id: "high",
|
||||||
body: { reasoningEffort: "high" },
|
body: { reasoningEffort: "high" },
|
||||||
})
|
})
|
||||||
|
|
@ -169,8 +164,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
custom: {
|
custom: {
|
||||||
name: "Configured",
|
name: "Configured",
|
||||||
env: ["CUSTOM_API_KEY"],
|
env: ["CUSTOM_API_KEY"],
|
||||||
api: { type: "native", settings: {} },
|
package: "native",
|
||||||
request: request({ first: "first", shared: "first" }),
|
headers: { first: "first", shared: "first" },
|
||||||
models: {
|
models: {
|
||||||
chat: {
|
chat: {
|
||||||
name: "First",
|
name: "First",
|
||||||
|
|
@ -178,7 +173,8 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
limit: { context: 100, output: 50 },
|
limit: { context: 100, output: 50 },
|
||||||
cost: { input: 1, output: 2 },
|
cost: { input: 1, output: 2 },
|
||||||
request: request({ first: "first", shared: "first" }, "retained"),
|
settings: { retained: true },
|
||||||
|
headers: { first: "first", shared: "first" },
|
||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
id: "fast",
|
id: "fast",
|
||||||
|
|
@ -197,17 +193,18 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
model: "custom/default",
|
model: "custom/default",
|
||||||
providers: {
|
providers: {
|
||||||
custom: {
|
custom: {
|
||||||
api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" },
|
package: "aisdk:custom-sdk",
|
||||||
request: request({ last: "last", shared: "last" }),
|
settings: { baseURL: "https://example.test" },
|
||||||
|
headers: { last: "last", shared: "last" },
|
||||||
models: {
|
models: {
|
||||||
default: {
|
default: {
|
||||||
name: "Default",
|
name: "Default",
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
api: { id: "api-chat" },
|
modelID: "api-chat",
|
||||||
name: "Last",
|
name: "Last",
|
||||||
limit: { output: 75 },
|
limit: { output: 75 },
|
||||||
request: request({ last: "last", shared: "last" }),
|
headers: { last: "last", shared: "last" },
|
||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
id: "fast",
|
id: "fast",
|
||||||
|
|
@ -247,22 +244,24 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||||
})
|
})
|
||||||
expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
|
expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed")
|
||||||
expect(provider.disabled).toBeUndefined()
|
expect(provider.disabled).toBeUndefined()
|
||||||
expect(provider.api).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" })
|
expect(provider.package).toBe("aisdk:custom-sdk")
|
||||||
expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
expect(provider.settings).toEqual({ baseURL: "https://example.test" })
|
||||||
expect(model.api.id).toBe(ModelV2.ID.make("api-chat"))
|
expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
||||||
|
expect(model.id).toBe(modelID)
|
||||||
|
expect(model.modelID).toBe(ModelV2.ID.make("api-chat"))
|
||||||
expect(model.name).toBe("Last")
|
expect(model.name).toBe("Last")
|
||||||
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
|
||||||
expect(model.enabled).toBe(false)
|
expect(model.enabled).toBe(false)
|
||||||
expect(model.limit).toEqual({ context: 100, output: 75 })
|
expect(model.limit).toEqual({ context: 100, output: 75 })
|
||||||
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
|
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
|
||||||
expect(model.request.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
expect(model.settings).toEqual({ baseURL: "https://example.test", retained: true })
|
||||||
expect(model.request.variant).toBe("retained")
|
expect(model.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
||||||
expect(model.variants.map((variant) => variant.id)).toEqual([
|
expect(model.variants?.map((variant) => variant.id)).toEqual([
|
||||||
ModelV2.VariantID.make("fast"),
|
ModelV2.VariantID.make("fast"),
|
||||||
ModelV2.VariantID.make("slow"),
|
ModelV2.VariantID.make("slow"),
|
||||||
])
|
])
|
||||||
expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
expect(model.variants?.[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" })
|
||||||
expect(model.variants[1]?.headers).toEqual({ slow: "slow" })
|
expect(model.variants?.[1]?.headers).toEqual({ slow: "slow" })
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ describe("LocationServiceMap", () => {
|
||||||
providers: {
|
providers: {
|
||||||
unavailable: {
|
unavailable: {
|
||||||
name: "Unavailable",
|
name: "Unavailable",
|
||||||
api: { type: "native", settings: {} },
|
package: "test-provider",
|
||||||
models: { chat: { disabled: true } },
|
models: { chat: { disabled: true } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -306,7 +306,7 @@ describe("LocationServiceMap", () => {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("preserves the selected catalog identity when the api model id differs", () =>
|
it.live("preserves the selected catalog identity when the package model id differs", () =>
|
||||||
Effect.acquireRelease(
|
Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||||
|
|
@ -318,12 +318,12 @@ describe("LocationServiceMap", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((editor) => {
|
yield* catalog.transform((editor) => {
|
||||||
editor.provider.update(ProviderV2.ID.make("aliased"), (provider) => {
|
editor.provider.update(ProviderV2.ID.make("aliased"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@ai-sdk/openai", settings: {} }
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai")
|
||||||
})
|
})
|
||||||
editor.model.update(ProviderV2.ID.make("aliased"), ModelV2.ID.make("fast"), (model) => {
|
editor.model.update(ProviderV2.ID.make("aliased"), ModelV2.ID.make("fast"), (model) => {
|
||||||
// Catalog id and provider API id intentionally differ, like gpt-5.5-fast -> gpt-5.5.
|
// Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5.
|
||||||
model.api = { ...model.api, id: ModelV2.ID.make("base") }
|
model.modelID = ModelV2.ID.make("base")
|
||||||
model.variants.push({ id: ModelV2.VariantID.make("high"), settings: {}, headers: {}, body: {} })
|
model.variants = [{ id: ModelV2.VariantID.make("high") }]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const models = yield* SessionRunnerModel.Service
|
const models = yield* SessionRunnerModel.Service
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
|
||||||
export default define({
|
export default define({
|
||||||
|
|
@ -7,14 +8,11 @@ export default define({
|
||||||
ctx.catalog
|
ctx.catalog
|
||||||
.transform((catalog) => {
|
.transform((catalog) => {
|
||||||
catalog.provider.update("configured", (provider) => {
|
catalog.provider.update("configured", (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
})
|
})
|
||||||
catalog.model.update("configured", "glm-5.2", (model) => {
|
catalog.model.update("configured", "glm-5.2", (model) => {
|
||||||
model.api = {
|
model.modelID = "glm-5.2"
|
||||||
id: "glm-5.2",
|
model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
}
|
|
||||||
model.variants = [
|
model.variants = [
|
||||||
{
|
{
|
||||||
id: "high",
|
id: "high",
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
|
||||||
id: ModelV2.ID.make(current.id),
|
id: ModelV2.ID.make(current.id),
|
||||||
providerID: ProviderV2.ID.make(current.providerID),
|
providerID: ProviderV2.ID.make(current.providerID),
|
||||||
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
|
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
|
||||||
variants: current.variants.map((variant) => ({
|
variants: current.variants?.map((variant) => ({
|
||||||
...variant,
|
...variant,
|
||||||
id: ModelV2.VariantID.make(variant.id),
|
id: ModelV2.VariantID.make(variant.id),
|
||||||
})),
|
})),
|
||||||
|
|
@ -327,35 +327,28 @@ function agentInfo(value: AgentV2.Info) {
|
||||||
function providerInfo(value: ProviderV2.MutableInfo) {
|
function providerInfo(value: ProviderV2.MutableInfo) {
|
||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
|
settings: value.settings && { ...value.settings },
|
||||||
request: {
|
headers: value.headers && { ...value.headers },
|
||||||
settings: { ...value.request.settings },
|
body: value.body && { ...value.body },
|
||||||
headers: { ...value.request.headers },
|
|
||||||
body: { ...value.request.body },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
|
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
|
||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
|
settings: value.settings && { ...value.settings },
|
||||||
|
headers: value.headers && { ...value.headers },
|
||||||
|
body: value.body && { ...value.body },
|
||||||
capabilities: {
|
capabilities: {
|
||||||
...value.capabilities,
|
...value.capabilities,
|
||||||
input: [...value.capabilities.input],
|
input: [...value.capabilities.input],
|
||||||
output: [...value.capabilities.output],
|
output: [...value.capabilities.output],
|
||||||
},
|
},
|
||||||
request: {
|
variants: value.variants?.map((variant) => ({
|
||||||
...value.request,
|
|
||||||
settings: { ...value.request.settings },
|
|
||||||
headers: { ...value.request.headers },
|
|
||||||
body: { ...value.request.body },
|
|
||||||
},
|
|
||||||
variants: value.variants.map((variant) => ({
|
|
||||||
...variant,
|
...variant,
|
||||||
settings: { ...variant.settings },
|
settings: variant.settings && { ...variant.settings },
|
||||||
headers: { ...variant.headers },
|
headers: variant.headers && { ...variant.headers },
|
||||||
body: { ...variant.body },
|
body: variant.body && { ...variant.body },
|
||||||
})),
|
})),
|
||||||
time: { ...value.time },
|
time: { ...value.time },
|
||||||
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
|
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
|
||||||
|
|
|
||||||
|
|
@ -94,16 +94,16 @@ describe("ModelsDevPlugin", () => {
|
||||||
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
|
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
|
||||||
|
|
||||||
expect(base?.variants).toEqual([])
|
expect(base?.variants).toEqual([])
|
||||||
expect(base?.request.body).toEqual({})
|
expect(base?.body).toEqual({})
|
||||||
expect(fast).toMatchObject({
|
expect(fast).toMatchObject({
|
||||||
id: "gpt-5.4-fast",
|
id: "gpt-5.4-fast",
|
||||||
|
modelID: "gpt-5.4",
|
||||||
providerID: "acme",
|
providerID: "acme",
|
||||||
name: "GPT-5.4 Fast",
|
name: "GPT-5.4 Fast",
|
||||||
api: { id: "gpt-5.4" },
|
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
|
||||||
request: {
|
settings: { baseURL: "https://api.acme.test/v1" },
|
||||||
headers: { "x-mode": "fast" },
|
headers: { "x-mode": "fast" },
|
||||||
body: { service_tier: "priority" },
|
body: { service_tier: "priority" },
|
||||||
},
|
|
||||||
variants: [],
|
variants: [],
|
||||||
})
|
})
|
||||||
expect(fast?.cost).toEqual([
|
expect(fast?.cost).toEqual([
|
||||||
|
|
@ -191,7 +191,7 @@ describe("ModelsDevPlugin", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning"))
|
const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning"))
|
||||||
expect(model?.variants.map((variant) => variant.id)).toEqual([
|
expect(model?.variants?.map((variant) => variant.id)).toEqual([
|
||||||
ModelV2.VariantID.make("low"),
|
ModelV2.VariantID.make("low"),
|
||||||
ModelV2.VariantID.make("high"),
|
ModelV2.VariantID.make("high"),
|
||||||
])
|
])
|
||||||
|
|
@ -202,8 +202,6 @@ describe("ModelsDevPlugin", () => {
|
||||||
reasoningSummary: "auto",
|
reasoningSummary: "auto",
|
||||||
include: ["reasoning.encrypted_content"],
|
include: ["reasoning.encrypted_content"],
|
||||||
},
|
},
|
||||||
headers: {},
|
|
||||||
body: {},
|
|
||||||
})
|
})
|
||||||
expect(model?.variants).toContainEqual({
|
expect(model?.variants).toContainEqual({
|
||||||
id: ModelV2.VariantID.make("high"),
|
id: ModelV2.VariantID.make("high"),
|
||||||
|
|
@ -212,20 +210,16 @@ describe("ModelsDevPlugin", () => {
|
||||||
reasoningSummary: "auto",
|
reasoningSummary: "auto",
|
||||||
include: ["reasoning.encrypted_content"],
|
include: ["reasoning.encrypted_content"],
|
||||||
},
|
},
|
||||||
headers: {},
|
|
||||||
body: {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high"))
|
const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high"))
|
||||||
expect(mode).toMatchObject({
|
expect(mode).toMatchObject({
|
||||||
id: "gpt-reasoning-high",
|
id: "gpt-reasoning-high",
|
||||||
name: "GPT Reasoning High",
|
name: "GPT Reasoning High",
|
||||||
request: {
|
headers: { "x-mode": "high" },
|
||||||
headers: { "x-mode": "high" },
|
body: { service_tier: "priority" },
|
||||||
body: { service_tier: "priority" },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
expect(mode?.variants.map((variant) => variant.id)).toEqual([
|
expect(mode?.variants?.map((variant) => variant.id)).toEqual([
|
||||||
ModelV2.VariantID.make("low"),
|
ModelV2.VariantID.make("low"),
|
||||||
ModelV2.VariantID.make("high"),
|
ModelV2.VariantID.make("high"),
|
||||||
])
|
])
|
||||||
|
|
@ -234,22 +228,19 @@ describe("ModelsDevPlugin", () => {
|
||||||
expect(budgetModel?.variants).toContainEqual({
|
expect(budgetModel?.variants).toContainEqual({
|
||||||
id: ModelV2.VariantID.make("high"),
|
id: ModelV2.VariantID.make("high"),
|
||||||
settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
|
settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
|
||||||
headers: {},
|
|
||||||
body: {},
|
|
||||||
})
|
})
|
||||||
expect(budgetModel?.variants).toContainEqual({
|
expect(budgetModel?.variants).toContainEqual({
|
||||||
id: ModelV2.VariantID.make("max"),
|
id: ModelV2.VariantID.make("max"),
|
||||||
settings: { thinking: { type: "enabled", budgetTokens: 64000 } },
|
settings: { thinking: { type: "enabled", budgetTokens: 64000 } },
|
||||||
headers: {},
|
|
||||||
body: {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const anthropicEffortModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-effort"))
|
const anthropicEffortModel = yield* catalog.model.get(
|
||||||
|
ProviderV2.ID.anthropic,
|
||||||
|
ModelV2.ID.make("claude-effort"),
|
||||||
|
)
|
||||||
expect(anthropicEffortModel?.variants).toContainEqual({
|
expect(anthropicEffortModel?.variants).toContainEqual({
|
||||||
id: ModelV2.VariantID.make("low"),
|
id: ModelV2.VariantID.make("low"),
|
||||||
settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
|
settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" },
|
||||||
headers: {},
|
|
||||||
body: {},
|
|
||||||
})
|
})
|
||||||
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
|
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
|
||||||
(previous) =>
|
(previous) =>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ describe("AlibabaPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
||||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("qwen"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/alibaba",
|
package: "@ai-sdk/alibaba",
|
||||||
options: { name: "alibaba" },
|
options: { name: "alibaba" },
|
||||||
|
|
@ -45,7 +46,8 @@ describe("AlibabaPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
||||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("qwen"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "alibaba" },
|
options: { name: "alibaba" },
|
||||||
|
|
@ -62,7 +64,8 @@ describe("AlibabaPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
|
||||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("qwen"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/alibaba",
|
package: "@ai-sdk/alibaba",
|
||||||
options: { name: "custom-alibaba", apiKey: "test" },
|
options: { name: "custom-alibaba", apiKey: "test" },
|
||||||
|
|
@ -74,17 +77,18 @@ describe("AlibabaPlugin", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("uses the old default languageModel(api.id) behavior", () =>
|
it.effect("uses the default languageModel(modelID) behavior", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const item = ModelV2.Info.make({
|
const item = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("qwen-plus"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
})
|
})
|
||||||
const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
|
const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
|
||||||
const language = result.sdk?.languageModel(item.api.id)
|
const language = result.sdk?.languageModel(item.modelID ?? item.id)
|
||||||
expect(language?.modelId).toBe("qwen-plus")
|
expect(language?.modelId).toBe("qwen-plus")
|
||||||
expect(language?.provider).toBe("alibaba.chat")
|
expect(language?.provider).toBe("alibaba.chat")
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -79,31 +79,24 @@ function openAIUrl(language: unknown, path: string, modelId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("AmazonBedrockPlugin", () => {
|
describe("AmazonBedrockPlugin", () => {
|
||||||
it.effect("moves endpoint option to api URL", () =>
|
it.effect("moves endpoint setting to baseURL", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const bedrock = ProviderV2.Info.make({
|
const bedrock = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
|
...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"),
|
||||||
request: {
|
settings: { endpoint: "https://bedrock.example" },
|
||||||
headers: {},
|
|
||||||
body: { endpoint: "https://bedrock.example" },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
catalog.provider.update(bedrock.id, (item) => {
|
catalog.provider.update(bedrock.id, (item) => {
|
||||||
item.api = bedrock.api
|
item.package = bedrock.package
|
||||||
item.request = { settings: {}, headers: {}, body: { endpoint: "https://bedrock.example" } }
|
item.settings = { endpoint: "https://bedrock.example" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
|
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
|
||||||
expect(result.api).toEqual({
|
expect(result.package).toBe(ProviderV2.aisdk("@ai-sdk/amazon-bedrock"))
|
||||||
type: "aisdk",
|
expect(result.settings).toEqual({ baseURL: "https://bedrock.example" })
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
|
||||||
url: "https://bedrock.example",
|
|
||||||
})
|
|
||||||
expect(result.request.body.endpoint).toBeUndefined()
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -116,7 +109,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -141,7 +135,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -175,11 +170,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: { name: "amazon-bedrock" },
|
options: { name: "amazon-bedrock" },
|
||||||
|
|
@ -199,7 +191,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: { name: "amazon-bedrock", region: "eu-west-1" },
|
options: { name: "amazon-bedrock", region: "eu-west-1" },
|
||||||
|
|
@ -218,7 +211,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: { name: "amazon-bedrock" },
|
options: { name: "amazon-bedrock" },
|
||||||
|
|
@ -237,7 +231,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: { name: "amazon-bedrock" },
|
options: { name: "amazon-bedrock" },
|
||||||
|
|
@ -257,7 +252,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -286,7 +282,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -314,11 +311,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("openai.gpt-5.5"),
|
||||||
id: ModelV2.ID.make("openai.gpt-5.5"),
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/amazon-bedrock/mantle",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock/mantle",
|
package: "@ai-sdk/amazon-bedrock/mantle",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -345,11 +339,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("openai.gpt-5.5"),
|
||||||
id: ModelV2.ID.make("openai.gpt-5.5"),
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/amazon-bedrock/mantle",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
|
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
|
||||||
|
|
@ -357,11 +348,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
|
||||||
id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"),
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/amazon-bedrock/mantle",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { region: "us-east-1" },
|
options: { region: "us-east-1" },
|
||||||
|
|
@ -378,11 +366,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"),
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/amazon-bedrock/anthropic",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock/anthropic",
|
package: "@ai-sdk/amazon-bedrock/anthropic",
|
||||||
options: { name: "amazon-bedrock" },
|
options: { name: "amazon-bedrock" },
|
||||||
|
|
@ -409,11 +394,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/amazon-bedrock",
|
package: "@ai-sdk/amazon-bedrock",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -444,7 +426,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -452,7 +435,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "eu-west-1" },
|
options: { region: "eu-west-1" },
|
||||||
|
|
@ -460,11 +444,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
|
||||||
id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "eu-west-1" },
|
options: { region: "eu-west-1" },
|
||||||
|
|
@ -472,7 +453,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "ap-northeast-1" },
|
options: { region: "ap-northeast-1" },
|
||||||
|
|
@ -480,7 +462,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "ap-southeast-2" },
|
options: { region: "ap-southeast-2" },
|
||||||
|
|
@ -505,7 +488,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -591,7 +575,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
|
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
|
||||||
api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make(item.modelID),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: item.region },
|
options: { region: item.region },
|
||||||
|
|
@ -610,7 +595,8 @@ describe("AmazonBedrockPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: { region: "eu-west-1" },
|
options: { region: "eu-west-1" },
|
||||||
|
|
|
||||||
|
|
@ -31,19 +31,19 @@ describe("AnthropicPlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const item = ProviderV2.Info.make({
|
const item = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
|
...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/anthropic" },
|
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
|
||||||
request: { headers: { Existing: "1" }, body: {} },
|
headers: { Existing: "1" },
|
||||||
})
|
})
|
||||||
catalog.provider.update(item.id, (draft) => {
|
catalog.provider.update(item.id, (draft) => {
|
||||||
draft.api = item.api
|
draft.package = item.package
|
||||||
draft.request = { settings: {}, headers: { Existing: "1" }, body: {} }
|
draft.headers = { Existing: "1" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.["anthropic-beta"]).toBe(
|
||||||
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
||||||
)
|
)
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.Existing).toBe("1")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -52,9 +52,7 @@ describe("AnthropicPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {}))
|
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {}))
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined()
|
||||||
required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
|
|
||||||
).toBeUndefined()
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -66,7 +64,8 @@ describe("AnthropicPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/anthropic",
|
package: "@ai-sdk/anthropic",
|
||||||
options: { name: "custom-anthropic", apiKey: "test" },
|
options: { name: "custom-anthropic", apiKey: "test" },
|
||||||
|
|
@ -83,7 +82,8 @@ describe("AnthropicPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
|
package: ProviderV2.aisdk("@ai-sdk/anthropic"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/anthropic",
|
package: "@ai-sdk/anthropic",
|
||||||
options: { name: "anthropic", apiKey: "test" },
|
options: { name: "anthropic", apiKey: "test" },
|
||||||
|
|
|
||||||
|
|
@ -66,18 +66,16 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
|
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
|
||||||
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
item.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
|
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
|
||||||
expect(result.api).toEqual({
|
expect(result).toMatchObject({
|
||||||
type: "aisdk",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
package: "@ai-sdk/openai-compatible",
|
settings: { baseURL: "https://cognitive.cognitiveservices.azure.com/openai" },
|
||||||
url: "https://cognitive.cognitiveservices.azure.com/openai",
|
|
||||||
})
|
})
|
||||||
expect(result.request.body.baseURL).toBeUndefined()
|
expect(result.settings?.resourceName).toBeUndefined()
|
||||||
expect(result.request.body.resourceName).toBeUndefined()
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -89,26 +87,28 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const azure = ProviderV2.Info.make({
|
const azure = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")),
|
...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
})
|
})
|
||||||
const openai = ProviderV2.Info.make({
|
const openai = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: "aisdk:test-provider",
|
||||||
})
|
})
|
||||||
catalog.provider.update(azure.id, (item) => {
|
catalog.provider.update(azure.id, (item) => {
|
||||||
item.api = azure.api
|
item.package = azure.package
|
||||||
|
item.package = azure.package
|
||||||
})
|
})
|
||||||
catalog.provider.update(openai.id, (item) => {
|
catalog.provider.update(openai.id, (item) => {
|
||||||
item.api = openai.api
|
item.package = openai.package
|
||||||
|
item.package = openai.package
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
|
const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
|
||||||
const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
|
const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
|
||||||
expect(azure.request.body.baseURL).toBeUndefined()
|
expect(azure.settings?.baseURL).toBeUndefined()
|
||||||
expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
|
expect(azure).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible" })
|
||||||
expect(openai.request.body.baseURL).toBeUndefined()
|
expect(openai.settings?.baseURL).toBeUndefined()
|
||||||
expect(openai.api).toEqual({ type: "aisdk", package: "test-provider" })
|
expect(openai).toMatchObject({ package: "aisdk:test-provider" })
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -122,7 +122,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { useCompletionUrls: true },
|
options: { useCompletionUrls: true },
|
||||||
|
|
@ -140,7 +141,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -148,7 +150,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
const ignored = yield* aisdk.runLanguage({
|
const ignored = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -168,7 +171,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
|
||||||
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("messages-deployment"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
|
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -176,7 +180,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
|
||||||
api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("chat-deployment"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
|
sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -184,7 +189,8 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
|
||||||
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("language-deployment"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: sdk.languageModel },
|
sdk: { languageModel: sdk.languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,11 @@ describe("AzurePlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.azure, (item) => {
|
catalog.provider.update(ProviderV2.ID.azure, (item) => {
|
||||||
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
|
item.package = ProviderV2.aisdk("@ai-sdk/azure")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -82,18 +82,18 @@ describe("AzurePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const azure = ProviderV2.Info.make({
|
const azure = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
package: ProviderV2.aisdk("@ai-sdk/azure"),
|
||||||
request: { headers: {}, body: { resourceName: "from-config" } },
|
settings: { resourceName: "from-config" },
|
||||||
})
|
})
|
||||||
catalog.provider.update(azure.id, (item) => {
|
catalog.provider.update(azure.id, (item) => {
|
||||||
item.api = azure.api
|
item.package = azure.package
|
||||||
item.request = { settings: {}, headers: {}, body: { resourceName: "from-config" } }
|
item.settings = { resourceName: "from-config" }
|
||||||
})
|
})
|
||||||
catalog.provider.update(ProviderV2.ID.openai, () => {})
|
catalog.provider.update(ProviderV2.ID.openai, () => {})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-config")
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.resourceName).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -105,16 +105,16 @@ describe("AzurePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const azure = ProviderV2.Info.make({
|
const azure = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
package: ProviderV2.aisdk("@ai-sdk/azure"),
|
||||||
request: { headers: {}, body: { resourceName: "" } },
|
settings: { resourceName: "" },
|
||||||
})
|
})
|
||||||
catalog.provider.update(azure.id, (item) => {
|
catalog.provider.update(azure.id, (item) => {
|
||||||
item.api = azure.api
|
item.package = azure.package
|
||||||
item.request = { settings: {}, headers: {}, body: { resourceName: "" } }
|
item.settings = { resourceName: "" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -126,16 +126,16 @@ describe("AzurePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const azure = ProviderV2.Info.make({
|
const azure = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
package: ProviderV2.aisdk("@ai-sdk/azure"),
|
||||||
request: { headers: {}, body: { resourceName: " " } },
|
settings: { resourceName: " " },
|
||||||
})
|
})
|
||||||
catalog.provider.update(azure.id, (item) => {
|
catalog.provider.update(azure.id, (item) => {
|
||||||
item.api = azure.api
|
item.package = azure.package
|
||||||
item.request = { settings: {}, headers: {}, body: { resourceName: " " } }
|
item.settings = { resourceName: " " }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -149,7 +149,8 @@ describe("AzurePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/azure",
|
package: "@ai-sdk/azure",
|
||||||
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
|
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
|
||||||
|
|
@ -168,7 +169,8 @@ describe("AzurePlugin", () => {
|
||||||
.runSDK({
|
.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/azure",
|
package: "@ai-sdk/azure",
|
||||||
options: { name: "azure" },
|
options: { name: "azure" },
|
||||||
|
|
@ -188,7 +190,8 @@ describe("AzurePlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { useCompletionUrls: true },
|
options: { useCompletionUrls: true },
|
||||||
|
|
@ -206,7 +209,8 @@ describe("AzurePlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: { useCompletionUrls: true },
|
options: { useCompletionUrls: true },
|
||||||
|
|
@ -224,8 +228,9 @@ describe("AzurePlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
request: { headers: {}, body: { useCompletionUrls: true } },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
|
body: { useCompletionUrls: true },
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -243,7 +248,8 @@ describe("AzurePlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -251,7 +257,8 @@ describe("AzurePlugin", () => {
|
||||||
const ignored = yield* aisdk.runLanguage({
|
const ignored = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
||||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("deployment"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -274,7 +281,8 @@ describe("AzurePlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
|
||||||
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("messages-deployment"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
|
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -282,7 +290,8 @@ describe("AzurePlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
|
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
|
||||||
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("language-deployment"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: make("languageModel") },
|
sdk: { languageModel: make("languageModel") },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,12 @@ describe("CerebrasPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
|
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
|
||||||
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
|
item.package = ProviderV2.aisdk("@ai-sdk/cerebras")
|
||||||
item.request.headers.Existing = "1"
|
item.headers = { ...item.headers, Existing: "1" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.headers).toEqual({
|
||||||
Existing: "1",
|
Existing: "1",
|
||||||
"X-Cerebras-3rd-Party-Integration": "opencode",
|
"X-Cerebras-3rd-Party-Integration": "opencode",
|
||||||
})
|
})
|
||||||
|
|
@ -53,7 +53,7 @@ describe("CerebrasPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
|
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.request.headers).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.headers).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -69,11 +69,8 @@ describe("CerebrasPlugin", () => {
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
),
|
),
|
||||||
api: {
|
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
package: "aisdk:test-provider",
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/cerebras",
|
package: "@ai-sdk/cerebras",
|
||||||
options: { name: "custom-cerebras", apiKey: "test" },
|
options: { name: "custom-cerebras", apiKey: "test" },
|
||||||
|
|
@ -95,11 +92,8 @@ describe("CerebrasPlugin", () => {
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
),
|
),
|
||||||
api: {
|
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
package: "aisdk:test-provider",
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/cerebras",
|
package: "@ai-sdk/cerebras",
|
||||||
options: { name: "configured-cerebras", apiKey: "test" },
|
options: { name: "configured-cerebras", apiKey: "test" },
|
||||||
|
|
@ -120,11 +114,8 @@ describe("CerebrasPlugin", () => {
|
||||||
ProviderV2.ID.make("custom-cerebras"),
|
ProviderV2.ID.make("custom-cerebras"),
|
||||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
),
|
),
|
||||||
api: {
|
modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||||
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
package: "aisdk:test-provider",
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/groq",
|
package: "@ai-sdk/groq",
|
||||||
options: { name: "custom-cerebras", apiKey: "test" },
|
options: { name: "custom-cerebras", apiKey: "test" },
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: { name: "cloudflare-ai-gateway" },
|
options: { name: "cloudflare-ai-gateway" },
|
||||||
|
|
@ -139,7 +140,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -183,7 +185,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -212,7 +215,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -249,7 +253,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -280,7 +285,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: { name: "cloudflare-ai-gateway" },
|
options: { name: "cloudflare-ai-gateway" },
|
||||||
|
|
@ -302,7 +308,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: { name: "cloudflare-ai-gateway" },
|
options: { name: "cloudflare-ai-gateway" },
|
||||||
|
|
@ -325,7 +332,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: { name: "cloudflare-ai-gateway" },
|
options: { name: "cloudflare-ai-gateway" },
|
||||||
|
|
@ -354,7 +362,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
|
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
|
||||||
|
|
@ -380,11 +389,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||||
ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
|
ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
|
||||||
),
|
),
|
||||||
api: {
|
modelID: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
|
||||||
id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
|
package: "aisdk:test-provider",
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "ai-gateway-provider",
|
package: "ai-gateway-provider",
|
||||||
options: { name: "cloudflare-ai-gateway" },
|
options: { name: "cloudflare-ai-gateway" },
|
||||||
|
|
@ -412,7 +418,8 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "cloudflare-ai-gateway" },
|
options: { name: "cloudflare-ai-gateway" },
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "test-provider" }
|
provider.package = ProviderV2.aisdk("test-provider")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
@ -95,15 +95,16 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const sdk = yield* aisdk.runSDK({
|
const sdk = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
api: { id: ModelV2.ID.make("@cf/model"), ...provider.api },
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
|
package: provider.package,
|
||||||
|
settings: provider.settings,
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
|
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
|
||||||
})
|
})
|
||||||
expect(provider.api).toEqual({
|
expect(provider).toMatchObject({
|
||||||
type: "aisdk",
|
package: "aisdk:test-provider",
|
||||||
package: "test-provider",
|
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1" },
|
||||||
url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1",
|
|
||||||
})
|
})
|
||||||
expect(sdk.sdk).toBeDefined()
|
expect(sdk.sdk).toBeDefined()
|
||||||
}),
|
}),
|
||||||
|
|
@ -116,14 +117,14 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
|
provider.package = ProviderV2.aisdk("test-provider")
|
||||||
|
provider.settings = { ...provider.settings, baseURL: "https://proxy.example/v1" }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
|
||||||
type: "aisdk",
|
package: "aisdk:test-provider",
|
||||||
package: "test-provider",
|
settings: { baseURL: "https://proxy.example/v1" },
|
||||||
url: "https://proxy.example/v1",
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -138,12 +139,9 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
id: ModelV2.ID.make("@cf/model"),
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
type: "aisdk",
|
settings: { baseURL: "https://proxy.example/v1" },
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://proxy.example/v1",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
|
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
|
||||||
|
|
@ -159,15 +157,14 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "test-provider" }
|
provider.package = ProviderV2.aisdk("test-provider")
|
||||||
provider.request.body.accountId = "configured-acct"
|
provider.settings = { ...provider.settings, accountId: "configured-acct" }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({
|
||||||
type: "aisdk",
|
package: "aisdk:test-provider",
|
||||||
package: "test-provider",
|
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" },
|
||||||
url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -182,12 +179,9 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
id: ModelV2.ID.make("@cf/model"),
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
type: "aisdk",
|
settings: { baseURL: "https://proxy.example/v1" },
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://proxy.example/v1",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -214,12 +208,9 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
id: ModelV2.ID.make("@cf/model"),
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
type: "aisdk",
|
settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" },
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -243,7 +234,8 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("@cf/api-model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -262,12 +254,9 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("@cf/model"),
|
||||||
id: ModelV2.ID.make("@cf/model"),
|
package: "aisdk:@ai-sdk/anthropic",
|
||||||
type: "aisdk",
|
settings: { baseURL: "https://proxy.example/v1" },
|
||||||
package: "@ai-sdk/anthropic",
|
|
||||||
url: "https://proxy.example/v1",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/anthropic",
|
package: "@ai-sdk/anthropic",
|
||||||
options: { name: "cloudflare-workers-ai" },
|
options: { name: "cloudflare-workers-ai" },
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,8 @@ describe("CoherePlugin", () => {
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
|
||||||
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("command"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "cohere" },
|
options: { name: "cohere" },
|
||||||
|
|
@ -66,7 +67,8 @@ describe("CoherePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
|
||||||
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("command"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/cohere",
|
package: "@ai-sdk/cohere",
|
||||||
options: { name: "cohere" },
|
options: { name: "cohere" },
|
||||||
|
|
@ -83,7 +85,8 @@ describe("CoherePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
|
||||||
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("command-r-plus"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/cohere",
|
package: "@ai-sdk/cohere",
|
||||||
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
|
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
|
||||||
|
|
@ -108,7 +111,8 @@ describe("CoherePlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("command-r-plus"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk,
|
sdk,
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ describe("DeepInfraPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:@ai-sdk/deepinfra",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/deepinfra",
|
package: "@ai-sdk/deepinfra",
|
||||||
options: { name: "deepinfra" },
|
options: { name: "deepinfra" },
|
||||||
|
|
@ -66,7 +67,8 @@ describe("DeepInfraPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:@ai-sdk/deepinfra",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/deepinfra",
|
package: "@ai-sdk/deepinfra",
|
||||||
options: { name: "custom-deepinfra", apiKey: "test" },
|
options: { name: "custom-deepinfra", apiKey: "test" },
|
||||||
|
|
@ -85,7 +87,8 @@ describe("DeepInfraPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:@ai-sdk/deepinfra",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/deepinfra",
|
package: "@ai-sdk/deepinfra",
|
||||||
options: { name: "deepinfra", apiKey: "test" },
|
options: { name: "deepinfra", apiKey: "test" },
|
||||||
|
|
@ -111,7 +114,8 @@ describe("DeepInfraPlugin", () => {
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:@ai-sdk/deepinfra",
|
||||||
}),
|
}),
|
||||||
package: item,
|
package: item,
|
||||||
options: { name: "deepinfra" },
|
options: { name: "deepinfra" },
|
||||||
|
|
@ -122,7 +126,8 @@ describe("DeepInfraPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:@ai-sdk/deepinfra",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/deepinfra",
|
package: "@ai-sdk/deepinfra",
|
||||||
options: { name: "deepinfra" },
|
options: { name: "deepinfra" },
|
||||||
|
|
@ -141,17 +146,14 @@ describe("DeepInfraPlugin", () => {
|
||||||
const sdkEvent = yield* aisdk.runSDK({
|
const sdkEvent = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
|
||||||
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
|
package: "aisdk:@ai-sdk/deepinfra",
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/deepinfra",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/deepinfra",
|
package: "@ai-sdk/deepinfra",
|
||||||
options: { name: "deepinfra" },
|
options: { name: "deepinfra" },
|
||||||
})
|
})
|
||||||
const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options })
|
const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options })
|
||||||
const language = result.language ?? result.sdk.languageModel(result.model.api.id)
|
const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
|
||||||
expect(language.provider).toBe("deepinfra.chat")
|
expect(language.provider).toBe("deepinfra.chat")
|
||||||
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
|
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ describe("DynamicProviderPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
||||||
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
}),
|
}),
|
||||||
package: fixtureProvider,
|
package: fixtureProvider,
|
||||||
options: { name: "custom", marker: "dynamic" },
|
options: { name: "custom", marker: "dynamic" },
|
||||||
|
|
@ -72,7 +73,8 @@ describe("DynamicProviderPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
||||||
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
}),
|
}),
|
||||||
package: fixtureProvider,
|
package: fixtureProvider,
|
||||||
options: { name: "custom", marker: "dynamic" },
|
options: { name: "custom", marker: "dynamic" },
|
||||||
|
|
@ -89,7 +91,8 @@ describe("DynamicProviderPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
|
||||||
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
}),
|
}),
|
||||||
package: fixtureProvider,
|
package: fixtureProvider,
|
||||||
options: { name: "custom-provider", marker: "dynamic" },
|
options: { name: "custom-provider", marker: "dynamic" },
|
||||||
|
|
@ -105,7 +108,8 @@ describe("DynamicProviderPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
|
||||||
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" },
|
modelID: ModelV2.ID.make("test-model"),
|
||||||
|
package: "aisdk:fixture-provider",
|
||||||
}),
|
}),
|
||||||
package: "fixture-provider",
|
package: "fixture-provider",
|
||||||
options: { name: "npm-provider", marker: "npm" },
|
options: { name: "npm-provider", marker: "npm" },
|
||||||
|
|
@ -122,7 +126,8 @@ describe("DynamicProviderPlugin", () => {
|
||||||
.language(
|
.language(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
|
modelID: ModelV2.ID.make("alias"),
|
||||||
|
package: "aisdk:fixture-provider",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.pipe(Effect.exit)
|
.pipe(Effect.exit)
|
||||||
|
|
@ -139,7 +144,8 @@ describe("DynamicProviderPlugin", () => {
|
||||||
.language(
|
.language(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "file:///missing/provider-factory.js" },
|
modelID: ModelV2.ID.make("alias"),
|
||||||
|
package: "aisdk:file:///missing/provider-factory.js",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.pipe(Effect.exit)
|
.pipe(Effect.exit)
|
||||||
|
|
@ -158,7 +164,8 @@ describe("DynamicProviderPlugin", () => {
|
||||||
.language(
|
.language(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
|
modelID: ModelV2.ID.make("alias"),
|
||||||
|
package: "aisdk:fixture-provider",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.pipe(Effect.exit)
|
.pipe(Effect.exit)
|
||||||
|
|
@ -167,7 +174,7 @@ describe("DynamicProviderPlugin", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
itWithAISDK.effect("uses the model api.id for the default language model", () =>
|
itWithAISDK.effect("uses the model modelID for the default language model", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
|
|
@ -175,7 +182,8 @@ describe("DynamicProviderPlugin", () => {
|
||||||
const language = yield* aisdk.language(
|
const language = yield* aisdk.language(
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("test-model-api"), type: "aisdk", package: fixtureProvider },
|
modelID: ModelV2.ID.make("test-model-api"),
|
||||||
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } })
|
expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } })
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ describe("GatewayPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/gateway",
|
package: "@ai-sdk/gateway",
|
||||||
options: { name: "gateway" },
|
options: { name: "gateway" },
|
||||||
|
|
@ -65,11 +66,8 @@ describe("GatewayPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"),
|
||||||
id: ModelV2.ID.make("anthropic/claude-sonnet-4"),
|
package: "aisdk:test-provider",
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/gateway",
|
package: "@ai-sdk/gateway",
|
||||||
options: { name: "vercel", apiKey: "test-key" },
|
options: { name: "vercel", apiKey: "test-key" },
|
||||||
|
|
@ -91,7 +89,8 @@ describe("GatewayPlugin", () => {
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
|
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
|
||||||
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make(modelID),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/vercel",
|
package: "@ai-sdk/vercel",
|
||||||
options: { name: "vercel" },
|
options: { name: "vercel" },
|
||||||
|
|
@ -101,7 +100,8 @@ describe("GatewayPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
|
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
|
||||||
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make(modelID),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/gateway",
|
package: "@ai-sdk/gateway",
|
||||||
options: { name: "vercel" },
|
options: { name: "vercel" },
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "github-copilot" },
|
options: { name: "github-copilot" },
|
||||||
|
|
@ -55,7 +56,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/github-copilot",
|
package: "@ai-sdk/github-copilot",
|
||||||
options: { name: "github-copilot" },
|
options: { name: "github-copilot" },
|
||||||
|
|
@ -74,7 +76,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -92,7 +95,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -110,7 +114,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -118,7 +123,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5.1-codex"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -126,7 +132,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
|
||||||
api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-4o"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -134,7 +141,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5-mini"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -142,7 +150,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -166,7 +175,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -174,7 +184,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5-mini"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -182,7 +193,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -230,7 +242,8 @@ describe("GithubCopilotPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ describe("GitLabPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "gitlab-ai-provider",
|
package: "gitlab-ai-provider",
|
||||||
options: { name: "gitlab" },
|
options: { name: "gitlab" },
|
||||||
|
|
@ -107,7 +108,8 @@ describe("GitLabPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "gitlab-ai-provider",
|
package: "gitlab-ai-provider",
|
||||||
options: { name: "gitlab" },
|
options: { name: "gitlab" },
|
||||||
|
|
@ -132,7 +134,8 @@ describe("GitLabPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "gitlab-ai-provider",
|
package: "gitlab-ai-provider",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -173,7 +176,8 @@ describe("GitLabPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai",
|
package: "@ai-sdk/openai",
|
||||||
options: { name: "gitlab" },
|
options: { name: "gitlab" },
|
||||||
|
|
@ -192,11 +196,10 @@ describe("GitLabPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
||||||
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("duo-workflow-custom"),
|
||||||
request: {
|
package: "aisdk:test-provider",
|
||||||
headers: {},
|
headers: {},
|
||||||
body: { workflowRef: "ref", workflowDefinition: "definition" },
|
settings: { workflowRef: "ref", workflowDefinition: "definition" },
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sdk: {
|
sdk: {
|
||||||
workflowChat: (id: string, options: unknown) => {
|
workflowChat: (id: string, options: unknown) => {
|
||||||
|
|
@ -227,7 +230,8 @@ describe("GitLabPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
|
||||||
api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("duo-workflow-exact"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: {
|
sdk: {
|
||||||
workflowChat: (id: string, options: unknown) => {
|
workflowChat: (id: string, options: unknown) => {
|
||||||
|
|
@ -245,7 +249,7 @@ describe("GitLabPlugin", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("uses provider feature flags instead of request feature flags", () =>
|
it.effect("uses provider feature flags instead of model settings feature flags", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
|
|
@ -254,11 +258,10 @@ describe("GitLabPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
||||||
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("duo-workflow-custom"),
|
||||||
request: {
|
package: "aisdk:test-provider",
|
||||||
headers: {},
|
headers: {},
|
||||||
body: { featureFlags: { request_flag: true } },
|
settings: { featureFlags: { request_flag: true } },
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sdk: {
|
sdk: {
|
||||||
workflowChat: (id: string, options: unknown) => {
|
workflowChat: (id: string, options: unknown) => {
|
||||||
|
|
@ -282,8 +285,10 @@ describe("GitLabPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude"),
|
||||||
request: { headers: { h: "v" }, body: {} },
|
package: "aisdk:test-provider",
|
||||||
|
headers: { h: "v" },
|
||||||
|
settings: {},
|
||||||
}),
|
}),
|
||||||
sdk: {
|
sdk: {
|
||||||
workflowChat: () => undefined,
|
workflowChat: () => undefined,
|
||||||
|
|
|
||||||
|
|
@ -64,16 +64,16 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
|
provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
expect(
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
|
||||||
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project,
|
"cloud-project",
|
||||||
).toBe("cloud-project")
|
)
|
||||||
expect(
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
|
||||||
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
|
"cloud-location",
|
||||||
).toBe("cloud-location")
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -84,18 +84,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
|
provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic")
|
||||||
provider.request.body.project = "configured-project"
|
provider.settings = { ...provider.settings, project: "configured-project", location: "configured-location" }
|
||||||
provider.request.body.location = "configured-location"
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project).toBe(
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe(
|
||||||
"configured-project",
|
"configured-project",
|
||||||
)
|
)
|
||||||
expect(
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe(
|
||||||
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
|
"configured-location",
|
||||||
).toBe("configured-location")
|
)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -121,7 +120,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
ProviderV2.ID.make("google-vertex-anthropic"),
|
ProviderV2.ID.make("google-vertex-anthropic"),
|
||||||
ModelV2.ID.make("claude-sonnet-4-5"),
|
ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
),
|
),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google-vertex/anthropic",
|
package: "@ai-sdk/google-vertex/anthropic",
|
||||||
options: { name: "google-vertex-anthropic" },
|
options: { name: "google-vertex-anthropic" },
|
||||||
|
|
@ -147,7 +147,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
ProviderV2.ID.make("google-vertex-anthropic"),
|
ProviderV2.ID.make("google-vertex-anthropic"),
|
||||||
ModelV2.ID.make("claude-sonnet-4-5"),
|
ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
),
|
),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google-vertex/anthropic",
|
package: "@ai-sdk/google-vertex/anthropic",
|
||||||
options: { name: "google-vertex-anthropic" },
|
options: { name: "google-vertex-anthropic" },
|
||||||
|
|
@ -167,7 +168,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google-vertex/anthropic",
|
package: "@ai-sdk/google-vertex/anthropic",
|
||||||
options: { name: "google-vertex", project: "project", location: "eu" },
|
options: { name: "google-vertex", project: "project", location: "eu" },
|
||||||
|
|
@ -186,7 +188,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google-vertex/anthropic",
|
package: "@ai-sdk/google-vertex/anthropic",
|
||||||
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
|
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
|
||||||
|
|
@ -204,7 +207,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
const sdkResult = yield* aisdk.runSDK({
|
const sdkResult = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
||||||
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google-vertex/anthropic",
|
package: "@ai-sdk/google-vertex/anthropic",
|
||||||
options: { name: "google-vertex", project: "project", location: "us" },
|
options: { name: "google-vertex", project: "project", location: "us" },
|
||||||
|
|
@ -212,7 +216,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
const languageResult = yield* aisdk.runLanguage({
|
const languageResult = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
||||||
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: sdkResult.sdk,
|
sdk: sdkResult.sdk,
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -234,7 +239,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
||||||
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make(" claude-sonnet-4-5 "),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: selector(calls) },
|
sdk: { languageModel: selector(calls) },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -252,7 +258,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-5"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: selector(calls) },
|
sdk: { languageModel: selector(calls) },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -92,17 +92,14 @@ describe("GoogleVertexPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
|
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://opencode.ai/zen/v1",
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
|
const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
|
||||||
expect(provider.request.body).toEqual({})
|
expect(provider.settings).toEqual({ baseURL: "https://opencode.ai/zen/v1" })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -121,21 +118,24 @@ describe("GoogleVertexPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = {
|
||||||
package: "@ai-sdk/openai-compatible",
|
...provider.settings,
|
||||||
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
baseURL:
|
||||||
|
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||||
expect(provider.request.body.project).toBe("google-cloud-project")
|
expect(provider.settings?.project).toBe("google-cloud-project")
|
||||||
expect(provider.request.body.location).toBe("google-vertex-location")
|
expect(provider.settings?.location).toBe("google-vertex-location")
|
||||||
expect(provider.api).toEqual({
|
expect(provider).toMatchObject({
|
||||||
type: "aisdk",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
package: "@ai-sdk/openai-compatible",
|
settings: {
|
||||||
url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
|
baseURL:
|
||||||
|
"https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -160,10 +160,11 @@ describe("GoogleVertexPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = {
|
||||||
package: "@ai-sdk/openai-compatible",
|
...provider.settings,
|
||||||
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
baseURL:
|
||||||
|
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -172,21 +173,20 @@ describe("GoogleVertexPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
id: ModelV2.ID.make("gemini"),
|
package: "aisdk:@ai-sdk/google-vertex",
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/google-vertex",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google-vertex",
|
package: "@ai-sdk/google-vertex",
|
||||||
options: { name: "google-vertex" },
|
options: { name: "google-vertex" },
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(provider.request.body.project).toBe("vertex-project")
|
expect(provider.settings?.project).toBe("vertex-project")
|
||||||
expect(provider.api).toEqual({
|
expect(provider).toMatchObject({
|
||||||
type: "aisdk",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
package: "@ai-sdk/openai-compatible",
|
settings: {
|
||||||
url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
|
baseURL:
|
||||||
|
"https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(vertexOptions[0].project).toBe("vertex-project")
|
expect(vertexOptions[0].project).toBe("vertex-project")
|
||||||
expect(vertexOptions[0].location).toBe("europe-west4")
|
expect(vertexOptions[0].location).toBe("europe-west4")
|
||||||
|
|
@ -209,23 +209,22 @@ describe("GoogleVertexPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = {
|
||||||
package: "@ai-sdk/openai-compatible",
|
...provider.settings,
|
||||||
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
baseURL:
|
||||||
|
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
||||||
}
|
}
|
||||||
provider.request.body.project = "config-project"
|
provider.settings = { ...provider.settings, project: "config-project", location: "global" }
|
||||||
provider.request.body.location = "global"
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||||
expect(provider.request.body.project).toBe("config-project")
|
expect(provider.settings?.project).toBe("config-project")
|
||||||
expect(provider.request.body.location).toBe("global")
|
expect(provider.settings?.location).toBe("global")
|
||||||
expect(provider.api).toEqual({
|
expect(provider).toMatchObject({
|
||||||
type: "aisdk",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
package: "@ai-sdk/openai-compatible",
|
settings: { baseURL: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global" },
|
||||||
url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global",
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -236,21 +235,20 @@ describe("GoogleVertexPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = {
|
||||||
package: "@ai-sdk/openai-compatible",
|
...provider.settings,
|
||||||
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
baseURL:
|
||||||
|
"https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
|
||||||
}
|
}
|
||||||
provider.request.body.project = "config-project"
|
provider.settings = { ...provider.settings, project: "config-project", location: "eu" }
|
||||||
provider.request.body.location = "eu"
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||||
expect(provider.api).toEqual({
|
expect(provider).toMatchObject({
|
||||||
type: "aisdk",
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
package: "@ai-sdk/openai-compatible",
|
settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" },
|
||||||
url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu",
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -270,14 +268,14 @@ describe("GoogleVertexPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
|
provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex")
|
||||||
provider.request.body.project = "config-project"
|
provider.settings = { ...provider.settings, project: "config-project" }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||||
expect(provider.request.body.project).toBe("config-project")
|
expect(provider.settings?.project).toBe("config-project")
|
||||||
expect(provider.request.body.location).toBe("us-central1")
|
expect(provider.settings?.location).toBe("us-central1")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -297,11 +295,8 @@ describe("GoogleVertexPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
id: ModelV2.ID.make("gemini"),
|
package: "aisdk:@ai-sdk/google-vertex",
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/google-vertex",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google-vertex",
|
package: "@ai-sdk/google-vertex",
|
||||||
options: { name: "google-vertex" },
|
options: { name: "google-vertex" },
|
||||||
|
|
@ -345,11 +340,8 @@ describe("GoogleVertexPlugin", () => {
|
||||||
aisdk.runSDK({
|
aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
id: ModelV2.ID.make("gemini"),
|
package: "aisdk:@ai-sdk/openai-compatible",
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "google-vertex" },
|
options: { name: "google-vertex" },
|
||||||
|
|
@ -359,11 +351,11 @@ describe("GoogleVertexPlugin", () => {
|
||||||
;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch
|
;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
expect(fetchCalls).toHaveLength(1)
|
const vertexCalls = fetchCalls.filter((call) => call.input === "https://vertex.example")
|
||||||
|
expect(vertexCalls).toHaveLength(1)
|
||||||
expect(googleAuthOptions).toEqual([{ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }])
|
expect(googleAuthOptions).toEqual([{ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }])
|
||||||
expect(fetchCalls[0].input).toBe("https://vertex.example")
|
expect(new Headers(vertexCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token")
|
||||||
expect(new Headers(fetchCalls[0].init?.headers).get("authorization")).toBe("Bearer vertex-token")
|
expect(new Headers(vertexCalls[0].init?.headers).get("x-test")).toBe("1")
|
||||||
expect(new Headers(fetchCalls[0].init?.headers).get("x-test")).toBe("1")
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -376,7 +368,8 @@ describe("GoogleVertexPlugin", () => {
|
||||||
yield* aisdk.runLanguage({
|
yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
|
||||||
api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make(" gemini-2.5-pro "),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ describe("GooglePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
|
||||||
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
|
package: "aisdk:@ai-sdk/google",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google",
|
package: "@ai-sdk/google",
|
||||||
options: { name: "custom-google", apiKey: "test" },
|
options: { name: "custom-google", apiKey: "test" },
|
||||||
|
|
@ -45,7 +46,8 @@ describe("GooglePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
|
||||||
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
|
modelID: ModelV2.ID.make("gemini"),
|
||||||
|
package: "aisdk:@ai-sdk/google",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google-vertex",
|
package: "@ai-sdk/google-vertex",
|
||||||
options: { name: "google" },
|
options: { name: "google" },
|
||||||
|
|
@ -62,7 +64,8 @@ describe("GooglePlugin", () => {
|
||||||
const sdkEvent = yield* aisdk.runSDK({
|
const sdkEvent = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" },
|
modelID: ModelV2.ID.make("gemini-api"),
|
||||||
|
package: "aisdk:@ai-sdk/google",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/google",
|
package: "@ai-sdk/google",
|
||||||
options: { name: "custom-google", apiKey: "test" },
|
options: { name: "custom-google", apiKey: "test" },
|
||||||
|
|
@ -72,9 +75,29 @@ describe("GooglePlugin", () => {
|
||||||
sdk: sdkEvent.sdk,
|
sdk: sdkEvent.sdk,
|
||||||
options: sdkEvent.options,
|
options: sdkEvent.options,
|
||||||
})
|
})
|
||||||
const language = result.language ?? result.sdk.languageModel(result.model.api.id)
|
const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id)
|
||||||
expect(language.modelId).toBe("gemini-api")
|
expect(language.modelId).toBe("gemini-api")
|
||||||
expect(language.provider).toBe("custom-google")
|
expect(language.provider).toBe("custom-google")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("wraps AI SDK language models for the native runner", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
|
yield* addPlugin()
|
||||||
|
|
||||||
|
const resolved = yield* aisdk.model(
|
||||||
|
ModelV2.Info.make({
|
||||||
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
|
||||||
|
modelID: ModelV2.ID.make("gemini-api"),
|
||||||
|
package: "aisdk:@ai-sdk/google",
|
||||||
|
settings: { apiKey: "test" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(String(resolved.id)).toBe("gemini-api")
|
||||||
|
expect(String(resolved.provider)).toBe("custom-google")
|
||||||
|
expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/google")
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ describe("GroqPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
||||||
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
|
modelID: ModelV2.ID.make("llama"),
|
||||||
|
package: "aisdk:@ai-sdk/groq",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/groq",
|
package: "@ai-sdk/groq",
|
||||||
options: { name: "groq" },
|
options: { name: "groq" },
|
||||||
|
|
@ -45,7 +46,8 @@ describe("GroqPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
||||||
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
|
modelID: ModelV2.ID.make("llama"),
|
||||||
|
package: "aisdk:@ai-sdk/groq",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "groq" },
|
options: { name: "groq" },
|
||||||
|
|
@ -62,7 +64,8 @@ describe("GroqPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
||||||
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
|
modelID: ModelV2.ID.make("llama"),
|
||||||
|
package: "aisdk:@ai-sdk/groq",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/groq/compat",
|
package: "@ai-sdk/groq/compat",
|
||||||
options: { name: "groq" },
|
options: { name: "groq" },
|
||||||
|
|
@ -79,7 +82,8 @@ describe("GroqPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
|
||||||
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
|
modelID: ModelV2.ID.make("llama"),
|
||||||
|
package: "aisdk:@ai-sdk/groq",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/groq",
|
package: "@ai-sdk/groq",
|
||||||
options: { name: "custom-groq", apiKey: "test" },
|
options: { name: "custom-groq", apiKey: "test" },
|
||||||
|
|
@ -93,7 +97,7 @@ describe("GroqPlugin", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("uses the default languageModel(api.id) behavior", () =>
|
it.effect("uses the default languageModel(modelID) behavior", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
|
|
@ -104,16 +108,13 @@ describe("GroqPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
|
||||||
api: {
|
modelID: ModelV2.ID.make("llama-api"),
|
||||||
id: ModelV2.ID.make("llama-api"),
|
package: "aisdk:@ai-sdk/groq",
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/groq",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sdk,
|
sdk,
|
||||||
options: { name: "groq", apiKey: "test" },
|
options: { name: "groq", apiKey: "test" },
|
||||||
})
|
})
|
||||||
const language = result.language ?? sdk.languageModel(result.model.api.id)
|
const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
|
||||||
expect(language.modelId).toBe("llama-api")
|
expect(language.modelId).toBe("llama-api")
|
||||||
expect(language.provider).toBe("groq.chat")
|
expect(language.provider).toBe("groq.chat")
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -22,27 +22,24 @@ describe("KiloPlugin", () => {
|
||||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.kilo")),
|
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.kilo")),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("applies legacy referer headers only to kilo", () =>
|
it.effect("applies legacy referer headers only to Kilo endpoints", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
provider.headers = { Existing: "value" }
|
||||||
url: "https://api.kilo.ai/api/gateway",
|
|
||||||
}
|
|
||||||
provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
|
|
||||||
})
|
})
|
||||||
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
|
||||||
Existing: "value",
|
Existing: "value",
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
})
|
})
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -51,49 +48,41 @@ describe("KiloPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://api.kilo.ai/api/gateway",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
})
|
})
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty(
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("http-referer")
|
||||||
"http-referer",
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("x-title")
|
||||||
)
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("X-Source")
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("x-title")
|
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("X-Source")
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("uses the legacy provider-id guard instead of endpoint package matching", () =>
|
it.effect("uses endpoint package matching instead of a provider ID guard", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("kilo")
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://api.kilo.ai/api/gateway",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "kilo" }
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
|
provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toBeUndefined()
|
||||||
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.headers).toEqual({
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
})
|
})
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.request.headers).toEqual({})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -34,23 +34,20 @@ describe("LLMGatewayPlugin", () => {
|
||||||
})
|
})
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
provider.headers = { Existing: "value" }
|
||||||
url: "https://api.llmgateway.io/v1",
|
|
||||||
}
|
|
||||||
provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
|
|
||||||
})
|
})
|
||||||
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toEqual({
|
||||||
Existing: "value",
|
Existing: "value",
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
"X-Source": "opencode",
|
"X-Source": "opencode",
|
||||||
})
|
})
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -64,17 +61,14 @@ describe("LLMGatewayPlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
|
||||||
provider.disabled = true
|
provider.disabled = true
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://api.llmgateway.io/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://api.llmgateway.io/v1",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true)
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true)
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ describe("MistralPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
||||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("mistral-large"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/mistral",
|
package: "@ai-sdk/mistral",
|
||||||
options: { name: "mistral" },
|
options: { name: "mistral" },
|
||||||
|
|
@ -45,7 +46,8 @@ describe("MistralPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
||||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("mistral-large"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "mistral" },
|
options: { name: "mistral" },
|
||||||
|
|
@ -68,7 +70,8 @@ describe("MistralPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
||||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("mistral-large"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/mistral",
|
package: "@ai-sdk/mistral",
|
||||||
options: { name: "mistral" },
|
options: { name: "mistral" },
|
||||||
|
|
@ -92,7 +95,8 @@ describe("MistralPlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
|
||||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("mistral-large"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/mistral",
|
package: "@ai-sdk/mistral",
|
||||||
options: { name: "custom-mistral" },
|
options: { name: "custom-mistral" },
|
||||||
|
|
@ -101,7 +105,7 @@ describe("MistralPlugin", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("leaves Mistral language selection on the default sdk.languageModel(api.id) path", () =>
|
it.effect("leaves Mistral language selection on the default sdk.languageModel(modelID) path", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
|
|
@ -116,12 +120,13 @@ describe("MistralPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("mistral-large"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk,
|
sdk,
|
||||||
options: {},
|
options: {},
|
||||||
})
|
})
|
||||||
const language = result.language ?? sdk.languageModel(result.model.api.id)
|
const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id)
|
||||||
expect(calls).toEqual(["languageModel:mistral-large"])
|
expect(calls).toEqual(["languageModel:mistral-large"])
|
||||||
expect(language).toBeDefined()
|
expect(language).toBeDefined()
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -27,23 +27,20 @@ describe("NvidiaPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
provider.headers = { Existing: "value" }
|
||||||
url: "https://integrate.api.nvidia.com/v1",
|
|
||||||
}
|
|
||||||
provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
|
|
||||||
})
|
})
|
||||||
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
|
||||||
Existing: "value",
|
Existing: "value",
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
|
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
|
||||||
})
|
})
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -52,16 +49,13 @@ describe("NvidiaPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://integrate.api.nvidia.com/v1",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
|
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
|
||||||
|
|
@ -74,21 +68,14 @@ describe("NvidiaPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" }
|
||||||
url: "https://integrate.api.nvidia.com/v1",
|
|
||||||
}
|
|
||||||
provider.request = {
|
|
||||||
settings: {},
|
|
||||||
headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
|
|
||||||
body: { baseURL: "https://integrate.api.nvidia.com/v1" },
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
|
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
const defaulted = yield* aisdk.runSDK({
|
const defaulted = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "custom" },
|
options: { name: "custom" },
|
||||||
|
|
@ -35,7 +36,8 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
const disabled = yield* aisdk.runSDK({
|
const disabled = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "custom", includeUsage: false },
|
options: { name: "custom", includeUsage: false },
|
||||||
|
|
@ -53,7 +55,8 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "file:///tmp/@ai-sdk/openai-compatible-provider.js",
|
package: "file:///tmp/@ai-sdk/openai-compatible-provider.js",
|
||||||
options: { name: "custom" },
|
options: { name: "custom" },
|
||||||
|
|
@ -76,7 +79,8 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
yield* aisdk.runSDK({
|
yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "custom-provider", baseURL: "https://example.com/v1" },
|
options: { name: "custom-provider", baseURL: "https://example.com/v1" },
|
||||||
|
|
@ -96,7 +100,8 @@ describe("OpenAICompatiblePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "cloudflare-workers-ai" },
|
options: { name: "cloudflare-workers-ai" },
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,8 @@ describe("OpenAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai",
|
package: "@ai-sdk/openai",
|
||||||
options: { name: "custom-openai", apiKey: "test" },
|
options: { name: "custom-openai", apiKey: "test" },
|
||||||
|
|
@ -99,7 +100,8 @@ describe("OpenAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "openai" },
|
options: { name: "openai" },
|
||||||
|
|
@ -117,7 +119,8 @@ describe("OpenAIPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -136,7 +139,8 @@ describe("OpenAIPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -152,10 +156,10 @@ describe("OpenAIPlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const item = ProviderV2.Info.make({
|
const item = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/openai" },
|
package: ProviderV2.aisdk("@ai-sdk/openai"),
|
||||||
})
|
})
|
||||||
catalog.provider.update(item.id, (draft) => {
|
catalog.provider.update(item.id, (draft) => {
|
||||||
draft.api = item.api
|
draft.package = item.package
|
||||||
})
|
})
|
||||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
|
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
|
||||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||||
|
|
@ -175,10 +179,10 @@ describe("OpenAIPlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const item = ProviderV2.Info.make({
|
const item = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/openai" },
|
package: ProviderV2.aisdk("@ai-sdk/openai"),
|
||||||
})
|
})
|
||||||
catalog.provider.update(item.id, (draft) => {
|
catalog.provider.update(item.id, (draft) => {
|
||||||
draft.api = item.api
|
draft.package = item.package
|
||||||
})
|
})
|
||||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => {
|
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => {
|
||||||
model.cost = [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }]
|
model.cost = [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }]
|
||||||
|
|
@ -220,10 +224,10 @@ describe("OpenAIPlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const item = ProviderV2.Info.make({
|
const item = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
||||||
api: { type: "aisdk", package: "@ai-sdk/openai" },
|
package: ProviderV2.aisdk("@ai-sdk/openai"),
|
||||||
})
|
})
|
||||||
catalog.provider.update(item.id, (draft) => {
|
catalog.provider.update(item.id, (draft) => {
|
||||||
draft.api = item.api
|
draft.package = item.package
|
||||||
})
|
})
|
||||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), () => {})
|
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), () => {})
|
||||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {})
|
catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {})
|
||||||
|
|
@ -248,10 +252,10 @@ describe("OpenAIPlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const item = ProviderV2.Info.make({
|
const item = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
|
...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
catalog.provider.update(item.id, (draft) => {
|
catalog.provider.update(item.id, (draft) => {
|
||||||
draft.api = item.api
|
draft.package = item.package
|
||||||
})
|
})
|
||||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -172,13 +172,10 @@ describe("OpencodePlugin", () => {
|
||||||
expect(provider).toMatchObject({
|
expect(provider).toMatchObject({
|
||||||
name: "Remote",
|
name: "Remote",
|
||||||
integrationID: "opencode",
|
integrationID: "opencode",
|
||||||
api: {
|
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
|
||||||
type: "aisdk",
|
settings: { baseURL: `${server.url.origin}/v1`, custom: "value" },
|
||||||
package: "@ai-sdk/openai-compatible",
|
headers: { "x-org-id": "org" },
|
||||||
url: `${server.url.origin}/v1`,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
expect(provider.request).toEqual({ settings: {}, headers: { "x-org-id": "org" }, body: { custom: "value" } })
|
|
||||||
expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
|
expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
|
||||||
|
|
||||||
const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))
|
const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))
|
||||||
|
|
@ -188,8 +185,10 @@ describe("OpencodePlugin", () => {
|
||||||
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
|
||||||
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
|
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
|
||||||
limit: { context: 1000, output: 100 },
|
limit: { context: 1000, output: 100 },
|
||||||
|
package: ProviderV2.aisdk("@ai-sdk/openai-compatible"),
|
||||||
|
settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 },
|
||||||
|
headers: { "x-org-id": "org" },
|
||||||
})
|
})
|
||||||
expect(model.request.body).toEqual({ custom: "value", temperature: 0.5 })
|
|
||||||
expect(model.variants).toEqual([
|
expect(model.variants).toEqual([
|
||||||
{
|
{
|
||||||
id: ModelV2.VariantID.make("custom"),
|
id: ModelV2.VariantID.make("custom"),
|
||||||
|
|
@ -199,9 +198,8 @@ describe("OpencodePlugin", () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: ModelV2.VariantID.make("high"),
|
id: ModelV2.VariantID.make("high"),
|
||||||
settings: {},
|
settings: { temperature: 0.2 },
|
||||||
headers: {},
|
headers: {},
|
||||||
body: { temperature: 0.2 },
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -221,11 +219,12 @@ describe("OpencodePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const provider = ProviderV2.Info.make({
|
const provider = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("paid"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
})
|
})
|
||||||
catalog.provider.update(provider.id, () => {})
|
catalog.provider.update(provider.id, () => {})
|
||||||
|
|
@ -234,7 +233,7 @@ describe("OpencodePlugin", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
|
||||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
|
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -247,11 +246,12 @@ describe("OpencodePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const provider = ProviderV2.Info.make({
|
const provider = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
|
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
|
||||||
api: { id: ModelV2.ID.make("free"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("free"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(0),
|
cost: cost(0),
|
||||||
})
|
})
|
||||||
catalog.provider.update(provider.id, () => {})
|
catalog.provider.update(provider.id, () => {})
|
||||||
|
|
@ -260,7 +260,7 @@ describe("OpencodePlugin", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
|
||||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
|
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -273,11 +273,12 @@ describe("OpencodePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const provider = ProviderV2.Info.make({
|
const provider = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
|
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
|
||||||
api: { id: ModelV2.ID.make("output-only"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("output-only"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(0, 1),
|
cost: cost(0, 1),
|
||||||
})
|
})
|
||||||
catalog.provider.update(provider.id, () => {})
|
catalog.provider.update(provider.id, () => {})
|
||||||
|
|
@ -286,7 +287,7 @@ describe("OpencodePlugin", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public")
|
||||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(
|
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
@ -301,11 +302,12 @@ describe("OpencodePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const provider = ProviderV2.Info.make({
|
const provider = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("paid"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
})
|
})
|
||||||
catalog.provider.update(provider.id, () => {})
|
catalog.provider.update(provider.id, () => {})
|
||||||
|
|
@ -314,7 +316,7 @@ describe("OpencodePlugin", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
|
||||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -334,11 +336,12 @@ describe("OpencodePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const provider = ProviderV2.Info.make({
|
const provider = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("paid"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
})
|
})
|
||||||
catalog.provider.update(provider.id, () => {})
|
catalog.provider.update(provider.id, () => {})
|
||||||
|
|
@ -347,7 +350,7 @@ describe("OpencodePlugin", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined()
|
||||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -360,27 +363,25 @@ describe("OpencodePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const provider = ProviderV2.Info.make({
|
const provider = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
request: {
|
settings: { apiKey: "configured" },
|
||||||
settings: {},
|
|
||||||
headers: {},
|
|
||||||
body: { apiKey: "configured" },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("paid"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
})
|
})
|
||||||
catalog.provider.update(provider.id, (draft) => {
|
catalog.provider.update(provider.id, (draft) => {
|
||||||
draft.request = { settings: {}, headers: {}, body: { apiKey: "configured" } }
|
draft.package = provider.package
|
||||||
|
draft.settings = { apiKey: "configured" }
|
||||||
})
|
})
|
||||||
catalog.model.update(provider.id, model.id, (draft) => {
|
catalog.model.update(provider.id, model.id, (draft) => {
|
||||||
draft.cost = [...model.cost]
|
draft.cost = [...model.cost]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured")
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("configured")
|
||||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -393,11 +394,12 @@ describe("OpencodePlugin", () => {
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
const provider = ProviderV2.Info.make({
|
const provider = ProviderV2.Info.make({
|
||||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
||||||
api: { type: "aisdk", package: "test-provider" },
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
})
|
})
|
||||||
const model = ModelV2.Info.make({
|
const model = ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("paid"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
cost: cost(1),
|
cost: cost(1),
|
||||||
})
|
})
|
||||||
catalog.provider.update(provider.id, () => {})
|
catalog.provider.update(provider.id, () => {})
|
||||||
|
|
@ -406,7 +408,7 @@ describe("OpencodePlugin", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined()
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.apiKey).toBeUndefined()
|
||||||
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,19 @@ describe("OpenRouterPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
|
provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
|
||||||
provider.request = { settings: {}, headers: { Existing: "value" }, body: {} }
|
provider.headers = { Existing: "value" }
|
||||||
})
|
})
|
||||||
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
|
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toEqual({
|
||||||
Existing: "value",
|
Existing: "value",
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
})
|
})
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -55,7 +55,8 @@ describe("OpenRouterPlugin", () => {
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "openrouter" },
|
options: { name: "openrouter" },
|
||||||
|
|
@ -65,7 +66,8 @@ describe("OpenRouterPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
|
||||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("openai/gpt-5"),
|
||||||
|
package: ProviderV2.aisdk("test-provider"),
|
||||||
}),
|
}),
|
||||||
package: "@openrouter/ai-sdk-provider",
|
package: "@openrouter/ai-sdk-provider",
|
||||||
options: { name: "custom" },
|
options: { name: "custom" },
|
||||||
|
|
@ -79,7 +81,7 @@ describe("OpenRouterPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
|
provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider")
|
||||||
})
|
})
|
||||||
catalog.provider.update(ProviderV2.ID.openai, () => {})
|
catalog.provider.update(ProviderV2.ID.openai, () => {})
|
||||||
catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {})
|
catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {})
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ describe("PerplexityPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
||||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("sonar"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/perplexity",
|
package: "@ai-sdk/perplexity",
|
||||||
options: { name: "perplexity" },
|
options: { name: "perplexity" },
|
||||||
|
|
@ -58,7 +59,8 @@ describe("PerplexityPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
||||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("sonar"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/perplexity-compatible",
|
package: "@ai-sdk/perplexity-compatible",
|
||||||
options: { name: "perplexity" },
|
options: { name: "perplexity" },
|
||||||
|
|
@ -75,7 +77,8 @@ describe("PerplexityPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
||||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("sonar"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/perplexity",
|
package: "@ai-sdk/perplexity",
|
||||||
options: { name: "perplexity" },
|
options: { name: "perplexity" },
|
||||||
|
|
@ -92,7 +95,8 @@ describe("PerplexityPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
|
||||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("sonar"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/perplexity",
|
package: "@ai-sdk/perplexity",
|
||||||
options: { name: "custom-perplexity" },
|
options: { name: "custom-perplexity" },
|
||||||
|
|
@ -110,7 +114,8 @@ describe("PerplexityPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("sonar"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,8 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
|
||||||
function model(providerID: string) {
|
function model(providerID: string) {
|
||||||
return ModelV2.Info.make({
|
return ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
|
||||||
api: { id: ModelV2.ID.make("sap-model"), type: "aisdk", package: fixtureProvider },
|
modelID: ModelV2.ID.make("sap-model"),
|
||||||
|
package: ProviderV2.aisdk(fixtureProvider),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,8 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
|
||||||
api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("gpt-4"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai",
|
package: "@ai-sdk/openai",
|
||||||
options: { name: "openai" },
|
options: { name: "openai" },
|
||||||
|
|
@ -77,7 +78,8 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
||||||
|
|
@ -96,7 +98,8 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -119,7 +122,8 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
||||||
|
|
@ -138,7 +142,8 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -161,7 +166,8 @@ describe("SnowflakeCortexPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")),
|
||||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("claude-sonnet-4-6"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ describe("TogetherAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/togetherai",
|
package: "@ai-sdk/togetherai",
|
||||||
options: { name: "togetherai" },
|
options: { name: "togetherai" },
|
||||||
|
|
@ -59,7 +60,8 @@ describe("TogetherAIPlugin", () => {
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "file:///tmp/@ai-sdk/togetherai-provider.js",
|
package: "file:///tmp/@ai-sdk/togetherai-provider.js",
|
||||||
options: { name: "togetherai" },
|
options: { name: "togetherai" },
|
||||||
|
|
@ -69,7 +71,8 @@ describe("TogetherAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/togetherai",
|
package: "@ai-sdk/togetherai",
|
||||||
options: { name: "togetherai" },
|
options: { name: "togetherai" },
|
||||||
|
|
@ -87,7 +90,8 @@ describe("TogetherAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/togetherai",
|
package: "@ai-sdk/togetherai",
|
||||||
options: { name: "custom-togetherai" },
|
options: { name: "custom-togetherai" },
|
||||||
|
|
@ -110,11 +114,8 @@ describe("TogetherAIPlugin", () => {
|
||||||
ProviderV2.ID.make("togetherai"),
|
ProviderV2.ID.make("togetherai"),
|
||||||
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
|
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
|
||||||
),
|
),
|
||||||
api: {
|
modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
|
||||||
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
|
package: "aisdk:test-provider",
|
||||||
type: "aisdk",
|
|
||||||
package: "test-provider",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -122,7 +123,9 @@ describe("TogetherAIPlugin", () => {
|
||||||
|
|
||||||
expect(result.language).toBeUndefined()
|
expect(result.language).toBeUndefined()
|
||||||
expect(calls).toEqual([])
|
expect(calls).toEqual([])
|
||||||
expect(result.language ?? fakeSelectorSdk(calls).languageModel(result.model.api.id)).toBeDefined()
|
expect(
|
||||||
|
result.language ?? fakeSelectorSdk(calls).languageModel(result.model.modelID ?? result.model.id),
|
||||||
|
).toBeDefined()
|
||||||
expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"])
|
expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ describe("VenicePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "venice-ai-sdk-provider",
|
package: "venice-ai-sdk-provider",
|
||||||
options: { name: "venice" },
|
options: { name: "venice" },
|
||||||
|
|
@ -58,7 +59,8 @@ describe("VenicePlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "venice-ai-sdk-provider",
|
package: "venice-ai-sdk-provider",
|
||||||
options: { name: "custom-venice", apiKey: "test" },
|
options: { name: "custom-venice", apiKey: "test" },
|
||||||
|
|
@ -76,7 +78,8 @@ describe("VenicePlugin", () => {
|
||||||
const similar = yield* aisdk.runSDK({
|
const similar = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "file:///tmp/venice-ai-sdk-provider.js",
|
package: "file:///tmp/venice-ai-sdk-provider.js",
|
||||||
options: { name: "venice" },
|
options: { name: "venice" },
|
||||||
|
|
@ -84,7 +87,8 @@ describe("VenicePlugin", () => {
|
||||||
const other = yield* aisdk.runSDK({
|
const other = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
||||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("model"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: { name: "venice" },
|
options: { name: "venice" },
|
||||||
|
|
@ -103,7 +107,8 @@ describe("VenicePlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" },
|
modelID: ModelV2.ID.make("alias"),
|
||||||
|
package: "aisdk:test-provider",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,12 @@ describe("VercelPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
|
provider.package = ProviderV2.aisdk("@ai-sdk/vercel")
|
||||||
provider.request.headers.Existing = "1"
|
provider.headers = { ...provider.headers, Existing: "1" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).toEqual({
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).toEqual({
|
||||||
Existing: "1",
|
Existing: "1",
|
||||||
"http-referer": "https://opencode.ai/",
|
"http-referer": "https://opencode.ai/",
|
||||||
"x-title": "opencode",
|
"x-title": "opencode",
|
||||||
|
|
@ -43,14 +43,12 @@ describe("VercelPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) =>
|
yield* catalog.transform((catalog) =>
|
||||||
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
|
provider.package = ProviderV2.aisdk("@ai-sdk/vercel")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty(
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer")
|
||||||
"HTTP-Referer",
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title")
|
||||||
)
|
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty("X-Title")
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -62,7 +60,8 @@ describe("VercelPlugin", () => {
|
||||||
const event = yield* aisdk.runSDK({
|
const event = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
|
||||||
api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" },
|
modelID: ModelV2.ID.make("v0-1.0-md"),
|
||||||
|
package: "aisdk:@ai-sdk/vercel",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/vercel",
|
package: "@ai-sdk/vercel",
|
||||||
options: { name: "custom-vercel" },
|
options: { name: "custom-vercel" },
|
||||||
|
|
@ -77,7 +76,7 @@ describe("VercelPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {}))
|
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {}))
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.request.headers).toEqual({})
|
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.headers).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ describe("XAIPlugin", () => {
|
||||||
const ignored = yield* aisdk.runSDK({
|
const ignored = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
||||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/openai-compatible",
|
package: "@ai-sdk/openai-compatible",
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -51,7 +52,8 @@ describe("XAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
||||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/xai",
|
package: "@ai-sdk/xai",
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -71,7 +73,8 @@ describe("XAIPlugin", () => {
|
||||||
const result = yield* aisdk.runSDK({
|
const result = yield* aisdk.runSDK({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
|
||||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
package: "@ai-sdk/xai",
|
package: "@ai-sdk/xai",
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -81,7 +84,7 @@ describe("XAIPlugin", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("uses responses with the model api.id for xAI language models", () =>
|
it.effect("uses responses with the model modelID for xAI language models", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
|
|
@ -91,7 +94,8 @@ describe("XAIPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
|
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
|
||||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
@ -112,7 +116,8 @@ describe("XAIPlugin", () => {
|
||||||
const result = yield* aisdk.runLanguage({
|
const result = yield* aisdk.runLanguage({
|
||||||
model: ModelV2.Info.make({
|
model: ModelV2.Info.make({
|
||||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
|
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
|
||||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
modelID: ModelV2.ID.make("grok-4"),
|
||||||
|
package: "aisdk:@ai-sdk/xai",
|
||||||
}),
|
}),
|
||||||
sdk: fakeSelectorSdk(calls),
|
sdk: fakeSelectorSdk(calls),
|
||||||
options: {},
|
options: {},
|
||||||
|
|
|
||||||
|
|
@ -32,17 +32,14 @@ describe("ZenmuxPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
url: "https://zenmux.ai/api/v1",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
|
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
|
||||||
expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
|
expect(result.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
|
||||||
expect(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
|
expect(Object.keys(required(result.headers)).sort()).toEqual(["HTTP-Referer", "X-Title"])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -51,17 +48,14 @@ describe("ZenmuxPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
provider.headers = { ...provider.headers, Existing: "value" }
|
||||||
url: "https://zenmux.ai/api/v1",
|
|
||||||
}
|
|
||||||
provider.request.headers.Existing = "value"
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
|
||||||
Existing: "value",
|
Existing: "value",
|
||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
|
|
@ -74,17 +68,14 @@ describe("ZenmuxPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
||||||
provider.api = {
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" }
|
||||||
package: "@ai-sdk/openai-compatible",
|
provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
|
||||||
url: "https://zenmux.ai/api/v1",
|
|
||||||
}
|
|
||||||
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({
|
||||||
"HTTP-Referer": "https://example.com/",
|
"HTTP-Referer": "https://example.com/",
|
||||||
"X-Title": "custom-title",
|
"X-Title": "custom-title",
|
||||||
})
|
})
|
||||||
|
|
@ -96,12 +87,12 @@ describe("ZenmuxPlugin", () => {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* catalog.transform((catalog) => {
|
yield* catalog.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
||||||
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
|
provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* addPlugin()
|
yield* addPlugin()
|
||||||
|
|
||||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
|
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).headers).toEqual({
|
||||||
"HTTP-Referer": "https://example.com/",
|
"HTTP-Referer": "https://example.com/",
|
||||||
"X-Title": "custom-title",
|
"X-Title": "custom-title",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,11 @@ describe("VariantPlugin", () => {
|
||||||
const service = yield* Catalog.Service
|
const service = yield* Catalog.Service
|
||||||
yield* service.transform((catalog) => {
|
yield* service.transform((catalog) => {
|
||||||
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
|
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
|
||||||
provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
})
|
})
|
||||||
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
|
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
|
||||||
model.api = {
|
model.modelID = ModelV2.ID.make("glm-5.2")
|
||||||
id: ModelV2.ID.make("glm-5.2"),
|
model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
|
yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) }))
|
||||||
|
|
@ -48,11 +45,8 @@ describe("VariantPlugin", () => {
|
||||||
const service = yield* Catalog.Service
|
const service = yield* Catalog.Service
|
||||||
yield* service.transform((catalog) => {
|
yield* service.transform((catalog) => {
|
||||||
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
|
catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => {
|
||||||
model.api = {
|
model.modelID = ModelV2.ID.make("glm-5.2")
|
||||||
id: ModelV2.ID.make("glm-5.2"),
|
model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible")
|
||||||
type: "aisdk",
|
|
||||||
package: "@ai-sdk/openai-compatible",
|
|
||||||
}
|
|
||||||
model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }]
|
model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { DateTime, Effect, Layer } from "effect"
|
import { DateTime, Effect, Layer } from "effect"
|
||||||
import { Message, Model } from "@opencode-ai/llm"
|
import { Message } from "@opencode-ai/llm"
|
||||||
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
|
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
|
@ -300,7 +299,7 @@ describe("SessionInstructions", () => {
|
||||||
|
|
||||||
test("toLLMMessages does not forward synthetic metadata to the provider", () => {
|
test("toLLMMessages does not forward synthetic metadata to the provider", () => {
|
||||||
const created = DateTime.makeUnsafe(0)
|
const created = DateTime.makeUnsafe(0)
|
||||||
const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route })
|
const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
|
||||||
const synthetic = SessionMessage.Synthetic.make({
|
const synthetic = SessionMessage.Synthetic.make({
|
||||||
id: SessionMessage.ID.make("msg_synthetic"),
|
id: SessionMessage.ID.make("msg_synthetic"),
|
||||||
type: "synthetic",
|
type: "synthetic",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Message, Model } from "@opencode-ai/llm"
|
import { Message } from "@opencode-ai/llm"
|
||||||
import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
|
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
|
|
@ -12,7 +11,7 @@ import { DateTime } from "effect"
|
||||||
|
|
||||||
const created = DateTime.makeUnsafe(0)
|
const created = DateTime.makeUnsafe(0)
|
||||||
const id = (value: string) => SessionMessage.ID.make(`msg_${value}`)
|
const id = (value: string) => SessionMessage.ID.make(`msg_${value}`)
|
||||||
const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route })
|
const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
|
||||||
|
|
||||||
describe("toLLMMessages", () => {
|
describe("toLLMMessages", () => {
|
||||||
test("omits empty assistant turns", () => {
|
test("omits empty assistant turns", () => {
|
||||||
|
|
@ -609,4 +608,35 @@ Recent work
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("preserves provider metadata for a catalog alias with a different API model ID", () => {
|
||||||
|
const messages = toLLMMessages(
|
||||||
|
[
|
||||||
|
SessionMessage.Assistant.make({
|
||||||
|
id: id("assistant-alias"),
|
||||||
|
type: "assistant",
|
||||||
|
agent: "build",
|
||||||
|
model: { id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") },
|
||||||
|
content: [
|
||||||
|
SessionMessage.AssistantReasoning.make({
|
||||||
|
type: "reasoning",
|
||||||
|
id: "reasoning-alias",
|
||||||
|
text: "Visible thought",
|
||||||
|
providerMetadata: { openai: { reasoningEncryptedContent: "encrypted" } },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
time: { created, completed: created },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
ModelV2.Ref.make({ id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(messages[0]?.content).toEqual([
|
||||||
|
{
|
||||||
|
type: "reasoning",
|
||||||
|
text: "Visible thought",
|
||||||
|
providerMetadata: { openai: { reasoningEncryptedContent: "encrypted" } },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { LLM } from "@opencode-ai/llm"
|
import { LLM, Model } from "@opencode-ai/llm"
|
||||||
import { LLMClient } from "@opencode-ai/llm/route"
|
import { LLMClient } from "@opencode-ai/llm/route"
|
||||||
import { DateTime, Effect } from "effect"
|
import { DateTime, Effect } from "effect"
|
||||||
import { Headers } from "effect/unstable/http"
|
import { Headers } from "effect/unstable/http"
|
||||||
|
|
@ -13,28 +13,26 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { it } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
|
|
||||||
type Api =
|
interface ModelOptions {
|
||||||
| {
|
readonly modelID?: string
|
||||||
readonly type: "aisdk"
|
readonly settings?: ModelV2.Info["settings"]
|
||||||
readonly package: string
|
readonly headers?: ModelV2.Info["headers"]
|
||||||
readonly url?: string
|
readonly body?: ModelV2.Info["body"]
|
||||||
readonly settings?: Record<string, unknown>
|
readonly variants?: ModelV2.Info["variants"]
|
||||||
}
|
}
|
||||||
| { readonly type: "native"; readonly url?: string; readonly settings: Record<string, unknown> }
|
|
||||||
|
|
||||||
const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
|
const model = (packageName: string | undefined, options: ModelOptions = {}) =>
|
||||||
ModelV2.Info.make({
|
ModelV2.Info.make({
|
||||||
id: ModelV2.ID.make("test-model"),
|
id: ModelV2.ID.make("test-model"),
|
||||||
|
modelID: ModelV2.ID.make(options.modelID ?? "api-test-model"),
|
||||||
providerID: ProviderV2.ID.make("test-provider"),
|
providerID: ProviderV2.ID.make("test-provider"),
|
||||||
name: "Test model",
|
name: "Test model",
|
||||||
api: { id: ModelV2.ID.make("api-test-model"), ...api },
|
package: packageName,
|
||||||
|
settings: options.settings ?? {},
|
||||||
|
headers: options.headers ?? { "x-test": "header" },
|
||||||
|
body: options.body ?? { custom_extension: { enabled: true } },
|
||||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||||
request: {
|
variants: options.variants ?? [],
|
||||||
settings: {},
|
|
||||||
headers: { "x-test": "header" },
|
|
||||||
body: { apiKey: "secret", custom_extension: { enabled: true } },
|
|
||||||
},
|
|
||||||
variants,
|
|
||||||
time: { released: 0 },
|
time: { released: 0 },
|
||||||
cost: [],
|
cost: [],
|
||||||
status: "active",
|
status: "active",
|
||||||
|
|
@ -43,12 +41,14 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("SessionRunnerModel", () => {
|
describe("SessionRunnerModel", () => {
|
||||||
it.effect("maps catalog OpenAI AI SDK models into native Responses routes", () =>
|
it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
)
|
})
|
||||||
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(catalog)
|
||||||
|
|
||||||
|
expect(catalog.id).toBe(ModelV2.ID.make("test-model"))
|
||||||
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
|
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
|
||||||
expect(resolved.route).toMatchObject({
|
expect(resolved.route).toMatchObject({
|
||||||
id: "openai-responses",
|
id: "openai-responses",
|
||||||
|
|
@ -65,7 +65,9 @@ describe("SessionRunnerModel", () => {
|
||||||
it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
|
it.effect("keeps catalog apiKey credentials out of provider JSON", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
|
settings: { apiKey: "secret", baseURL: "https://openai.example/v1" },
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
|
const prepared = yield* LLMClient.prepare(LLM.request({ model: resolved, prompt: "Hello" }))
|
||||||
|
|
||||||
|
|
@ -77,14 +79,14 @@ describe("SessionRunnerModel", () => {
|
||||||
it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
|
it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
ModelV2.Info.make({
|
model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
|
||||||
...model({
|
settings: {
|
||||||
type: "aisdk",
|
apiKey: "settings-secret",
|
||||||
package: "@ai-sdk/openai-compatible",
|
baseURL: "https://compatible.example/v1",
|
||||||
url: "https://compatible.example/v1",
|
compatibility: "strict",
|
||||||
settings: { apiKey: "settings-secret", compatibility: "strict" },
|
},
|
||||||
}),
|
headers: {},
|
||||||
request: { settings: {}, headers: {}, body: {} },
|
body: {},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const request = LLM.request({ model: resolved, prompt: "Hello" })
|
const request = LLM.request({ model: resolved, prompt: "Hello" })
|
||||||
|
|
@ -97,24 +99,29 @@ describe("SessionRunnerModel", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(headers.authorization).toBe("Bearer settings-secret")
|
expect(headers.authorization).toBe("Bearer settings-secret")
|
||||||
|
expect(resolved.route.id).toBe("openai-compatible-chat")
|
||||||
|
expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
|
||||||
expect(resolved.route.defaults.http?.body).toEqual({})
|
expect(resolved.route.defaults.http?.body).toEqual({})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("overlays selected OpenAI Session variant settings and bodies", () =>
|
it.effect("overlays selected OpenAI Session variant settings and bodies", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }, [
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
{
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
id: ModelV2.VariantID.make("high"),
|
variants: [
|
||||||
settings: { reasoningEffort: "high" },
|
{
|
||||||
headers: { "x-variant": "high" },
|
id: ModelV2.VariantID.make("high"),
|
||||||
body: {
|
settings: { reasoningEffort: "high" },
|
||||||
store: false,
|
headers: { "x-variant": "high" },
|
||||||
service_tier: "priority",
|
body: {
|
||||||
temperature: 0.2,
|
store: false,
|
||||||
|
service_tier: "priority",
|
||||||
|
temperature: 0.2,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
])
|
})
|
||||||
const session = SessionV2.Info.make({
|
const session = SessionV2.Info.make({
|
||||||
id: SessionV2.ID.make("ses_model_variant"),
|
id: SessionV2.ID.make("ses_model_variant"),
|
||||||
projectID: ProjectV2.ID.global,
|
projectID: ProjectV2.ID.global,
|
||||||
|
|
@ -147,9 +154,9 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
|
it.effect("overlays selected OpenAI-compatible Session variant bodies", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model(
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), {
|
||||||
{ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://compatible.example/v1" },
|
settings: { baseURL: "https://compatible.example/v1" },
|
||||||
[
|
variants: [
|
||||||
{
|
{
|
||||||
id: ModelV2.VariantID.make("high"),
|
id: ModelV2.VariantID.make("high"),
|
||||||
settings: {},
|
settings: {},
|
||||||
|
|
@ -157,7 +164,7 @@ describe("SessionRunnerModel", () => {
|
||||||
body: { store: false, reasoning_effort: "high" },
|
body: { store: false, reasoning_effort: "high" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
})
|
||||||
const session = SessionV2.Info.make({
|
const session = SessionV2.Info.make({
|
||||||
id: SessionV2.ID.make("ses_compatible_variant"),
|
id: SessionV2.ID.make("ses_compatible_variant"),
|
||||||
projectID: ProjectV2.ID.global,
|
projectID: ProjectV2.ID.global,
|
||||||
|
|
@ -181,7 +188,9 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
|
it.effect("rejects an explicit unavailable Session variant during model resolution", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" })
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
|
})
|
||||||
const session = SessionV2.Info.make({
|
const session = SessionV2.Info.make({
|
||||||
id: SessionV2.ID.make("ses_model_variant_unavailable"),
|
id: SessionV2.ID.make("ses_model_variant_unavailable"),
|
||||||
projectID: ProjectV2.ID.global,
|
projectID: ProjectV2.ID.global,
|
||||||
|
|
@ -211,14 +220,17 @@ describe("SessionRunnerModel", () => {
|
||||||
|
|
||||||
it.effect("overlays selected Anthropic Session variant settings", () =>
|
it.effect("overlays selected Anthropic Session variant settings", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }, [
|
const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
|
||||||
{
|
settings: { baseURL: "https://anthropic.example/v1" },
|
||||||
id: ModelV2.VariantID.make("high"),
|
variants: [
|
||||||
settings: { thinking: { type: "enabled", budgetTokens: 12000 } },
|
{
|
||||||
headers: {},
|
id: ModelV2.VariantID.make("high"),
|
||||||
body: {},
|
settings: { thinking: { type: "enabled", budgetTokens: 12000 } },
|
||||||
},
|
headers: {},
|
||||||
])
|
body: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
const session = SessionV2.Info.make({
|
const session = SessionV2.Info.make({
|
||||||
id: SessionV2.ID.make("ses_anthropic_variant"),
|
id: SessionV2.ID.make("ses_anthropic_variant"),
|
||||||
projectID: ProjectV2.ID.global,
|
projectID: ProjectV2.ID.global,
|
||||||
|
|
@ -244,7 +256,9 @@ describe("SessionRunnerModel", () => {
|
||||||
it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
|
it.effect("maps catalog Anthropic AI SDK models into native routes", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
model({ type: "aisdk", package: "@ai-sdk/anthropic", url: "https://anthropic.example/v1" }),
|
model(ProviderV2.aisdk("@ai-sdk/anthropic"), {
|
||||||
|
settings: { baseURL: "https://anthropic.example/v1" },
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(resolved.route).toMatchObject({
|
expect(resolved.route).toMatchObject({
|
||||||
|
|
@ -257,9 +271,10 @@ describe("SessionRunnerModel", () => {
|
||||||
it.effect("uses resolved credentials for bearer auth", () =>
|
it.effect("uses resolved credentials for bearer auth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
ModelV2.Info.make({
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
request: { settings: {}, headers: {}, body: {} },
|
headers: {},
|
||||||
|
body: {},
|
||||||
}),
|
}),
|
||||||
Credential.Key.make({ type: "key", key: "secret" }),
|
Credential.Key.make({ type: "key", key: "secret" }),
|
||||||
)
|
)
|
||||||
|
|
@ -280,9 +295,10 @@ describe("SessionRunnerModel", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
|
const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
ModelV2.Info.make({
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" },
|
||||||
request: { settings: {}, headers: {}, body: { apiKey: "configured-secret" } },
|
headers: {},
|
||||||
|
body: {},
|
||||||
}),
|
}),
|
||||||
credential,
|
credential,
|
||||||
)
|
)
|
||||||
|
|
@ -302,9 +318,10 @@ describe("SessionRunnerModel", () => {
|
||||||
it.effect("does not project OAuth account metadata into the request body", () =>
|
it.effect("does not project OAuth account metadata into the request body", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
ModelV2.Info.make({
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
request: { settings: {}, headers: {}, body: {} },
|
headers: {},
|
||||||
|
body: {},
|
||||||
}),
|
}),
|
||||||
Credential.OAuth.make({
|
Credential.OAuth.make({
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
|
|
@ -323,9 +340,10 @@ describe("SessionRunnerModel", () => {
|
||||||
it.effect("routes ChatGPT OAuth credentials to the codex backend", () =>
|
it.effect("routes ChatGPT OAuth credentials to the codex backend", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
ModelV2.Info.make({
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
request: { headers: {}, body: {} },
|
headers: {},
|
||||||
|
body: {},
|
||||||
}),
|
}),
|
||||||
Credential.OAuth.make({
|
Credential.OAuth.make({
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
|
|
@ -354,12 +372,78 @@ describe("SessionRunnerModel", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
|
model("@opencode-ai/llm/providers/openai", {
|
||||||
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
|
}),
|
||||||
|
Credential.OAuth.make({
|
||||||
|
type: "oauth",
|
||||||
|
methodID: Integration.MethodID.make("chatgpt-browser"),
|
||||||
|
access: "chatgpt-token",
|
||||||
|
refresh: "refresh",
|
||||||
|
expires: Date.now() + 60_000,
|
||||||
|
metadata: { accountID: "acct_123" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const headers = yield* resolved.route.auth.apply({
|
||||||
|
request: LLM.request({ model: resolved, prompt: "Hello" }),
|
||||||
|
method: "POST",
|
||||||
|
url: "https://chatgpt.com/backend-api/codex/responses",
|
||||||
|
body: "{}",
|
||||||
|
headers: Headers.empty,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(resolved.route.endpoint.baseURL).toBe("https://chatgpt.com/backend-api/codex")
|
||||||
|
expect(headers.authorization).toBe("Bearer chatgpt-token")
|
||||||
|
expect(headers["chatgpt-account-id"]).toBe("acct_123")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("does not route native OpenAI-compatible packages to the codex backend", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
|
model("@opencode-ai/llm/providers/openai-compatible", {
|
||||||
|
settings: { baseURL: "https://compatible.example/v1" },
|
||||||
|
}),
|
||||||
|
Credential.OAuth.make({
|
||||||
|
type: "oauth",
|
||||||
|
methodID: Integration.MethodID.make("chatgpt-browser"),
|
||||||
|
access: "chatgpt-token",
|
||||||
|
refresh: "refresh",
|
||||||
|
expires: Date.now() + 60_000,
|
||||||
|
metadata: { accountID: "acct_123" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(resolved.route.id).toBe("openai-compatible-chat")
|
||||||
|
expect(resolved.route.endpoint.baseURL).toBe("https://compatible.example/v1")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("maps legacy OpenAI organization and project settings to headers", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
|
settings: { organization: "org_123", project: "proj_123" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(resolved.route.defaults.headers).toMatchObject({
|
||||||
|
"OpenAI-Organization": "org_123",
|
||||||
|
"OpenAI-Project": "proj_123",
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () =>
|
it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
ModelV2.Info.make({
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
request: { headers: {}, body: {} },
|
headers: {},
|
||||||
|
body: {},
|
||||||
}),
|
}),
|
||||||
Credential.OAuth.make({
|
Credential.OAuth.make({
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
|
|
@ -387,9 +471,10 @@ describe("SessionRunnerModel", () => {
|
||||||
it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () =>
|
it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
ModelV2.Info.make({
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
request: { headers: {}, body: {} },
|
headers: {},
|
||||||
|
body: {},
|
||||||
}),
|
}),
|
||||||
Credential.OAuth.make({
|
Credential.OAuth.make({
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
|
|
@ -415,35 +500,106 @@ describe("SessionRunnerModel", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("rejects catalog APIs without a native route", () =>
|
it.effect("loads dynamic native provider packages through the injected package loader", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const native = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
|
model("@opencode-ai/llm/providers/custom", {
|
||||||
|
settings: { region: "test" },
|
||||||
|
headers: { "x-package": "header" },
|
||||||
|
body: { custom: true },
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
loadPackage: (specifier) => {
|
||||||
|
expect(specifier).toBe("@opencode-ai/llm/providers/custom")
|
||||||
|
return Effect.succeed({
|
||||||
|
model: (modelID, settings) => {
|
||||||
|
expect(modelID).toBe("api-test-model")
|
||||||
|
expect(settings).toEqual({
|
||||||
|
region: "test",
|
||||||
|
headers: { "x-package": "header" },
|
||||||
|
body: { custom: true },
|
||||||
|
limits: { context: 100, output: 20 },
|
||||||
|
})
|
||||||
|
return Model.make({ id: modelID, provider: "package-provider", route: native.route })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const native = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
|
model(ProviderV2.aisdk("@ai-sdk/openai"), {
|
||||||
|
settings: { baseURL: "https://openai.example/v1" },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const resolved = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
|
model(ProviderV2.aisdk("@ai-sdk/google"), {
|
||||||
|
modelID: "gemini-api-model",
|
||||||
|
settings: { project: "test" },
|
||||||
|
headers: { "x-aisdk": "header" },
|
||||||
|
body: { custom: true },
|
||||||
|
}),
|
||||||
|
Credential.Key.make({ type: "key", key: "fallback-secret" }),
|
||||||
|
{
|
||||||
|
loadAISDK: (runtime) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
expect(runtime).toMatchObject({
|
||||||
|
id: "test-model",
|
||||||
|
modelID: "gemini-api-model",
|
||||||
|
providerID: "test-provider",
|
||||||
|
package: ProviderV2.aisdk("@ai-sdk/google"),
|
||||||
|
settings: { project: "test", apiKey: "fallback-secret" },
|
||||||
|
headers: { "x-aisdk": "header" },
|
||||||
|
body: { custom: true },
|
||||||
|
})
|
||||||
|
return Model.make({
|
||||||
|
id: runtime.modelID ?? runtime.id,
|
||||||
|
provider: runtime.providerID,
|
||||||
|
route: native.route,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(resolved).toMatchObject({ id: "gemini-api-model", provider: "test-provider" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("rejects AISDK packages without an available loader", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const failure = yield* SessionRunnerModel.fromCatalogModel(
|
const failure = yield* SessionRunnerModel.fromCatalogModel(
|
||||||
model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
|
model(ProviderV2.aisdk("@ai-sdk/google"), {
|
||||||
|
settings: { baseURL: "https://google.example/v1" },
|
||||||
|
}),
|
||||||
).pipe(Effect.flip)
|
).pipe(Effect.flip)
|
||||||
|
|
||||||
expect(failure).toMatchObject({
|
expect(failure).toMatchObject({
|
||||||
_tag: "SessionRunnerModel.UnsupportedApiError",
|
_tag: "SessionRunnerModel.UnsupportedPackageError",
|
||||||
providerID: "test-provider",
|
providerID: "test-provider",
|
||||||
modelID: "test-model",
|
modelID: "test-model",
|
||||||
api: "aisdk:@ai-sdk/google",
|
package: "aisdk:@ai-sdk/google",
|
||||||
})
|
})
|
||||||
expect(failure.message).toBe("Unsupported API for test-provider/test-model: aisdk:@ai-sdk/google")
|
expect(failure.message).toBe("Unsupported package for test-provider/test-model: aisdk:@ai-sdk/google")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("reports whether a catalog model has a supported native route", () =>
|
it.effect("reports whether a catalog model declares a provider package", () =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
expect(
|
expect(SessionRunnerModel.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true)
|
||||||
SessionRunnerModel.supported(
|
expect(SessionRunnerModel.supported(model("@opencode-ai/llm/providers/custom"))).toBe(true)
|
||||||
model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
|
expect(SessionRunnerModel.supported(model(undefined))).toBe(false)
|
||||||
),
|
|
||||||
).toBe(true)
|
|
||||||
expect(
|
|
||||||
SessionRunnerModel.supported(
|
|
||||||
model({ type: "aisdk", package: "@ai-sdk/google", url: "https://google.example/v1" }),
|
|
||||||
),
|
|
||||||
).toBe(false)
|
|
||||||
expect(SessionRunnerModel.supported(model({ type: "native", settings: {} }))).toBe(false)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,8 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||||
[ModelV2.Family, Model.Family],
|
[ModelV2.Family, Model.Family],
|
||||||
[ModelV2.Capabilities, Model.Capabilities],
|
[ModelV2.Capabilities, Model.Capabilities],
|
||||||
[ModelV2.Cost, Model.Cost],
|
[ModelV2.Cost, Model.Cost],
|
||||||
[ModelV2.Api, Model.Api],
|
|
||||||
[ModelV2.Info, Model.Info],
|
[ModelV2.Info, Model.Info],
|
||||||
[ProviderV2.ID, Provider.ID],
|
[ProviderV2.ID, Provider.ID],
|
||||||
[ProviderV2.AISDK, Provider.AISDK],
|
|
||||||
[ProviderV2.Native, Provider.Native],
|
|
||||||
[ProviderV2.Api, Provider.Api],
|
|
||||||
[ProviderV2.Request, Provider.Request],
|
[ProviderV2.Request, Provider.Request],
|
||||||
[ProviderV2.Info, Provider.Info],
|
[ProviderV2.Info, Provider.Info],
|
||||||
[corePermission.Effect, Permission.Effect],
|
[corePermission.Effect, Permission.Effect],
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"./route": "./src/route/index.ts",
|
"./route": "./src/route/index.ts",
|
||||||
"./provider": "./src/provider.ts",
|
"./provider": "./src/provider.ts",
|
||||||
"./providers": "./src/providers/index.ts",
|
"./providers": "./src/providers/index.ts",
|
||||||
|
"./provider-package": "./src/provider-package.ts",
|
||||||
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
|
"./providers/amazon-bedrock": "./src/providers/amazon-bedrock.ts",
|
||||||
"./providers/anthropic": "./src/providers/anthropic.ts",
|
"./providers/anthropic": "./src/providers/anthropic.ts",
|
||||||
"./providers/azure": "./src/providers/azure.ts",
|
"./providers/azure": "./src/providers/azure.ts",
|
||||||
|
|
@ -22,6 +23,8 @@
|
||||||
"./providers/github-copilot": "./src/providers/github-copilot.ts",
|
"./providers/github-copilot": "./src/providers/github-copilot.ts",
|
||||||
"./providers/google": "./src/providers/google.ts",
|
"./providers/google": "./src/providers/google.ts",
|
||||||
"./providers/openai": "./src/providers/openai.ts",
|
"./providers/openai": "./src/providers/openai.ts",
|
||||||
|
"./providers/openai/responses": "./src/providers/openai/responses.ts",
|
||||||
|
"./providers/openai/chat": "./src/providers/openai/chat.ts",
|
||||||
"./providers/openai-compatible": "./src/providers/openai-compatible.ts",
|
"./providers/openai-compatible": "./src/providers/openai-compatible.ts",
|
||||||
"./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
|
"./providers/openai-compatible-profile": "./src/providers/openai-compatible-profile.ts",
|
||||||
"./providers/openrouter": "./src/providers/openrouter.ts",
|
"./providers/openrouter": "./src/providers/openrouter.ts",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export { LLMClient } from "./route/client"
|
export { LLMClient } from "./route/client"
|
||||||
export { Auth } from "./route/auth"
|
export { Auth } from "./route/auth"
|
||||||
export { Provider } from "./provider"
|
export { Provider } from "./provider"
|
||||||
|
export { ProviderPackage } from "./provider-package"
|
||||||
export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
|
export { isContextOverflow, isContextOverflowFailure } from "./provider-error"
|
||||||
export type {
|
export type {
|
||||||
RouteModelInput,
|
RouteModelInput,
|
||||||
|
|
@ -31,3 +32,4 @@ export type {
|
||||||
ModelFactory as ProviderModelFactory,
|
ModelFactory as ProviderModelFactory,
|
||||||
ModelOptions as ProviderModelOptions,
|
ModelOptions as ProviderModelOptions,
|
||||||
} from "./provider"
|
} from "./provider"
|
||||||
|
export type { Definition as ProviderPackageDefinition, Settings as ProviderPackageSettings } from "./provider-package"
|
||||||
|
|
|
||||||
16
packages/llm/src/provider-package.ts
Normal file
16
packages/llm/src/provider-package.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { Model } from "./schema"
|
||||||
|
|
||||||
|
export interface Settings extends Readonly<Record<string, unknown>> {
|
||||||
|
readonly headers?: Readonly<Record<string, string>>
|
||||||
|
readonly body?: Readonly<Record<string, unknown>>
|
||||||
|
readonly limits?: {
|
||||||
|
readonly context: number
|
||||||
|
readonly output: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Definition<ProviderSettings extends Settings = Settings> {
|
||||||
|
readonly model: (modelID: string, settings: ProviderSettings) => Model
|
||||||
|
}
|
||||||
|
|
||||||
|
export * as ProviderPackage from "./provider-package"
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { RouteDefaultsInput } from "../route/client"
|
import type { RouteDefaultsInput } from "../route/client"
|
||||||
import { Auth } from "../route/auth"
|
import { Auth } from "../route/auth"
|
||||||
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import * as BedrockConverse from "../protocols/bedrock-converse"
|
import * as BedrockConverse from "../protocols/bedrock-converse"
|
||||||
import type { BedrockCredentials } from "../protocols/bedrock-converse"
|
import type { BedrockCredentials } from "../protocols/bedrock-converse"
|
||||||
|
|
@ -15,6 +16,15 @@ export type Config = RouteDefaultsInput & {
|
||||||
/** Override the computed `https://bedrock-runtime.<region>.amazonaws.com` URL. */
|
/** Override the computed `https://bedrock-runtime.<region>.amazonaws.com` URL. */
|
||||||
readonly baseURL?: string
|
readonly baseURL?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Settings extends ProviderPackage.Settings {
|
||||||
|
readonly apiKey?: string
|
||||||
|
readonly auth?: "bearer" | "sigv4"
|
||||||
|
readonly baseURL?: string
|
||||||
|
readonly credentials?: BedrockCredentials
|
||||||
|
readonly region?: string
|
||||||
|
readonly topP?: number
|
||||||
|
}
|
||||||
export const routes = [BedrockConverse.route]
|
export const routes = [BedrockConverse.route]
|
||||||
|
|
||||||
const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com`
|
const bedrockBaseURL = (region: string) => `https://bedrock-runtime.${region}.amazonaws.com`
|
||||||
|
|
@ -40,4 +50,19 @@ export const configure = (input: Config = {}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const provider = configure()
|
export const provider = configure()
|
||||||
export const model = provider.model
|
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||||
|
if (settings.auth === "bearer" && settings.apiKey === undefined)
|
||||||
|
throw new Error("Amazon Bedrock bearer auth requires apiKey")
|
||||||
|
if (settings.auth === "sigv4" && settings.apiKey !== undefined)
|
||||||
|
throw new Error("Amazon Bedrock SigV4 auth does not accept apiKey")
|
||||||
|
return configure({
|
||||||
|
apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey,
|
||||||
|
baseURL: settings.baseURL,
|
||||||
|
credentials: settings.credentials,
|
||||||
|
generation: settings.topP === undefined ? undefined : { topP: settings.topP },
|
||||||
|
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
||||||
|
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||||
|
limits: settings.limits,
|
||||||
|
region: settings.region,
|
||||||
|
}).model(modelID)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { RouteDefaultsInput } from "../route/client"
|
import type { RouteDefaultsInput } from "../route/client"
|
||||||
import { Auth } from "../route/auth"
|
import { Auth } from "../route/auth"
|
||||||
import type { ProviderAuthOption } from "../route/auth-options"
|
import type { ProviderAuthOption } from "../route/auth-options"
|
||||||
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import * as AnthropicMessages from "../protocols/anthropic-messages"
|
import * as AnthropicMessages from "../protocols/anthropic-messages"
|
||||||
|
|
||||||
|
|
@ -10,6 +11,12 @@ export const routes = [AnthropicMessages.route]
|
||||||
|
|
||||||
export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
|
export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
|
||||||
|
|
||||||
|
export interface Settings extends ProviderPackage.Settings {
|
||||||
|
readonly apiKey?: string
|
||||||
|
readonly authToken?: string
|
||||||
|
readonly baseURL?: string
|
||||||
|
}
|
||||||
|
|
||||||
const auth = (options: ProviderAuthOption<"optional">) => {
|
const auth = (options: ProviderAuthOption<"optional">) => {
|
||||||
if ("auth" in options && options.auth) return options.auth
|
if ("auth" in options && options.auth) return options.auth
|
||||||
return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
|
return Auth.optional("apiKey" in options ? options.apiKey : undefined, "apiKey")
|
||||||
|
|
@ -32,4 +39,11 @@ export const configure = (input: Config = {}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const provider = configure()
|
export const provider = configure()
|
||||||
export const model = provider.model
|
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||||
|
configure({
|
||||||
|
...(settings.authToken === undefined ? { apiKey: settings.apiKey } : { auth: Auth.bearer(settings.authToken) }),
|
||||||
|
baseURL: settings.baseURL,
|
||||||
|
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
||||||
|
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||||
|
limits: settings.limits,
|
||||||
|
}).model(modelID)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { ProviderID, type ModelID } from "../schema"
|
||||||
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
import * as OpenAICompatibleChat from "../protocols/openai-compatible-chat"
|
||||||
import type { RouteDefaultsInput } from "../route/client"
|
import type { RouteDefaultsInput } from "../route/client"
|
||||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
||||||
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
import { profiles, type OpenAICompatibleProfile } from "./openai-compatible-profile"
|
||||||
|
|
||||||
export const id = ProviderID.make("openai-compatible")
|
export const id = ProviderID.make("openai-compatible")
|
||||||
|
|
@ -12,6 +13,12 @@ type GenericModelOptions = RouteDefaultsInput &
|
||||||
readonly baseURL: string
|
readonly baseURL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Settings extends ProviderPackage.Settings {
|
||||||
|
readonly apiKey?: string
|
||||||
|
readonly baseURL: string
|
||||||
|
readonly provider?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type FamilyModelOptions = RouteDefaultsInput &
|
export type FamilyModelOptions = RouteDefaultsInput &
|
||||||
ProviderAuthOption<"optional"> & {
|
ProviderAuthOption<"optional"> & {
|
||||||
readonly baseURL?: string
|
readonly baseURL?: string
|
||||||
|
|
@ -56,6 +63,16 @@ export const provider = {
|
||||||
configure,
|
configure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||||
|
configure({
|
||||||
|
apiKey: settings.apiKey,
|
||||||
|
baseURL: settings.baseURL,
|
||||||
|
headers: settings.headers === undefined ? undefined : { ...settings.headers },
|
||||||
|
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||||
|
limits: settings.limits,
|
||||||
|
provider: settings.provider,
|
||||||
|
}).model(modelID)
|
||||||
|
|
||||||
export const baseten = define(profiles.baseten)
|
export const baseten = define(profiles.baseten)
|
||||||
export const cerebras = define(profiles.cerebras)
|
export const cerebras = define(profiles.cerebras)
|
||||||
export const deepinfra = define(profiles.deepinfra)
|
export const deepinfra = define(profiles.deepinfra)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
||||||
import type { Route, RouteDefaultsInput } from "../route/client"
|
import type { Route, RouteDefaultsInput } from "../route/client"
|
||||||
|
import type { ProviderPackage } from "../provider-package"
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import * as OpenAIChat from "../protocols/openai-chat"
|
import * as OpenAIChat from "../protocols/openai-chat"
|
||||||
import * as OpenAIResponses from "../protocols/openai-responses"
|
import * as OpenAIResponses from "../protocols/openai-responses"
|
||||||
|
|
@ -21,6 +22,16 @@ export type Config = RouteDefaultsInput &
|
||||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Settings extends ProviderPackage.Settings {
|
||||||
|
readonly apiKey?: string
|
||||||
|
readonly baseURL?: string
|
||||||
|
readonly organization?: string
|
||||||
|
readonly project?: string
|
||||||
|
readonly queryParams?: Readonly<Record<string, string>>
|
||||||
|
readonly transport?: "http" | "websocket"
|
||||||
|
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||||
|
}
|
||||||
|
|
||||||
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
|
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "OPENAI_API_KEY")
|
||||||
|
|
||||||
const defaults = (input: Config) => {
|
const defaults = (input: Config) => {
|
||||||
|
|
@ -57,7 +68,32 @@ export const configure = (input: Config = {}) => {
|
||||||
|
|
||||||
export const provider = configure()
|
export const provider = configure()
|
||||||
|
|
||||||
export const model = provider.model
|
const config = (settings: Settings): Config => {
|
||||||
|
const headers = {
|
||||||
|
...(settings.organization === undefined ? {} : { "OpenAI-Organization": settings.organization }),
|
||||||
|
...(settings.project === undefined ? {} : { "OpenAI-Project": settings.project }),
|
||||||
|
...settings.headers,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
apiKey: settings.apiKey,
|
||||||
|
baseURL: settings.baseURL,
|
||||||
|
headers: Object.keys(headers).length === 0 ? undefined : headers,
|
||||||
|
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
|
||||||
|
limits: settings.limits,
|
||||||
|
providerOptions: settings.providerOptions,
|
||||||
|
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const model: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) => {
|
||||||
|
const configured = configure(config(settings))
|
||||||
|
if (settings.transport === undefined || settings.transport === "http") return configured.responses(modelID)
|
||||||
|
if (settings.transport === "websocket") return configured.responsesWebSocket(modelID)
|
||||||
|
throw new Error(`Unsupported OpenAI Responses transport: ${String(settings.transport)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatModel: ProviderPackage.Definition<Settings>["model"] = (modelID, settings) =>
|
||||||
|
configure(config(settings)).chat(modelID)
|
||||||
export const responses = provider.responses
|
export const responses = provider.responses
|
||||||
export const responsesWebSocket = provider.responsesWebSocket
|
export const responsesWebSocket = provider.responsesWebSocket
|
||||||
export const chat = provider.chat
|
export const chat = provider.chat
|
||||||
|
|
|
||||||
2
packages/llm/src/providers/openai/chat.ts
Normal file
2
packages/llm/src/providers/openai/chat.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { chatModel as model } from "../openai"
|
||||||
|
export type { Settings } from "../openai"
|
||||||
2
packages/llm/src/providers/openai/responses.ts
Normal file
2
packages/llm/src/providers/openai/responses.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { model } from "../openai"
|
||||||
|
export type { Settings } from "../openai"
|
||||||
|
|
@ -30,7 +30,6 @@ describe("public exports", () => {
|
||||||
|
|
||||||
test("provider barrels expose user-facing facades", () => {
|
test("provider barrels expose user-facing facades", () => {
|
||||||
expect(OpenAI.model).toBeFunction()
|
expect(OpenAI.model).toBeFunction()
|
||||||
expect(OpenAI.provider.model).toBe(OpenAI.model)
|
|
||||||
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
||||||
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
|
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
|
||||||
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
|
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue