feat(core): load native provider packages
This commit is contained in:
parent
248db536e2
commit
cd3885910d
66 changed files with 588 additions and 693 deletions
|
|
@ -198,23 +198,23 @@ export const locationLayer = Layer.effect(
|
|||
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.aisdk)
|
||||
if (!ProviderV2.isAISDK(model.package))
|
||||
return yield* new InitError({
|
||||
providerID: model.providerID,
|
||||
cause: new Error(`Unsupported package ${model.package}`),
|
||||
})
|
||||
|
||||
const options = prepareOptions(model, model.package ?? "")
|
||||
const packageName = ProviderV2.packageName(model.package) ?? ""
|
||||
const options = prepareOptions(model, packageName)
|
||||
const sdkKey = JSON.stringify({
|
||||
providerID: model.providerID,
|
||||
package: model.package,
|
||||
package: packageName,
|
||||
settings: model.settings,
|
||||
options,
|
||||
})
|
||||
const sdk =
|
||||
sdks.get(sdkKey) ??
|
||||
(yield* service.runSDK({ model, package: model.package ?? "", options }).pipe(initError(model.providerID)))
|
||||
.sdk
|
||||
(yield* service.runSDK({ model, package: packageName, options }).pipe(initError(model.providerID))).sdk
|
||||
if (!sdk)
|
||||
return yield* new InitError({
|
||||
providerID: model.providerID,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { EventV2 } from "./event"
|
|||
import { Policy } from "./policy"
|
||||
import { State } from "./state"
|
||||
import { Integration } from "./integration"
|
||||
import { ProviderOverlay } from "./provider-overlay"
|
||||
|
||||
export type ProviderRecord = {
|
||||
provider: ProviderV2.MutableInfo
|
||||
|
|
@ -79,10 +80,9 @@ export const layer = Layer.effect(
|
|||
return ModelV2.Info.make({
|
||||
...model,
|
||||
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),
|
||||
settings: ProviderOverlay.merge(provider.settings, model.settings),
|
||||
headers: ProviderOverlay.headers(provider.headers, model.headers),
|
||||
body: ProviderOverlay.merge(provider.body, model.body),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -270,43 +270,6 @@ 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),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { define } from "../../plugin/internal"
|
|||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ProviderOverlay } from "../../provider-overlay"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const Plugin = define({
|
||||
|
|
@ -53,10 +54,10 @@ export const Plugin = define({
|
|||
catalog.provider.update(providerID, (provider) => {
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
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 }
|
||||
if (item.settings !== undefined)
|
||||
provider.settings = ProviderOverlay.merge(provider.settings, item.settings)
|
||||
if (item.headers !== undefined) provider.headers = ProviderOverlay.headers(provider.headers, item.headers)
|
||||
if (item.body !== undefined) provider.body = ProviderOverlay.merge(provider.body, item.body)
|
||||
})
|
||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||
catalog.model.update(providerID, id, (model) => {
|
||||
|
|
@ -64,10 +65,10 @@ export const Plugin = define({
|
|||
if (config.name !== undefined) model.name = config.name
|
||||
if (config.modelID !== undefined) model.modelID = config.modelID
|
||||
if (config.package !== undefined) model.package = config.package
|
||||
if (config.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.settings !== undefined)
|
||||
model.settings = ProviderOverlay.merge(model.settings, config.settings)
|
||||
if (config.headers !== undefined) model.headers = ProviderOverlay.headers(model.headers, config.headers)
|
||||
if (config.body !== undefined) model.body = ProviderOverlay.merge(model.body, config.body)
|
||||
if (config.capabilities !== undefined) {
|
||||
model.capabilities = {
|
||||
tools: config.capabilities.tools,
|
||||
|
|
@ -86,9 +87,10 @@ export const Plugin = define({
|
|||
model.variants.push(existing)
|
||||
}
|
||||
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 }
|
||||
existing.settings = ProviderOverlay.merge(existing.settings, variant.settings)
|
||||
if (variant.headers !== undefined)
|
||||
existing.headers = ProviderOverlay.headers(existing.headers, variant.headers)
|
||||
if (variant.body !== undefined) existing.body = ProviderOverlay.merge(existing.body, variant.body)
|
||||
}
|
||||
}
|
||||
if (config.cost !== undefined) {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
|
|||
family: ModelV2.Family.pipe(Schema.optional),
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
package: Schema.String.pipe(Schema.optional),
|
||||
aisdk: Schema.Literal(true).pipe(Schema.optional),
|
||||
...Overlays,
|
||||
capabilities: ModelV2.Capabilities.pipe(Schema.optional),
|
||||
variants: Schema.Struct({
|
||||
|
|
@ -56,7 +55,6 @@ export class Info extends Schema.Class<Info>("ConfigV2.Provider")({
|
|||
name: Schema.String.pipe(Schema.optional),
|
||||
env: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||
package: Schema.String.pipe(Schema.optional),
|
||||
aisdk: Schema.Literal(true).pipe(Schema.optional),
|
||||
...Overlays,
|
||||
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 { ProviderV2 } from "./provider"
|
||||
import type { DeepMutable } from "./schema"
|
||||
|
||||
export const ID = Model.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
|
@ -23,7 +23,7 @@ export type Ref = typeof Ref.Type
|
|||
export const Info = Model.Info
|
||||
export type Info = Model.Info
|
||||
|
||||
export type MutableInfo = Types.DeepMutable<Info>
|
||||
export type MutableInfo = DeepMutable<Info>
|
||||
|
||||
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
||||
const [providerID, ...modelID] = input.split("/")
|
||||
|
|
|
|||
|
|
@ -75,8 +75,7 @@ export const ModelsDevPlugin = define({
|
|||
const providerID = ProviderV2.ID.make(item.id)
|
||||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.name = item.name
|
||||
provider.package = item.npm ?? ""
|
||||
provider.aisdk = item.npm ? true : undefined
|
||||
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
|
||||
provider.settings = item.api ? { ...provider.settings, baseURL: item.api } : provider.settings
|
||||
})
|
||||
|
||||
|
|
@ -85,8 +84,7 @@ export const ModelsDevPlugin = define({
|
|||
catalog.model.update(providerID, modelID, (draft) => {
|
||||
draft.name = model.name
|
||||
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
|
||||
draft.package = model.provider?.npm
|
||||
draft.aisdk = model.provider?.npm ? true : undefined
|
||||
draft.package = model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined
|
||||
draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
|
||||
draft.capabilities = {
|
||||
tools: model.tool_call,
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ export const AmazonBedrockPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/amazon-bedrock") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (typeof provider.settings?.endpoint !== "string") return
|
||||
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
|
||||
|
|
@ -113,7 +113,10 @@ export const AmazonBedrockPlugin = define({
|
|||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||
if (evt.model.aisdk && evt.model.package === "@ai-sdk/amazon-bedrock/mantle") {
|
||||
if (
|
||||
ProviderV2.isAISDK(evt.model.package) &&
|
||||
ProviderV2.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
|
||||
) {
|
||||
evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const AnthropicPlugin = define({
|
||||
id: "anthropic",
|
||||
|
|
@ -7,8 +8,8 @@ export const AnthropicPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/anthropic") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = {
|
||||
...provider.headers,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ export const AzurePlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/azure") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(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
|
||||
|
|
@ -35,7 +35,7 @@ export const AzurePlugin = define({
|
|||
if (
|
||||
!evt.options.resourceName &&
|
||||
!evt.options.baseURL &&
|
||||
(!evt.model.aisdk || typeof evt.model.settings?.baseURL !== "string")
|
||||
(!ProviderV2.isAISDK(evt.model.package) || 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",
|
||||
|
|
@ -67,8 +67,8 @@ 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.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(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.settings = {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const CerebrasPlugin = define({
|
||||
id: "cerebras",
|
||||
|
|
@ -7,8 +8,8 @@ export const CerebrasPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/cerebras") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const CloudflareWorkersAIPlugin = define({
|
|||
const item = evt.provider.get(providerID)
|
||||
if (!item) return
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
if (!provider.aisdk) return
|
||||
if (!ProviderV2.isAISDK(provider.package)) return
|
||||
if (typeof provider.settings?.baseURL === "string") return
|
||||
const accountId = resolveAccountId(provider.settings ?? {})
|
||||
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
|
||||
|
|
@ -54,8 +54,11 @@ function workersEndpoint(accountId: string) {
|
|||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
|
||||
}
|
||||
|
||||
function hasWorkersEndpoint(model: { readonly aisdk?: true; readonly settings?: Readonly<Record<string, unknown>> }) {
|
||||
return model.aisdk && typeof model.settings?.baseURL === "string"
|
||||
function hasWorkersEndpoint(model: {
|
||||
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>) {
|
||||
|
|
|
|||
|
|
@ -60,12 +60,12 @@ export const GoogleVertexPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (
|
||||
item.provider.package !== "@ai-sdk/google-vertex" &&
|
||||
ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
|
||||
!(
|
||||
item.provider.id === ProviderV2.ID.googleVertex &&
|
||||
item.provider.package.includes("@ai-sdk/openai-compatible")
|
||||
ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
|
@ -79,7 +79,7 @@ export const GoogleVertexPlugin = define({
|
|||
...(typeof provider.settings?.baseURL === "string"
|
||||
? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
|
||||
: {}),
|
||||
...(provider.package.includes("@ai-sdk/openai-compatible")
|
||||
...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
|
||||
? { fetch: authFetch(provider.settings?.fetch) }
|
||||
: {}),
|
||||
}
|
||||
|
|
@ -121,8 +121,8 @@ export const GoogleVertexAnthropicPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/google-vertex/anthropic") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
|
||||
const project =
|
||||
item.provider.settings?.project ??
|
||||
process.env.GOOGLE_CLOUD_PROJECT ??
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const KiloPlugin = define({
|
||||
id: "kilo",
|
||||
|
|
@ -7,8 +8,8 @@ export const KiloPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "../internal"
|
||||
import { Integration } from "../../integration"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const LLMGatewayPlugin = define({
|
||||
id: "llmgateway",
|
||||
|
|
@ -10,8 +11,8 @@ export const LLMGatewayPlugin = define({
|
|||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (item.provider.disabled) continue
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
|
||||
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const NvidiaPlugin = define({
|
||||
id: "nvidia",
|
||||
|
|
@ -7,8 +8,8 @@ export const NvidiaPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = {
|
||||
|
|
|
|||
|
|
@ -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.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(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
|
||||
|
|
|
|||
|
|
@ -111,8 +111,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
|||
catalog.provider.update(providerID, (provider) => {
|
||||
provider.integrationID = Integration.ID.make("opencode")
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
provider.package = item.npm ?? ""
|
||||
provider.aisdk = item.npm ? true : undefined
|
||||
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
|
||||
provider.settings = {
|
||||
...provider.settings,
|
||||
...withoutCredentials(item.options),
|
||||
|
|
@ -132,8 +131,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
|||
if (config.name !== undefined) model.name = config.name
|
||||
if (config.id !== undefined) model.modelID = config.id
|
||||
if (config.provider !== undefined) {
|
||||
model.package = config.provider.npm
|
||||
model.aisdk = config.provider.npm ? true : undefined
|
||||
model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined
|
||||
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
|
||||
}
|
||||
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const OpenRouterPlugin = define({
|
||||
|
|
@ -8,8 +9,8 @@ export const OpenRouterPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@openrouter/ai-sdk-provider") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const VercelPlugin = define({
|
||||
id: "vercel",
|
||||
|
|
@ -7,8 +8,8 @@ export const VercelPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/vercel") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const ZenmuxPlugin = define({
|
||||
id: "zenmux",
|
||||
|
|
@ -7,8 +8,8 @@ export const ZenmuxPlugin = define({
|
|||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
if (!item.provider.aisdk) continue
|
||||
if (item.provider.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (!ProviderV2.isAISDK(item.provider.package)) continue
|
||||
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.headers = {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export * as VariantPlugin from "./variant"
|
|||
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../model"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { define } from "./internal"
|
||||
|
||||
export const Plugin = define({
|
||||
|
|
@ -29,9 +30,10 @@ export const Plugin = define({
|
|||
|
||||
export function generate(
|
||||
model: ModelV2.Info,
|
||||
provider?: { readonly package: string; readonly aisdk?: true },
|
||||
provider?: { readonly package: string },
|
||||
): NonNullable<ModelV2.Info["variants"]> {
|
||||
if (!(model.aisdk ?? provider?.aisdk) || (model.package ?? provider?.package) !== "@ai-sdk/openai-compatible") return []
|
||||
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 []
|
||||
return ["high", "max"].map((id) => ({
|
||||
|
|
|
|||
38
packages/core/src/provider-overlay.ts
Normal file
38
packages/core/src/provider-overlay.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
export * as ProviderOverlay from "./provider-overlay"
|
||||
|
||||
export 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]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export 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(),
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
75
packages/core/src/provider-package.ts
Normal file
75
packages/core/src/provider-package.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
export * as ProviderPackage from "./provider-package"
|
||||
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import type { Model, ProviderPackageDefinition, ProviderPackageSettings } from "@opencode-ai/llm"
|
||||
import { Npm } from "./npm"
|
||||
|
||||
const cache = new Map<string, Promise<unknown>>()
|
||||
|
||||
export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderPackage.LoadError", {
|
||||
package: Schema.String,
|
||||
cause: Schema.Defect(),
|
||||
}) {}
|
||||
|
||||
export const load = Effect.fn("ProviderPackage.load")(function* (specifier: string) {
|
||||
const npm = Option.getOrUndefined(yield* Effect.serviceOption(Npm.Service))
|
||||
const resolved =
|
||||
specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/llm/")
|
||||
? specifier
|
||||
: yield* Effect.sync(() => {
|
||||
try {
|
||||
return import.meta.resolve(specifier)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
if (resolved) return yield* importProviderPackage(specifier, resolved)
|
||||
if (!npm) {
|
||||
return yield* new LoadError({
|
||||
package: specifier,
|
||||
cause: new Error(`Provider package ${specifier} is not installed`),
|
||||
})
|
||||
}
|
||||
const installed = yield* npm
|
||||
.add(packageName(specifier))
|
||||
.pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
|
||||
const entrypoint = yield* Effect.try({
|
||||
try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
|
||||
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||
})
|
||||
return yield* importProviderPackage(specifier, entrypoint)
|
||||
})
|
||||
|
||||
const importProviderPackage = Effect.fn("ProviderPackage.import")(function* (specifier: string, entrypoint: string) {
|
||||
const module = yield* Effect.tryPromise({
|
||||
try: () => {
|
||||
const existing = cache.get(entrypoint)
|
||||
if (existing) return existing
|
||||
const loaded = import(entrypoint)
|
||||
cache.set(entrypoint, loaded)
|
||||
return loaded
|
||||
},
|
||||
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||
})
|
||||
if (!isProviderPackage(module)) {
|
||||
return yield* new LoadError({
|
||||
package: specifier,
|
||||
cause: new Error(`Provider package ${specifier} does not export model(id, settings)`),
|
||||
})
|
||||
}
|
||||
return module
|
||||
})
|
||||
|
||||
export const make = (module: ProviderPackageDefinition, modelID: string, settings: ProviderPackageSettings): Model =>
|
||||
module.model(modelID, settings)
|
||||
|
||||
function isProviderPackage(input: unknown): input is ProviderPackageDefinition {
|
||||
return typeof input === "object" && input !== null && "model" in input && typeof input.model === "function"
|
||||
}
|
||||
|
||||
function packageName(specifier: string) {
|
||||
const parts = specifier.split("/")
|
||||
if (specifier.startsWith("@")) return parts.slice(0, 2).join("/")
|
||||
return parts[0]
|
||||
}
|
||||
|
|
@ -1,15 +1,21 @@
|
|||
export * as ProviderV2 from "./provider"
|
||||
|
||||
import { Types } from "effect"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import type { DeepMutable } from "./schema"
|
||||
|
||||
export const ID = Provider.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const AISDK_PREFIX = "aisdk:"
|
||||
export const isAISDK = (packageName: string | undefined) => packageName?.startsWith(AISDK_PREFIX) ?? false
|
||||
export const aisdk = (packageName: string) => (isAISDK(packageName) ? packageName : `${AISDK_PREFIX}${packageName}`)
|
||||
export const packageName = (packageName: string | undefined) =>
|
||||
isAISDK(packageName) ? packageName!.slice(AISDK_PREFIX.length) : packageName
|
||||
|
||||
export const Request = Provider.Request
|
||||
export type Request = Provider.Request
|
||||
|
||||
export const Info = Provider.Info
|
||||
export type Info = Provider.Info
|
||||
|
||||
export type MutableInfo = Types.DeepMutable<Info>
|
||||
export type MutableInfo = DeepMutable<Info>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { Credential } from "../../credential"
|
|||
import { Integration } from "../../integration"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { ProviderPackage } from "../../provider-package"
|
||||
import { ProviderOverlay } from "../../provider-overlay"
|
||||
import { SessionSchema } from "../schema"
|
||||
|
||||
export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelectedError>()(
|
||||
|
|
@ -117,9 +119,9 @@ const withVariant = (
|
|||
return Effect.succeed(
|
||||
variant
|
||||
? produce(model, (draft) => {
|
||||
draft.settings = merge(draft.settings, variant.settings)
|
||||
draft.headers = { ...draft.headers, ...variant.headers }
|
||||
draft.body = merge(draft.body, variant.body)
|
||||
draft.settings = ProviderOverlay.merge(draft.settings, variant.settings)
|
||||
draft.headers = ProviderOverlay.headers(draft.headers, variant.headers)
|
||||
draft.body = ProviderOverlay.merge(draft.body, variant.body)
|
||||
})
|
||||
: model,
|
||||
)
|
||||
|
|
@ -133,17 +135,18 @@ export const fromCatalogModel = (
|
|||
credential?.metadata === undefined
|
||||
? model
|
||||
: produce(model, (draft) => {
|
||||
draft.settings = { ...draft.settings, ...credential.metadata }
|
||||
draft.settings = ProviderOverlay.merge(draft.settings, credential.metadata)
|
||||
})
|
||||
const key = apiKey(resolved, credential)
|
||||
if (resolved.aisdk && resolved.package === "@ai-sdk/openai") {
|
||||
const packageName = ProviderV2.packageName(resolved.package)
|
||||
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") {
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, OpenAIResponses.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||
.model({ id: resolved.modelID ?? resolved.id }),
|
||||
)
|
||||
}
|
||||
if (resolved.aisdk && resolved.package === "@ai-sdk/anthropic") {
|
||||
if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") {
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, AnthropicMessages.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) })
|
||||
|
|
@ -151,8 +154,8 @@ export const fromCatalogModel = (
|
|||
)
|
||||
}
|
||||
if (
|
||||
resolved.aisdk &&
|
||||
resolved.package === "@ai-sdk/openai-compatible" &&
|
||||
ProviderV2.isAISDK(resolved.package) &&
|
||||
packageName === "@ai-sdk/openai-compatible" &&
|
||||
typeof resolved.settings?.baseURL === "string"
|
||||
) {
|
||||
return Effect.succeed(
|
||||
|
|
@ -161,11 +164,44 @@ export const fromCatalogModel = (
|
|||
.model({ id: resolved.modelID ?? resolved.id }),
|
||||
)
|
||||
}
|
||||
if (!ProviderV2.isAISDK(resolved.package) && resolved.package) {
|
||||
const specifier = resolved.package
|
||||
return Effect.gen(function* () {
|
||||
const module = yield* ProviderPackage.load(specifier).pipe(
|
||||
Effect.mapError(
|
||||
() =>
|
||||
new UnsupportedPackageError({
|
||||
providerID: resolved.providerID,
|
||||
modelID: resolved.id,
|
||||
package: specifier,
|
||||
}),
|
||||
),
|
||||
)
|
||||
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: () => ProviderPackage.make(module, resolved.modelID ?? resolved.id, settings),
|
||||
catch: () =>
|
||||
new UnsupportedPackageError({
|
||||
providerID: resolved.providerID,
|
||||
modelID: resolved.id,
|
||||
package: specifier,
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
return Effect.fail(
|
||||
new UnsupportedPackageError({
|
||||
providerID: resolved.providerID,
|
||||
modelID: resolved.id,
|
||||
package: resolved.aisdk ? `aisdk:${resolved.package}` : (resolved.package ?? "unknown"),
|
||||
package: resolved.package ?? "unknown",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -174,10 +210,12 @@ 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.aisdk === true &&
|
||||
(model.package === "@ai-sdk/openai" ||
|
||||
model.package === "@ai-sdk/anthropic" ||
|
||||
(model.package === "@ai-sdk/openai-compatible" && typeof model.settings?.baseURL === "string"))
|
||||
Boolean(model.package) &&
|
||||
(!ProviderV2.isAISDK(model.package) ||
|
||||
ProviderV2.packageName(model.package) === "@ai-sdk/openai" ||
|
||||
ProviderV2.packageName(model.package) === "@ai-sdk/anthropic" ||
|
||||
(ProviderV2.packageName(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(
|
||||
|
|
@ -215,26 +253,3 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ConfigMCPV1 } from "./mcp"
|
|||
import { ConfigPermissionV1 } from "./permission"
|
||||
import { ConfigProviderV1 } from "./provider"
|
||||
import { ConfigProviderOptionsV1 } from "./provider-options"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
const keys = new Set([
|
||||
"logLevel",
|
||||
|
|
@ -173,8 +174,7 @@ function migrateProvider(info: ConfigProviderV1.Info) {
|
|||
return {
|
||||
name: info.name,
|
||||
env: info.env,
|
||||
package: info.npm,
|
||||
aisdk: info.npm ? (true as const) : undefined,
|
||||
package: info.npm ? ProviderV2.aisdk(info.npm) : undefined,
|
||||
settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings,
|
||||
headers: info.options && options.headers,
|
||||
body: info.options && options.body,
|
||||
|
|
@ -213,8 +213,7 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
|
|||
modelID: info.id,
|
||||
family: info.family,
|
||||
name: info.name,
|
||||
package: info.provider?.npm,
|
||||
aisdk: info.provider?.npm ? (true as const) : undefined,
|
||||
package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined,
|
||||
settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings,
|
||||
capabilities,
|
||||
headers: info.headers,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue