refactor(core): migrate provider consumers
This commit is contained in:
parent
e40ce1d556
commit
248db536e2
71 changed files with 1313 additions and 1138 deletions
|
|
@ -73,10 +73,10 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
|||
function prepareOptions(model: ModelV2.Info, pkg: string) {
|
||||
const options: Record<string, any> = {
|
||||
name: model.providerID,
|
||||
...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}),
|
||||
...model.request.body,
|
||||
...(model.settings ?? {}),
|
||||
headers: model.headers,
|
||||
body: model.body,
|
||||
}
|
||||
if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url
|
||||
|
||||
const customFetch = options.fetch
|
||||
const chunkTimeout = options.chunkTimeout
|
||||
|
|
@ -195,24 +195,26 @@ export const locationLayer = Layer.effect(
|
|||
runSDK: (event) => run(sdkHooks, event),
|
||||
runLanguage: (event) => run(languageHooks, event),
|
||||
language: Effect.fn("AISDK.language")(function* (model) {
|
||||
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
|
||||
const key = `${model.providerID}/${model.id}/${model.modelID ?? model.id}/${JSON.stringify(model.settings)}`
|
||||
const existing = languages.get(key)
|
||||
if (existing) return existing
|
||||
if (model.api.type !== "aisdk")
|
||||
if (!model.aisdk)
|
||||
return yield* new InitError({
|
||||
providerID: model.providerID,
|
||||
cause: new Error(`Unsupported api ${model.api.type}`),
|
||||
cause: new Error(`Unsupported package ${model.package}`),
|
||||
})
|
||||
|
||||
const options = prepareOptions(model, model.api.package)
|
||||
const options = prepareOptions(model, model.package ?? "")
|
||||
const sdkKey = JSON.stringify({
|
||||
providerID: model.providerID,
|
||||
api: model.api,
|
||||
package: model.package,
|
||||
settings: model.settings,
|
||||
options,
|
||||
})
|
||||
const sdk =
|
||||
sdks.get(sdkKey) ??
|
||||
(yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
|
||||
(yield* service.runSDK({ model, package: model.package ?? "", options }).pipe(initError(model.providerID)))
|
||||
.sdk
|
||||
if (!sdk)
|
||||
return yield* new InitError({
|
||||
providerID: model.providerID,
|
||||
|
|
@ -220,7 +222,7 @@ export const locationLayer = Layer.effect(
|
|||
})
|
||||
sdks.set(sdkKey, sdk)
|
||||
const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
|
||||
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
|
||||
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.modelID ?? model.id)).pipe(
|
||||
initError(model.providerID),
|
||||
)
|
||||
languages.set(key, language)
|
||||
|
|
|
|||
|
|
@ -70,38 +70,22 @@ export const layer = Layer.effect(
|
|||
|
||||
const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
|
||||
if (provider.disabled) return false
|
||||
if (typeof provider.request.body.apiKey === "string") return true
|
||||
if (typeof provider.settings?.apiKey === "string") return true
|
||||
if (integration?.connections.length) return true
|
||||
return provider.integrationID === undefined && !integration
|
||||
}
|
||||
|
||||
const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => {
|
||||
const api =
|
||||
model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0
|
||||
? { ...provider.api, id: model.api.id }
|
||||
: model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url
|
||||
? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } }
|
||||
: model.api.type === "aisdk" && provider.api.type === "aisdk"
|
||||
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
|
||||
: model.api
|
||||
const request = {
|
||||
headers: { ...provider.request.headers, ...model.request.headers },
|
||||
body: { ...provider.request.body, ...model.request.body },
|
||||
variant: model.request.variant,
|
||||
}
|
||||
return ModelV2.Info.make({
|
||||
...model,
|
||||
api,
|
||||
request,
|
||||
package: model.package ?? provider.package,
|
||||
aisdk: model.package === undefined ? (model.aisdk ?? provider.aisdk) : model.aisdk,
|
||||
settings: merge(provider.settings, model.settings),
|
||||
headers: headers(provider.headers, model.headers),
|
||||
body: merge(provider.body, model.body),
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => {
|
||||
if (typeof item.request.body.baseURL !== "string") return
|
||||
item.api.url = item.request.body.baseURL
|
||||
delete item.request.body.baseURL
|
||||
}
|
||||
|
||||
const state = State.create<Data, Draft>({
|
||||
initial: () => ({ providers: new Map() }),
|
||||
draft: (draft) => {
|
||||
|
|
@ -119,7 +103,6 @@ export const layer = Layer.effect(
|
|||
draft.providers.set(providerID, current)
|
||||
}
|
||||
fn(current.provider)
|
||||
normalizeApi(current.provider)
|
||||
},
|
||||
remove: (providerID) => {
|
||||
draft.providers.delete(providerID)
|
||||
|
|
@ -142,7 +125,6 @@ export const layer = Layer.effect(
|
|||
fn(model)
|
||||
model.id = modelID
|
||||
model.providerID = providerID
|
||||
normalizeApi(model)
|
||||
},
|
||||
remove: (providerID, modelID) => {
|
||||
draft.providers.get(providerID)?.models.delete(modelID)
|
||||
|
|
@ -288,6 +270,43 @@ export const layer = Layer.effect(
|
|||
|
||||
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
|
||||
|
||||
function merge(
|
||||
base: Readonly<Record<string, unknown>> | undefined,
|
||||
overlay: Readonly<Record<string, unknown>> | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (base === undefined) return overlay && { ...overlay }
|
||||
if (overlay === undefined) return { ...base }
|
||||
return Object.fromEntries(
|
||||
new Set([...Object.keys(base), ...Object.keys(overlay)]).values().map((key) => {
|
||||
const left = base[key]
|
||||
const right = overlay[key]
|
||||
if (right === undefined) return [key, left]
|
||||
if (plain(left) && plain(right)) return [key, merge(left, right)]
|
||||
return [key, right]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function plain(input: unknown): input is Readonly<Record<string, unknown>> {
|
||||
if (typeof input !== "object" || input === null || Array.isArray(input)) return false
|
||||
const prototype = Object.getPrototypeOf(input)
|
||||
return prototype === Object.prototype || prototype === null
|
||||
}
|
||||
|
||||
function headers(
|
||||
base: Readonly<Record<string, string>> | undefined,
|
||||
overlay: Readonly<Record<string, string>> | undefined,
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
[...Object.entries(base ?? {}), ...Object.entries(overlay ?? {})]
|
||||
.reduce((result, entry) => {
|
||||
result.set(entry[0].toLowerCase(), entry)
|
||||
return result
|
||||
}, new Map<string, [string, string]>())
|
||||
.values(),
|
||||
)
|
||||
}
|
||||
|
||||
export const locationLayer = layer.pipe(
|
||||
Layer.provideMerge(Integration.locationLayer),
|
||||
Layer.provideMerge(Policy.locationLayer),
|
||||
|
|
|
|||
|
|
@ -52,17 +52,22 @@ export const Plugin = define({
|
|||
const providerID = id
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
if (item.api !== undefined) provider.api = { ...item.api }
|
||||
if (item.request !== undefined) {
|
||||
Object.assign(provider.request.headers, item.request.headers)
|
||||
Object.assign(provider.request.body, item.request.body)
|
||||
}
|
||||
if (item.package !== undefined) provider.package = item.package
|
||||
if (item.aisdk !== undefined) provider.aisdk = item.aisdk
|
||||
if (item.settings !== undefined) provider.settings = { ...provider.settings, ...item.settings }
|
||||
if (item.headers !== undefined) provider.headers = { ...provider.headers, ...item.headers }
|
||||
if (item.body !== undefined) provider.body = { ...provider.body, ...item.body }
|
||||
})
|
||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||
catalog.model.update(providerID, id, (model) => {
|
||||
if (config.family !== undefined) model.family = config.family
|
||||
if (config.name !== undefined) model.name = config.name
|
||||
if (config.api !== undefined) model.api = { ...model.api, ...config.api }
|
||||
if (config.modelID !== undefined) model.modelID = config.modelID
|
||||
if (config.package !== undefined) model.package = config.package
|
||||
if (config.aisdk !== undefined) model.aisdk = config.aisdk
|
||||
if (config.settings !== undefined) model.settings = { ...model.settings, ...config.settings }
|
||||
if (config.headers !== undefined) model.headers = { ...model.headers, ...config.headers }
|
||||
if (config.body !== undefined) model.body = { ...model.body, ...config.body }
|
||||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
|
|
@ -70,24 +75,20 @@ export const Plugin = define({
|
|||
output: [...config.capabilities.output],
|
||||
}
|
||||
}
|
||||
if (config.request !== undefined) {
|
||||
Object.assign(model.request.headers, config.request.headers)
|
||||
Object.assign(model.request.body, config.request.body)
|
||||
if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
||||
}
|
||||
if (config.variants !== undefined) {
|
||||
model.variants ??= []
|
||||
for (const variant of config.variants) {
|
||||
let existing = model.variants.find((item) => item.id === variant.id)
|
||||
if (!existing) {
|
||||
existing = {
|
||||
id: variant.id,
|
||||
headers: {},
|
||||
body: {},
|
||||
}
|
||||
model.variants.push(existing)
|
||||
}
|
||||
Object.assign(existing.headers, variant.headers)
|
||||
Object.assign(existing.body, variant.body)
|
||||
if (variant.settings !== undefined)
|
||||
existing.settings = { ...existing.settings, ...variant.settings }
|
||||
if (variant.headers !== undefined) existing.headers = { ...existing.headers, ...variant.headers }
|
||||
if (variant.body !== undefined) existing.body = { ...existing.body, ...variant.body }
|
||||
}
|
||||
}
|
||||
if (config.cost !== undefined) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class Limit extends Schema.Class<Limit>("ConfigV2.Model.Limit")({
|
|||
}) {}
|
||||
|
||||
class Model extends Schema.Class<Model>("ConfigV2.Model")({
|
||||
id: ModelV2.ID.pipe(Schema.optional),
|
||||
modelID: ModelV2.ID.pipe(Schema.optional),
|
||||
family: ModelV2.Family.pipe(Schema.optional),
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
package: Schema.String.pipe(Schema.optional),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { Schema } from "effect"
|
||||
import { Types } from "effect"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { withStatics } from "./schema"
|
||||
import type { DeepMutable } from "./schema"
|
||||
|
||||
export const ID = Model.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
|
@ -10,6 +8,7 @@ export type ID = typeof ID.Type
|
|||
export const VariantID = Model.VariantID
|
||||
export type VariantID = typeof VariantID.Type
|
||||
|
||||
// Grouping of models, eg claude opus, claude sonnet
|
||||
export const Family = Model.Family
|
||||
export type Family = Model.Family
|
||||
|
||||
|
|
@ -21,62 +20,10 @@ export const Cost = Model.Cost
|
|||
export const Ref = Model.Ref
|
||||
export type Ref = typeof Ref.Type
|
||||
|
||||
// Temporary runtime schema until core catalog consumers migrate to the flat
|
||||
// package identity in @opencode-ai/schema.
|
||||
export const Api = Schema.Union([
|
||||
Schema.Struct({ id: ID, ...ProviderV2.AISDK.fields }),
|
||||
Schema.Struct({ id: ID, ...ProviderV2.Native.fields }),
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Api = typeof Api.Type
|
||||
export const Info = Model.Info
|
||||
export type Info = Model.Info
|
||||
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
providerID: ProviderV2.ID,
|
||||
family: Family.pipe(Schema.optional),
|
||||
name: Schema.String,
|
||||
api: Api,
|
||||
capabilities: Capabilities,
|
||||
request: Schema.Struct({
|
||||
...ProviderV2.Request.fields,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
variants: Schema.Struct({
|
||||
id: VariantID,
|
||||
...ProviderV2.Request.fields,
|
||||
}).pipe(Schema.Array, Schema.mutable),
|
||||
time: Schema.Struct({ released: Schema.Finite }),
|
||||
cost: Cost.pipe(Schema.Array, Schema.mutable),
|
||||
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
||||
enabled: Schema.Boolean,
|
||||
limit: Schema.Struct({
|
||||
context: Schema.Int,
|
||||
input: Schema.Int.pipe(Schema.optional),
|
||||
output: Schema.Int,
|
||||
}),
|
||||
})
|
||||
.annotate({ identifier: "ModelV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
empty: (providerID: ProviderV2.ID, modelID: ID) =>
|
||||
schema.make({
|
||||
id: modelID,
|
||||
providerID,
|
||||
name: modelID,
|
||||
api: { id: modelID, type: "native", settings: {} },
|
||||
capabilities: { tools: false, input: [], output: [] },
|
||||
request: { headers: {}, body: {} },
|
||||
variants: [],
|
||||
time: { released: 0 },
|
||||
cost: [],
|
||||
status: "active",
|
||||
enabled: true,
|
||||
limit: { context: 0, output: 0 },
|
||||
}),
|
||||
})),
|
||||
)
|
||||
|
||||
export type MutableInfo = Omit<DeepMutable<Info>, "api"> & { api: ProviderV2.MutableApi<Api> }
|
||||
export type MutableInfo = Types.DeepMutable<Info>
|
||||
|
||||
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
||||
const [providerID, ...modelID] = input.split("/")
|
||||
|
|
|
|||
|
|
@ -75,17 +75,9 @@ export const ModelsDevPlugin = define({
|
|||
const providerID = ProviderV2.ID.make(item.id)
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.name = item.name
|
||||
provider.api = item.npm
|
||||
? {
|
||||
type: "aisdk",
|
||||
package: item.npm,
|
||||
url: item.api,
|
||||
}
|
||||
: {
|
||||
type: "native",
|
||||
url: item.api,
|
||||
settings: {},
|
||||
}
|
||||
provider.package = item.npm ?? ""
|
||||
provider.aisdk = item.npm ? true : undefined
|
||||
provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
|
||||
})
|
||||
|
||||
for (const model of Object.values(item.models)) {
|
||||
|
|
@ -93,19 +85,9 @@ export const ModelsDevPlugin = define({
|
|||
catalog.model.update(providerID, modelID, (draft) => {
|
||||
draft.name = model.name
|
||||
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
|
||||
draft.api = model.provider?.npm
|
||||
? {
|
||||
id: draft.api.id,
|
||||
type: "aisdk",
|
||||
package: model.provider?.npm,
|
||||
url: model.provider.api,
|
||||
}
|
||||
: {
|
||||
id: draft.api.id,
|
||||
type: "native",
|
||||
url: model.provider?.api,
|
||||
settings: {},
|
||||
}
|
||||
draft.package = model.provider?.npm
|
||||
draft.aisdk = model.provider?.npm ? true : undefined
|
||||
draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
|
||||
draft.capabilities = {
|
||||
tools: model.tool_call,
|
||||
input: [...(model.modalities?.input ?? [])],
|
||||
|
|
|
|||
|
|
@ -65,15 +65,14 @@ export const AmazonBedrockPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/amazon-bedrock") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (provider.api.type !== "aisdk") return
|
||||
if (typeof provider.request.body.endpoint !== "string") return
|
||||
if (typeof provider.settings?.endpoint !== "string") return
|
||||
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
|
||||
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
|
||||
provider.api.url = provider.request.body.endpoint
|
||||
delete provider.request.body.endpoint
|
||||
provider.settings.baseURL = provider.settings.endpoint
|
||||
delete provider.settings.endpoint
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
@ -114,12 +113,12 @@ export const AmazonBedrockPlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
|
||||
evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
|
||||
if (evt.model.aisdk && evt.model.package === "@ai-sdk/amazon-bedrock/mantle") {
|
||||
evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
|
||||
return
|
||||
}
|
||||
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
|
||||
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
|
||||
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.modelID ?? evt.model.id, region))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ export const AnthropicPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/anthropic") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/anthropic") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["anthropic-beta"] =
|
||||
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
||||
provider.headers = {
|
||||
...provider.headers,
|
||||
"anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ export const AzurePlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/azure") continue
|
||||
const configured = item.provider.request.body.resourceName
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/azure") continue
|
||||
const configured = item.provider.settings?.resourceName
|
||||
const resourceName =
|
||||
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
|
||||
if (!resourceName) continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.body.resourceName = resourceName
|
||||
provider.settings = { ...provider.settings, resourceName }
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
@ -35,7 +35,7 @@ export const AzurePlugin = define({
|
|||
if (
|
||||
!evt.options.resourceName &&
|
||||
!evt.options.baseURL &&
|
||||
(evt.model.api.type !== "aisdk" || !evt.model.api.url)
|
||||
(!evt.model.aisdk || typeof evt.model.settings?.baseURL !== "string")
|
||||
) {
|
||||
throw new Error(
|
||||
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
|
||||
|
|
@ -49,7 +49,11 @@ export const AzurePlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
||||
evt.language = selectLanguage(
|
||||
evt.sdk,
|
||||
evt.model.modelID ?? evt.model.id,
|
||||
Boolean(evt.options.useCompletionUrls),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
@ -63,11 +67,14 @@ export const AzureCognitiveServicesPlugin = define({
|
|||
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||
if (!resourceName) return
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (!item.provider.id.includes("azure-cognitive-services")) continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
|
||||
provider.settings = {
|
||||
...provider.settings,
|
||||
baseURL: `https://${resourceName}.cognitiveservices.azure.com/openai`,
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
@ -75,7 +82,11 @@ export const AzureCognitiveServicesPlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
||||
evt.language = selectLanguage(
|
||||
evt.sdk,
|
||||
evt.model.modelID ?? evt.model.id,
|
||||
Boolean(evt.options.useCompletionUrls),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ export const CerebrasPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/cerebras") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/cerebras") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
|
||||
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ export const CloudflareWorkersAIPlugin = define({
|
|||
const item = evt.provider.get(providerID)
|
||||
if (!item) return
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (provider.api.type !== "aisdk") return
|
||||
if (provider.api.url) return
|
||||
const accountId = resolveAccountId(provider.request.body)
|
||||
if (accountId) provider.api.url = workersEndpoint(accountId)
|
||||
if (!provider.aisdk) return
|
||||
if (typeof provider.settings?.baseURL === "string") return
|
||||
const accountId = resolveAccountId(provider.settings ?? {})
|
||||
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
|
@ -27,7 +27,7 @@ export const CloudflareWorkersAIPlugin = define({
|
|||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
|
||||
const accountId = resolveAccountId(evt.options)
|
||||
if (!hasWorkersEndpoint(evt.model.api) && !accountId) return
|
||||
if (!hasWorkersEndpoint(evt.model) && !accountId) return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
||||
evt.sdk = mod.createOpenAICompatible(
|
||||
sdkOptions({
|
||||
|
|
@ -40,7 +40,7 @@ export const CloudflareWorkersAIPlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
||||
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
@ -54,8 +54,8 @@ function workersEndpoint(accountId: string) {
|
|||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
|
||||
}
|
||||
|
||||
function hasWorkersEndpoint(api: ProviderV2.Api) {
|
||||
return api.type === "aisdk" && Boolean(api.url)
|
||||
function hasWorkersEndpoint(model: { readonly aisdk?: true; readonly settings?: Readonly<Record<string, unknown>> }) {
|
||||
return model.aisdk && typeof model.settings?.baseURL === "string"
|
||||
}
|
||||
|
||||
function sdkOptions(options: Record<string, any>) {
|
||||
|
|
|
|||
|
|
@ -36,12 +36,11 @@ export const GithubCopilotPlugin = define({
|
|||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
||||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
||||
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
|
||||
return
|
||||
}
|
||||
evt.language = shouldUseResponses(evt.model.api.id)
|
||||
? evt.sdk.responses(evt.model.api.id)
|
||||
: evt.sdk.chat(evt.model.api.id)
|
||||
const id = evt.model.modelID ?? evt.model.id
|
||||
evt.language = shouldUseResponses(id) ? evt.sdk.responses(id) : evt.sdk.chat(id)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -36,26 +36,24 @@ export const GitLabPlugin = define({
|
|||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||
const featureFlags =
|
||||
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
|
||||
if (evt.model.api.id.startsWith("duo-workflow-")) {
|
||||
const id = evt.model.modelID ?? evt.model.id
|
||||
if (id.startsWith("duo-workflow-")) {
|
||||
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
|
||||
const workflowRef =
|
||||
typeof evt.model.request.body.workflowRef === "string" ? evt.model.request.body.workflowRef : undefined
|
||||
typeof evt.model.settings?.workflowRef === "string" ? evt.model.settings.workflowRef : undefined
|
||||
const workflowDefinition =
|
||||
typeof evt.model.request.body.workflowDefinition === "string"
|
||||
? evt.model.request.body.workflowDefinition
|
||||
typeof evt.model.settings?.workflowDefinition === "string"
|
||||
? evt.model.settings.workflowDefinition
|
||||
: undefined
|
||||
const language = evt.sdk.workflowChat(
|
||||
gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow",
|
||||
{
|
||||
featureFlags,
|
||||
workflowDefinition,
|
||||
},
|
||||
)
|
||||
const language = evt.sdk.workflowChat(gitlab.isWorkflowModel(id) ? id : "duo-workflow", {
|
||||
featureFlags,
|
||||
workflowDefinition,
|
||||
})
|
||||
if (workflowRef) language.selectedModelRef = workflowRef
|
||||
evt.language = language
|
||||
return
|
||||
}
|
||||
evt.language = evt.sdk.agenticChat(evt.model.api.id, {
|
||||
evt.language = evt.sdk.agenticChat(id, {
|
||||
aiGatewayHeaders: evt.options.aiGatewayHeaders,
|
||||
featureFlags,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -60,25 +60,28 @@ export const GoogleVertexPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (
|
||||
item.provider.api.package !== "@ai-sdk/google-vertex" &&
|
||||
item.provider.package !== "@ai-sdk/google-vertex" &&
|
||||
!(
|
||||
item.provider.id === ProviderV2.ID.googleVertex &&
|
||||
item.provider.api.package.includes("@ai-sdk/openai-compatible")
|
||||
item.provider.package.includes("@ai-sdk/openai-compatible")
|
||||
)
|
||||
)
|
||||
continue
|
||||
const project = resolveProject(item.provider.request.body)
|
||||
const location = String(resolveLocation(item.provider.request.body))
|
||||
const project = resolveProject(item.provider.settings ?? {})
|
||||
const location = String(resolveLocation(item.provider.settings ?? {}))
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (project) provider.request.body.project = project
|
||||
provider.request.body.location = location
|
||||
if (provider.api.type === "aisdk" && provider.api.url) {
|
||||
provider.api.url = replaceVertexVars(provider.api.url, project, location)
|
||||
}
|
||||
if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) {
|
||||
provider.request.body.fetch = authFetch(provider.request.body.fetch)
|
||||
provider.settings = {
|
||||
...provider.settings,
|
||||
...(project ? { project } : {}),
|
||||
location,
|
||||
...(typeof provider.settings?.baseURL === "string"
|
||||
? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
|
||||
: {}),
|
||||
...(provider.package.includes("@ai-sdk/openai-compatible")
|
||||
? { fetch: authFetch(provider.settings?.fetch) }
|
||||
: {}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -106,7 +109,7 @@ export const GoogleVertexPlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
@ -118,21 +121,20 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/google-vertex/anthropic") continue
|
||||
const project =
|
||||
item.provider.request.body.project ??
|
||||
item.provider.settings?.project ??
|
||||
process.env.GOOGLE_CLOUD_PROJECT ??
|
||||
process.env.GCP_PROJECT ??
|
||||
process.env.GCLOUD_PROJECT
|
||||
const location =
|
||||
item.provider.request.body.location ??
|
||||
item.provider.settings?.location ??
|
||||
process.env.GOOGLE_CLOUD_LOCATION ??
|
||||
process.env.VERTEX_LOCATION ??
|
||||
"global"
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (project) provider.request.body.project = project
|
||||
provider.request.body.location = location
|
||||
provider.settings = { ...provider.settings, ...(project ? { project } : {}), location }
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
@ -166,7 +168,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ export const KiloPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
provider.request.headers["X-Title"] = "opencode"
|
||||
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -10,14 +10,17 @@ export const LLMGatewayPlugin = define({
|
|||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.disabled) continue
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
|
||||
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
provider.request.headers["X-Title"] = "opencode"
|
||||
provider.request.headers["X-Source"] = "opencode"
|
||||
provider.headers = {
|
||||
...provider.headers,
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
"X-Source": "opencode",
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -7,13 +7,16 @@ export const NvidiaPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
provider.request.headers["X-Title"] = "opencode"
|
||||
provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode"
|
||||
provider.headers = {
|
||||
...provider.headers,
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
"X-BILLING-INVOKE-ORIGIN": provider.headers?.["X-BILLING-INVOKE-ORIGIN"] ?? "OpenCode",
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -156,8 +156,8 @@ export const OpenAIPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/openai") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai") continue
|
||||
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
|
||||
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
|
||||
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
|
||||
|
|
@ -177,7 +177,7 @@ export const OpenAIPlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -111,11 +111,14 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
|||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.integrationID = Integration.ID.make("opencode")
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
provider.api = item.npm
|
||||
? { type: "aisdk", package: item.npm, url: item.api }
|
||||
: { type: "native", url: item.api, settings: {} }
|
||||
Object.assign(provider.request.headers, item.options?.headers)
|
||||
Object.assign(provider.request.body, withoutCredentials(item.options))
|
||||
provider.package = item.npm ?? ""
|
||||
provider.aisdk = item.npm ? true : undefined
|
||||
provider.settings = {
|
||||
...provider.settings,
|
||||
...withoutCredentials(item.options),
|
||||
...(item.api ? { baseURL: item.api } : {}),
|
||||
}
|
||||
provider.headers = { ...provider.headers, ...item.options?.headers }
|
||||
})
|
||||
|
||||
const modelIDs = new Set(Object.keys(item.models ?? {}))
|
||||
|
|
@ -127,29 +130,24 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
|||
catalog.model.update(providerID, modelID, (model) => {
|
||||
if (config.family !== undefined) model.family = config.family
|
||||
if (config.name !== undefined) model.name = config.name
|
||||
if (config.id !== undefined) model.api.id = config.id
|
||||
if (config.id !== undefined) model.modelID = config.id
|
||||
if (config.provider !== undefined) {
|
||||
model.api = config.provider.npm
|
||||
? {
|
||||
id: model.api.id,
|
||||
type: "aisdk",
|
||||
package: config.provider.npm,
|
||||
url: config.provider.api,
|
||||
}
|
||||
: { id: model.api.id, type: "native", url: config.provider.api, settings: {} }
|
||||
model.package = config.provider.npm
|
||||
model.aisdk = config.provider.npm ? true : undefined
|
||||
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
|
||||
}
|
||||
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
|
||||
if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input]
|
||||
if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output]
|
||||
const packageName = config.provider?.npm ?? item.npm
|
||||
const lowerer = ConfigProviderOptionsV1.get(packageName)
|
||||
Object.assign(model.request.headers, config.headers)
|
||||
Object.assign(model.request.body, lowerer.model(withoutCredentials(config.options)))
|
||||
model.headers = { ...model.headers, ...config.headers }
|
||||
model.settings = { ...model.settings, ...lowerer.model(withoutCredentials(config.options)) }
|
||||
if (config.variants !== undefined) {
|
||||
model.variants = Object.entries(config.variants).map(([id, options]) => ({
|
||||
id: ModelV2.VariantID.make(id),
|
||||
headers: { ...(options.headers ?? {}) },
|
||||
body: lowerer.model(withoutCredentials(options)),
|
||||
settings: lowerer.model(withoutCredentials(options)),
|
||||
}))
|
||||
}
|
||||
if (config.release_date !== undefined) {
|
||||
|
|
@ -168,9 +166,9 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
|||
|
||||
const item = catalog.provider.get(ProviderV2.ID.opencode)
|
||||
if (!item) return
|
||||
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey)
|
||||
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
|
||||
catalog.provider.update(item.provider.id, (provider) => {
|
||||
if (!hasKey) provider.request.body.apiKey = "public"
|
||||
if (!hasKey) provider.settings = { ...provider.settings, apiKey: "public" }
|
||||
})
|
||||
if (hasKey) return
|
||||
for (const model of item.models.values()) {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ export const OpenRouterPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@openrouter/ai-sdk-provider") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
provider.request.headers["X-Title"] = "opencode"
|
||||
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
|
||||
})
|
||||
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
|
||||
if (!item.models.has(modelID)) continue
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const SapAICorePlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
evt.language = evt.sdk(evt.model.api.id)
|
||||
evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ export const VercelPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/vercel") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/vercel") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["http-referer"] = "https://opencode.ai/"
|
||||
provider.request.headers["x-title"] = "opencode"
|
||||
provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const XAIPlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ export const ZenmuxPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/"
|
||||
provider.request.headers["X-Title"] ??= "opencode"
|
||||
provider.headers = {
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
...provider.headers,
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export * as VariantPlugin from "./variant"
|
||||
|
||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../model"
|
||||
import { define } from "./internal"
|
||||
|
||||
export const Plugin = define({
|
||||
|
|
@ -11,14 +11,14 @@ export const Plugin = define({
|
|||
for (const record of catalog.provider.list()) {
|
||||
for (const model of record.models.values()) {
|
||||
catalog.model.update(model.providerID, model.id, (draft) => {
|
||||
const generated = generate(draft)
|
||||
const generated = generate(draft as ModelV2.Info, record.provider)
|
||||
if (generated.length === 0) return
|
||||
|
||||
const explicit = new Map(draft.variants.map((variant) => [variant.id, variant]))
|
||||
const generatedIDs = new Set(generated.map((variant) => variant.id))
|
||||
const explicit = new Map((draft.variants ?? []).map((variant) => [variant.id, variant]))
|
||||
const generatedIDs = new Set<string>(generated.map((variant) => variant.id))
|
||||
draft.variants = [
|
||||
...generated.map((variant) => explicit.get(variant.id) ?? variant),
|
||||
...draft.variants.filter((variant) => !generatedIDs.has(variant.id)),
|
||||
...(draft.variants ?? []).filter((variant) => !generatedIDs.has(variant.id)),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
|
@ -27,13 +27,15 @@ export const Plugin = define({
|
|||
}),
|
||||
})
|
||||
|
||||
export function generate(model: ModelV2Info): ModelV2Info["variants"] {
|
||||
if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return []
|
||||
const ids = `${model.id} ${model.api.id}`.toLowerCase()
|
||||
export function generate(
|
||||
model: ModelV2.Info,
|
||||
provider?: { readonly package: string; readonly aisdk?: true },
|
||||
): NonNullable<ModelV2.Info["variants"]> {
|
||||
if (!(model.aisdk ?? provider?.aisdk) || (model.package ?? provider?.package) !== "@ai-sdk/openai-compatible") return []
|
||||
const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase()
|
||||
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
|
||||
return ["high", "max"].map((id) => ({
|
||||
id,
|
||||
headers: {},
|
||||
body: { reasoning_effort: id },
|
||||
id: ModelV2.VariantID.make(id),
|
||||
settings: { reasoningEffort: id },
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +1,15 @@
|
|||
export * as ProviderV2 from "./provider"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Types } from "effect"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { Integration } from "./integration"
|
||||
import { withStatics } from "./schema"
|
||||
import type { DeepMutable } from "./schema"
|
||||
|
||||
export const ID = Provider.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
// Temporary runtime schema until core catalog consumers migrate to the flat
|
||||
// package identity in @opencode-ai/schema.
|
||||
export interface AISDK extends Schema.Schema.Type<typeof AISDK> {}
|
||||
export const AISDK = Schema.Struct({
|
||||
type: Schema.Literal("aisdk"),
|
||||
package: Schema.String,
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
})
|
||||
|
||||
export interface Native extends Schema.Schema.Type<typeof Native> {}
|
||||
export const Native = Schema.Struct({
|
||||
type: Schema.Literal("native"),
|
||||
url: Schema.String.pipe(Schema.optional),
|
||||
settings: Schema.Record(Schema.String, Schema.Unknown),
|
||||
})
|
||||
|
||||
export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Api = typeof Api.Type
|
||||
|
||||
export const Request = Provider.Request
|
||||
export type Request = Provider.Request
|
||||
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
id: ID,
|
||||
integrationID: Integration.ID.pipe(Schema.optional),
|
||||
name: Schema.String,
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
api: Api,
|
||||
request: Request,
|
||||
})
|
||||
.annotate({ identifier: "ProviderV2.Info" })
|
||||
.pipe(
|
||||
withStatics((schema) => ({
|
||||
empty: (id: ID) =>
|
||||
schema.make({
|
||||
id,
|
||||
name: id,
|
||||
api: { type: "native", settings: {} },
|
||||
request: { headers: {}, body: {} },
|
||||
}),
|
||||
})),
|
||||
)
|
||||
export const Info = Provider.Info
|
||||
export type Info = Provider.Info
|
||||
|
||||
export type MutableApi<T extends Api = Api> = T extends Api
|
||||
? Omit<DeepMutable<T>, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any })
|
||||
: never
|
||||
|
||||
export type MutableInfo = Omit<DeepMutable<Info>, "api"> & { api: MutableApi }
|
||||
export type MutableInfo = Types.DeepMutable<Info>
|
||||
|
|
|
|||
|
|
@ -50,16 +50,16 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass<VariantUnav
|
|||
}
|
||||
}
|
||||
|
||||
export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiError>()(
|
||||
"SessionRunnerModel.UnsupportedApiError",
|
||||
export class UnsupportedPackageError extends Schema.TaggedErrorClass<UnsupportedPackageError>()(
|
||||
"SessionRunnerModel.UnsupportedPackageError",
|
||||
{
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ModelV2.ID,
|
||||
api: Schema.String,
|
||||
package: Schema.String,
|
||||
},
|
||||
) {
|
||||
override get message() {
|
||||
return `Unsupported API for ${this.providerID}/${this.modelID}: ${this.api}`
|
||||
return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ export type Error =
|
|||
| ModelNotSelectedError
|
||||
| ModelUnavailableError
|
||||
| VariantUnavailableError
|
||||
| UnsupportedApiError
|
||||
| UnsupportedPackageError
|
||||
| Integration.AuthorizationError
|
||||
|
||||
export interface Interface {
|
||||
|
|
@ -82,19 +82,19 @@ export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Servic
|
|||
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
||||
if (credential?.type === "key") return Auth.value(credential.key)
|
||||
if (credential?.type === "oauth") return Auth.value(credential.access)
|
||||
const value = model.request.body.apiKey ?? model.api.settings?.apiKey
|
||||
const value = model.settings?.apiKey
|
||||
if (typeof value === "string") return Auth.value(value)
|
||||
}
|
||||
|
||||
const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
|
||||
const body = model.request.body
|
||||
const body = model.body ?? {}
|
||||
const httpBody = Object.hasOwn(body, "apiKey")
|
||||
? Object.fromEntries(Object.entries(body).filter(([key]) => key !== "apiKey"))
|
||||
: body
|
||||
return route.with({
|
||||
provider: model.providerID,
|
||||
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
|
||||
headers: model.request.headers,
|
||||
endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined,
|
||||
headers: model.headers,
|
||||
http: { body: httpBody },
|
||||
limits: { context: model.limit.context, output: model.limit.output },
|
||||
})
|
||||
|
|
@ -104,8 +104,8 @@ const withVariant = (
|
|||
model: ModelV2.Info,
|
||||
variantID: ModelV2.VariantID | undefined,
|
||||
): Effect.Effect<ModelV2.Info, VariantUnavailableError> => {
|
||||
const id = variantID === "default" || variantID === undefined ? model.request.variant : variantID
|
||||
const variant = model.variants.find((item) => item.id === id)
|
||||
const id = variantID === "default" ? undefined : variantID
|
||||
const variant = model.variants?.find((item) => item.id === id)
|
||||
if (!variant && variantID !== undefined && variantID !== "default")
|
||||
return Effect.fail(
|
||||
new VariantUnavailableError({
|
||||
|
|
@ -117,53 +117,55 @@ const withVariant = (
|
|||
return Effect.succeed(
|
||||
variant
|
||||
? produce(model, (draft) => {
|
||||
Object.assign(draft.request.headers, variant.headers)
|
||||
Object.assign(draft.request.body, variant.body)
|
||||
draft.settings = merge(draft.settings, variant.settings)
|
||||
draft.headers = { ...draft.headers, ...variant.headers }
|
||||
draft.body = merge(draft.body, variant.body)
|
||||
})
|
||||
: model,
|
||||
)
|
||||
}
|
||||
|
||||
const apiName = (model: ModelV2.Info) =>
|
||||
model.api.type === "aisdk" ? `${model.api.type}:${model.api.package}` : model.api.type
|
||||
|
||||
export const fromCatalogModel = (
|
||||
model: ModelV2.Info,
|
||||
credential?: Credential.Value,
|
||||
): Effect.Effect<Model, UnsupportedApiError> => {
|
||||
): Effect.Effect<Model, UnsupportedPackageError> => {
|
||||
const resolved =
|
||||
credential?.metadata === undefined
|
||||
? model
|
||||
: produce(model, (draft) => {
|
||||
Object.assign(draft.request.body, credential.metadata)
|
||||
draft.settings = { ...draft.settings, ...credential.metadata }
|
||||
})
|
||||
const key = apiKey(resolved, credential)
|
||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
|
||||
if (resolved.aisdk && resolved.package === "@ai-sdk/openai") {
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, OpenAIResponses.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||
.model({ id: resolved.api.id }),
|
||||
.model({ id: resolved.modelID ?? resolved.id }),
|
||||
)
|
||||
}
|
||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/anthropic") {
|
||||
if (resolved.aisdk && resolved.package === "@ai-sdk/anthropic") {
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, AnthropicMessages.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
|
||||
.model({ id: resolved.api.id }),
|
||||
.model({ id: resolved.modelID ?? resolved.id }),
|
||||
)
|
||||
}
|
||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai-compatible" && resolved.api.url) {
|
||||
if (
|
||||
resolved.aisdk &&
|
||||
resolved.package === "@ai-sdk/openai-compatible" &&
|
||||
typeof resolved.settings?.baseURL === "string"
|
||||
) {
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, OpenAICompatibleChat.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||
.model({ id: resolved.api.id }),
|
||||
.model({ id: resolved.modelID ?? resolved.id }),
|
||||
)
|
||||
}
|
||||
return Effect.fail(
|
||||
new UnsupportedApiError({
|
||||
new UnsupportedPackageError({
|
||||
providerID: resolved.providerID,
|
||||
modelID: resolved.id,
|
||||
api: apiName(resolved),
|
||||
package: resolved.aisdk ? `aisdk:${resolved.package}` : (resolved.package ?? "unknown"),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -172,10 +174,10 @@ export const resolve = (session: SessionSchema.Info, model: ModelV2.Info, creden
|
|||
withVariant(model, session.model?.variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential)))
|
||||
|
||||
export const supported = (model: ModelV2.Info) =>
|
||||
model.api.type === "aisdk" &&
|
||||
(model.api.package === "@ai-sdk/openai" ||
|
||||
model.api.package === "@ai-sdk/anthropic" ||
|
||||
(model.api.package === "@ai-sdk/openai-compatible" && model.api.url !== undefined))
|
||||
model.aisdk === true &&
|
||||
(model.package === "@ai-sdk/openai" ||
|
||||
model.package === "@ai-sdk/anthropic" ||
|
||||
(model.package === "@ai-sdk/openai-compatible" && typeof model.settings?.baseURL === "string"))
|
||||
|
||||
/** Resolves models from the catalog belonging to the current Location runtime. */
|
||||
export const locationLayer = Layer.effect(
|
||||
|
|
@ -213,3 +215,26 @@ export const locationLayer = Layer.effect(
|
|||
})
|
||||
}),
|
||||
)
|
||||
|
||||
function merge(
|
||||
base: Readonly<Record<string, unknown>> | undefined,
|
||||
overlay: Readonly<Record<string, unknown>> | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (base === undefined) return overlay && { ...overlay }
|
||||
if (overlay === undefined) return { ...base }
|
||||
return Object.fromEntries(
|
||||
Array.from(new Set([...Object.keys(base), ...Object.keys(overlay)])).map((key) => {
|
||||
const left = base[key]
|
||||
const right = overlay[key]
|
||||
if (right === undefined) return [key, left]
|
||||
if (plain(left) && plain(right)) return [key, merge(left, right)]
|
||||
return [key, right]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function plain(input: unknown): input is Readonly<Record<string, unknown>> {
|
||||
if (typeof input !== "object" || input === null || Array.isArray(input)) return false
|
||||
const prototype = Object.getPrototypeOf(input)
|
||||
return prototype === Object.prototype || prototype === null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
|
|||
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
|
||||
: undefined
|
||||
return {
|
||||
id: info.id,
|
||||
modelID: info.id,
|
||||
family: info.family,
|
||||
name: info.name,
|
||||
package: info.provider?.npm,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue