mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-26 22:35:05 +00:00
core: expose v2 model listing API (#25821)
This commit is contained in:
27
packages/core/src/plugin/auth.ts
Normal file
27
packages/core/src/plugin/auth.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Effect } from "effect"
|
||||
import { AuthV2 } from "../auth"
|
||||
import { PluginV2 } from "../plugin"
|
||||
|
||||
export const AuthPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("auth"),
|
||||
effect: Effect.gen(function* () {
|
||||
const auth = yield* AuthV2.Service
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
const account = yield* auth.active(AuthV2.ServiceID.make(evt.provider.id)).pipe(Effect.orDie)
|
||||
if (!account) return
|
||||
evt.provider.enabled = {
|
||||
via: "auth",
|
||||
service: account.serviceID,
|
||||
}
|
||||
if (account.credential.type === "api") {
|
||||
evt.provider.options.aisdk.provider.apiKey = account.credential.key
|
||||
Object.assign(evt.provider.options.aisdk.provider, account.credential.metadata ?? {})
|
||||
}
|
||||
if (account.credential.type === "oauth") {
|
||||
evt.provider.options.aisdk.provider.apiKey = account.credential.access
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
18
packages/core/src/plugin/env.ts
Normal file
18
packages/core/src/plugin/env.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../plugin"
|
||||
|
||||
export const EnvPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("env"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
const key = evt.provider.env.find((item) => process.env[item])
|
||||
if (!key) return
|
||||
evt.provider.enabled = {
|
||||
via: "env",
|
||||
name: key,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
1
packages/core/src/plugin/provider.ts
Normal file
1
packages/core/src/plugin/provider.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ProviderPlugins } from "./provider/index"
|
||||
15
packages/core/src/plugin/provider/alibaba.ts
Normal file
15
packages/core/src/plugin/provider/alibaba.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const AlibabaPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("alibaba"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/alibaba") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
||||
evt.sdk = mod.createAlibaba(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
94
packages/core/src/plugin/provider/amazon-bedrock.ts
Normal file
94
packages/core/src/plugin/provider/amazon-bedrock.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
// Bedrock cross-region inference profiles require regional prefixes only for
|
||||
// specific model/region combinations. Keep the mapping narrow and avoid
|
||||
// double-prefixing model IDs that models.dev already marks as global/us/eu/etc.
|
||||
function resolveModelID(modelID: string, region: string | undefined) {
|
||||
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
||||
if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) return modelID
|
||||
|
||||
const resolvedRegion = region ?? "us-east-1"
|
||||
const regionPrefix = resolvedRegion.split("-")[0]
|
||||
if (regionPrefix === "us") {
|
||||
const requiresPrefix = ["nova-micro", "nova-lite", "nova-pro", "nova-premier", "nova-2", "claude", "deepseek"].some(
|
||||
(item) => modelID.includes(item),
|
||||
)
|
||||
if (requiresPrefix && !resolvedRegion.startsWith("us-gov")) return `${regionPrefix}.${modelID}`
|
||||
return modelID
|
||||
}
|
||||
if (regionPrefix === "eu") {
|
||||
const regionRequiresPrefix = [
|
||||
"eu-west-1",
|
||||
"eu-west-2",
|
||||
"eu-west-3",
|
||||
"eu-north-1",
|
||||
"eu-central-1",
|
||||
"eu-south-1",
|
||||
"eu-south-2",
|
||||
].some((item) => resolvedRegion.includes(item))
|
||||
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((item) =>
|
||||
modelID.includes(item),
|
||||
)
|
||||
return regionRequiresPrefix && modelRequiresPrefix ? `${regionPrefix}.${modelID}` : modelID
|
||||
}
|
||||
if (regionPrefix !== "ap") return modelID
|
||||
|
||||
const australia = ["ap-southeast-2", "ap-southeast-4"].includes(resolvedRegion)
|
||||
if (australia && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((item) => modelID.includes(item))) {
|
||||
return `au.${modelID}`
|
||||
}
|
||||
|
||||
const prefix = resolvedRegion === "ap-northeast-1" ? "jp" : "apac"
|
||||
return ["claude", "nova-lite", "nova-micro", "nova-pro"].some((item) => modelID.includes(item))
|
||||
? `${prefix}.${modelID}`
|
||||
: modelID
|
||||
}
|
||||
|
||||
export const AmazonBedrockPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("amazon-bedrock"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.amazonBedrock) return
|
||||
if (evt.provider.endpoint.type !== "aisdk") return
|
||||
if (typeof evt.provider.options.aisdk.provider.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.
|
||||
evt.provider.endpoint.url = evt.provider.options.aisdk.provider.endpoint
|
||||
delete evt.provider.options.aisdk.provider.endpoint
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/amazon-bedrock") return
|
||||
const options = { ...evt.options }
|
||||
const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
|
||||
const region = typeof options.region === "string" ? options.region : (process.env.AWS_REGION ?? "us-east-1")
|
||||
const bearerToken =
|
||||
process.env.AWS_BEARER_TOKEN_BEDROCK ??
|
||||
(typeof options.bearerToken === "string" ? options.bearerToken : undefined)
|
||||
if (bearerToken && !process.env.AWS_BEARER_TOKEN_BEDROCK) process.env.AWS_BEARER_TOKEN_BEDROCK = bearerToken
|
||||
const containerCreds = Boolean(
|
||||
process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
|
||||
)
|
||||
|
||||
options.region = region
|
||||
if (typeof options.endpoint === "string") options.baseURL = options.endpoint
|
||||
if (!bearerToken && options.credentialProvider === undefined) {
|
||||
// Do not gate SDK creation on explicit AWS env vars. The default chain
|
||||
// also handles ~/.aws/credentials, SSO, process creds, and instance roles.
|
||||
const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers"))
|
||||
options.credentialProvider = fromNodeProviderChain(profile ? { profile } : {})
|
||||
}
|
||||
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
|
||||
evt.sdk = mod.createAmazonBedrock(options)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
|
||||
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.apiID, region))
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
21
packages/core/src/plugin/provider/anthropic.ts
Normal file
21
packages/core/src/plugin/provider/anthropic.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const AnthropicPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("anthropic"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.anthropic) return
|
||||
evt.provider.options.headers["anthropic-beta"] =
|
||||
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
|
||||
evt.sdk = mod.createAnthropic(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
67
packages/core/src/plugin/provider/azure.ts
Normal file
67
packages/core/src/plugin/provider/azure.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
||||
if (useChat && sdk.chat) return sdk.chat(modelID)
|
||||
if (sdk.responses) return sdk.responses(modelID)
|
||||
if (sdk.messages) return sdk.messages(modelID)
|
||||
if (sdk.chat) return sdk.chat(modelID)
|
||||
return sdk.languageModel(modelID)
|
||||
}
|
||||
|
||||
export const AzurePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("azure"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.azure) return
|
||||
const configured = evt.provider.options.aisdk.provider.resourceName
|
||||
const resourceName =
|
||||
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
|
||||
if (resourceName) evt.provider.options.aisdk.provider.resourceName = resourceName
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/azure") return
|
||||
if (evt.model.providerID === ProviderV2.ID.azure) {
|
||||
if (!evt.options.resourceName && !evt.options.baseURL && (evt.model.endpoint.type !== "aisdk" || !evt.model.endpoint.url)) {
|
||||
throw new Error(
|
||||
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
|
||||
)
|
||||
}
|
||||
}
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/azure"))
|
||||
evt.sdk = mod.createAzure(evt.options)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
||||
evt.language = selectLanguage(
|
||||
evt.sdk,
|
||||
evt.model.apiID,
|
||||
Boolean(evt.options.useCompletionUrls),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
export const AzureCognitiveServicesPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("azure-cognitive-services"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
|
||||
if (resourceName) evt.provider.options.aisdk.provider.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai`
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||
evt.language = selectLanguage(
|
||||
evt.sdk,
|
||||
evt.model.apiID,
|
||||
Boolean(evt.options.useCompletionUrls),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
20
packages/core/src/plugin/provider/cerebras.ts
Normal file
20
packages/core/src/plugin/provider/cerebras.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const CerebrasPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("cerebras"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("cerebras")) return
|
||||
evt.provider.options.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cerebras") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
|
||||
evt.sdk = mod.createCerebras(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
81
packages/core/src/plugin/provider/cloudflare-ai-gateway.ts
Normal file
81
packages/core/src/plugin/provider/cloudflare-ai-gateway.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const CloudflareAIGatewayPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("cloudflare-ai-gateway"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "ai-gateway-provider") return
|
||||
if (evt.options.baseURL) return
|
||||
|
||||
const config = gatewayConfig(evt.options)
|
||||
if (!config) return
|
||||
const metadata = gatewayMetadata(evt.options)
|
||||
const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")).pipe(Effect.orDie)
|
||||
const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")).pipe(
|
||||
Effect.orDie,
|
||||
)
|
||||
const gateway = createAiGateway({
|
||||
accountId: config.accountId,
|
||||
gateway: config.gatewayId,
|
||||
apiKey: config.apiKey,
|
||||
options: gatewayOptions(evt.options, metadata),
|
||||
} as any)
|
||||
const unified = createUnified()
|
||||
evt.sdk = {
|
||||
languageModel(modelID: string) {
|
||||
return gateway(unified(modelID))
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
type GatewayConfig = {
|
||||
accountId: string
|
||||
gatewayId: string
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
|
||||
|
||||
function gatewayConfig(options: Record<string, unknown>): GatewayConfig | undefined {
|
||||
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId")
|
||||
// AuthPlugin copies CLI prompt metadata into options. The prompt stores the
|
||||
// gateway as gatewayId, while older config examples may use gateway.
|
||||
const gatewayId =
|
||||
process.env.CLOUDFLARE_GATEWAY_ID ?? stringOption(options, "gatewayId") ?? stringOption(options, "gateway")
|
||||
const apiKey = process.env.CLOUDFLARE_API_TOKEN ?? process.env.CF_AIG_TOKEN ?? stringOption(options, "apiKey")
|
||||
if (!accountId || !gatewayId || !apiKey) return undefined
|
||||
|
||||
return { accountId, gatewayId, apiKey }
|
||||
}
|
||||
|
||||
function gatewayMetadata(options: Record<string, unknown>) {
|
||||
// Preserve the legacy cf-aig-metadata header escape hatch for gateway logging
|
||||
// metadata, but prefer the typed metadata option when present.
|
||||
if (options.metadata !== undefined) return options.metadata
|
||||
const raw = (options.headers as Record<string, string> | undefined)?.["cf-aig-metadata"]
|
||||
return raw ? Option.getOrUndefined(decodeJson(raw)) : undefined
|
||||
}
|
||||
|
||||
function gatewayOptions(options: Record<string, unknown>, metadata: unknown) {
|
||||
return {
|
||||
metadata,
|
||||
cacheTtl: options.cacheTtl,
|
||||
cacheKey: options.cacheKey,
|
||||
skipCache: options.skipCache,
|
||||
collectLog: options.collectLog,
|
||||
headers: {
|
||||
"User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function stringOption(options: Record<string, unknown>, key: string) {
|
||||
return typeof options[key] === "string" ? options[key] : undefined
|
||||
}
|
||||
69
packages/core/src/plugin/provider/cloudflare-workers-ai.ts
Normal file
69
packages/core/src/plugin/provider/cloudflare-workers-ai.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
||||
|
||||
export const CloudflareWorkersAIPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("cloudflare-workers-ai"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== providerID) return
|
||||
if (evt.provider.endpoint.type !== "aisdk") return
|
||||
if (evt.provider.endpoint.url) return
|
||||
|
||||
const accountId = resolveAccountId(evt.provider.options.aisdk.provider)
|
||||
if (accountId) evt.provider.endpoint.url = workersEndpoint(accountId)
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
|
||||
if (!hasWorkersEndpoint(evt.model.endpoint)) return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
||||
evt.sdk = mod.createOpenAICompatible(sdkOptions(evt.options) as any)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
evt.language = evt.sdk.languageModel(evt.model.apiID)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
function resolveAccountId(options: Record<string, unknown>) {
|
||||
return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId")
|
||||
}
|
||||
|
||||
function workersEndpoint(accountId: string) {
|
||||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`
|
||||
}
|
||||
|
||||
function hasWorkersEndpoint(endpoint: ProviderV2.Endpoint) {
|
||||
return endpoint.type === "aisdk" && Boolean(endpoint.url)
|
||||
}
|
||||
|
||||
function sdkOptions(options: Record<string, any>) {
|
||||
return {
|
||||
...options,
|
||||
baseURL: expandAccountId(options.baseURL),
|
||||
apiKey: process.env.CLOUDFLARE_API_KEY ?? options.apiKey,
|
||||
headers: {
|
||||
"User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
...options.headers,
|
||||
},
|
||||
name: providerID,
|
||||
}
|
||||
}
|
||||
|
||||
function expandAccountId(baseURL: unknown) {
|
||||
if (typeof baseURL !== "string") return baseURL
|
||||
return baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", process.env.CLOUDFLARE_ACCOUNT_ID ?? "${CLOUDFLARE_ACCOUNT_ID}")
|
||||
}
|
||||
|
||||
function stringOption(options: Record<string, unknown>, key: string) {
|
||||
return typeof options[key] === "string" ? options[key] : undefined
|
||||
}
|
||||
15
packages/core/src/plugin/provider/cohere.ts
Normal file
15
packages/core/src/plugin/provider/cohere.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const CoherePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("cohere"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cohere") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
|
||||
evt.sdk = mod.createCohere(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
15
packages/core/src/plugin/provider/deepinfra.ts
Normal file
15
packages/core/src/plugin/provider/deepinfra.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const DeepInfraPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("deepinfra"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/deepinfra") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
|
||||
evt.sdk = mod.createDeepInfra(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
31
packages/core/src/plugin/provider/dynamic.ts
Normal file
31
packages/core/src/plugin/provider/dynamic.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Npm } from "../../npm"
|
||||
import { Effect, Option } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const DynamicProviderPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("dynamic-provider"),
|
||||
effect: Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
|
||||
const installedPath = evt.package.startsWith("file://")
|
||||
? evt.package
|
||||
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
|
||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||
|
||||
const mod = yield* Effect.promise(async () => {
|
||||
return (await import(
|
||||
installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
|
||||
)) as Record<string, (options: any) => any>
|
||||
}).pipe(Effect.orDie)
|
||||
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
||||
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
|
||||
|
||||
evt.sdk = mod[match](evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
15
packages/core/src/plugin/provider/gateway.ts
Normal file
15
packages/core/src/plugin/provider/gateway.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const GatewayPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("gateway"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/gateway") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
|
||||
evt.sdk = mod.createGateway(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
44
packages/core/src/plugin/provider/github-copilot.ts
Normal file
44
packages/core/src/plugin/provider/github-copilot.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function shouldUseResponses(modelID: string) {
|
||||
// Copilot supports Responses for GPT-5 class models, except mini variants
|
||||
// which still need the chat-completions endpoint.
|
||||
const match = /^gpt-(\d+)/.exec(modelID)
|
||||
if (!match) return false
|
||||
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
||||
}
|
||||
|
||||
export const GithubCopilotPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("github-copilot"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.githubCopilot) return
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/github-copilot") return
|
||||
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
|
||||
evt.sdk = mod.createOpenaiCompatible(evt.options)
|
||||
}),
|
||||
"aisdk.language": 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.apiID)
|
||||
return
|
||||
}
|
||||
evt.language = shouldUseResponses(evt.model.apiID)
|
||||
? evt.sdk.responses(evt.model.apiID)
|
||||
: evt.sdk.chat(evt.model.apiID)
|
||||
}),
|
||||
"model.update": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||
// This chat-only alias conflicts with the Copilot GPT-5 Responses route,
|
||||
// so hide it only for Copilot rather than for every provider catalog.
|
||||
if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
64
packages/core/src/plugin/provider/gitlab.ts
Normal file
64
packages/core/src/plugin/provider/gitlab.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const GitLabPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("gitlab"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "gitlab-ai-provider") return
|
||||
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
|
||||
evt.sdk = mod.createGitLab({
|
||||
...evt.options,
|
||||
instanceUrl:
|
||||
typeof evt.options.instanceUrl === "string"
|
||||
? evt.options.instanceUrl
|
||||
: (process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com"),
|
||||
apiKey: typeof evt.options.apiKey === "string" ? evt.options.apiKey : process.env.GITLAB_TOKEN,
|
||||
aiGatewayHeaders: {
|
||||
"User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${mod.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
|
||||
"anthropic-beta": "context-1m-2025-08-07",
|
||||
...evt.options.aiGatewayHeaders,
|
||||
},
|
||||
featureFlags: {
|
||||
duo_agent_platform_agentic_chat: true,
|
||||
duo_agent_platform: true,
|
||||
...evt.options.featureFlags,
|
||||
},
|
||||
})
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||
const featureFlags = typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
|
||||
if (evt.model.apiID.startsWith("duo-workflow-")) {
|
||||
const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie)
|
||||
const workflowRef =
|
||||
typeof evt.model.options.aisdk.request.workflowRef === "string"
|
||||
? evt.model.options.aisdk.request.workflowRef
|
||||
: undefined
|
||||
const workflowDefinition =
|
||||
typeof evt.model.options.aisdk.request.workflowDefinition === "string"
|
||||
? evt.model.options.aisdk.request.workflowDefinition
|
||||
: undefined
|
||||
const language = evt.sdk.workflowChat(
|
||||
gitlab.isWorkflowModel(evt.model.apiID) ? evt.model.apiID : "duo-workflow",
|
||||
{
|
||||
featureFlags,
|
||||
workflowDefinition,
|
||||
},
|
||||
)
|
||||
if (workflowRef) language.selectedModelRef = workflowRef
|
||||
evt.language = language
|
||||
return
|
||||
}
|
||||
evt.language = evt.sdk.agenticChat(evt.model.apiID, {
|
||||
aiGatewayHeaders: evt.options.aiGatewayHeaders,
|
||||
featureFlags,
|
||||
})
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
124
packages/core/src/plugin/provider/google-vertex.ts
Normal file
124
packages/core/src/plugin/provider/google-vertex.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function resolveProject(options: Record<string, any>) {
|
||||
// models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex, while Google SDKs
|
||||
// and ADC examples commonly use the broader Google Cloud project aliases.
|
||||
return (
|
||||
options.project ??
|
||||
process.env.GOOGLE_VERTEX_PROJECT ??
|
||||
process.env.GOOGLE_CLOUD_PROJECT ??
|
||||
process.env.GCP_PROJECT ??
|
||||
process.env.GCLOUD_PROJECT
|
||||
)
|
||||
}
|
||||
|
||||
function resolveLocation(options: Record<string, any>) {
|
||||
return options.location ?? process.env.GOOGLE_VERTEX_LOCATION ?? process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "us-central1"
|
||||
}
|
||||
|
||||
function vertexEndpoint(location: string) {
|
||||
return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`
|
||||
}
|
||||
|
||||
function replaceVertexVars(value: string, project: string | undefined, location: string) {
|
||||
// Vertex OpenAI-compatible endpoints are stored as templates in the catalog;
|
||||
// expand them after provider config/env project and location have been resolved.
|
||||
return value
|
||||
.replaceAll("${GOOGLE_VERTEX_PROJECT}", project ?? "${GOOGLE_VERTEX_PROJECT}")
|
||||
.replaceAll("${GOOGLE_VERTEX_LOCATION}", location)
|
||||
.replaceAll("${GOOGLE_VERTEX_ENDPOINT}", vertexEndpoint(location))
|
||||
}
|
||||
|
||||
function authFetch(fetchWithRuntimeOptions?: unknown) {
|
||||
// Native Vertex SDKs handle ADC internally. OpenAI-compatible Vertex endpoints
|
||||
// do not, so inject a Google access token into their fetch path.
|
||||
return async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
const { GoogleAuth } = await import("google-auth-library")
|
||||
const auth = new GoogleAuth()
|
||||
const client = await auth.getApplicationDefault()
|
||||
const token = await client.credential.getAccessToken()
|
||||
const headers = new Headers(init?.headers)
|
||||
headers.set("Authorization", `Bearer ${token.token}`)
|
||||
return typeof fetchWithRuntimeOptions === "function"
|
||||
? fetchWithRuntimeOptions(input, { ...init, headers })
|
||||
: fetch(input, { ...init, headers })
|
||||
}
|
||||
}
|
||||
|
||||
export const GoogleVertexPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("google-vertex"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.googleVertex) return
|
||||
const project = resolveProject(evt.provider.options.aisdk.provider)
|
||||
const location = String(resolveLocation(evt.provider.options.aisdk.provider))
|
||||
if (project) evt.provider.options.aisdk.provider.project = project
|
||||
evt.provider.options.aisdk.provider.location = location
|
||||
if (evt.provider.endpoint.type === "aisdk" && evt.provider.endpoint.url) {
|
||||
evt.provider.endpoint.url = replaceVertexVars(evt.provider.endpoint.url, project, location)
|
||||
}
|
||||
if (evt.provider.endpoint.type === "aisdk" && evt.provider.endpoint.package.includes("@ai-sdk/openai-compatible")) {
|
||||
evt.provider.options.aisdk.provider.fetch = authFetch(evt.provider.options.aisdk.provider.fetch)
|
||||
}
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
|
||||
evt.options.fetch = authFetch(evt.options.fetch)
|
||||
return
|
||||
}
|
||||
if (evt.package !== "@ai-sdk/google-vertex") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex"))
|
||||
const project = resolveProject(evt.options)
|
||||
const location = resolveLocation(evt.options)
|
||||
const options = { ...evt.options }
|
||||
delete options.fetch
|
||||
evt.sdk = mod.createVertex({
|
||||
...options,
|
||||
project,
|
||||
location,
|
||||
})
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.apiID).trim())
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
export const GoogleVertexAnthropicPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("google-vertex-anthropic"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||
const project = evt.provider.options.aisdk.provider.project ?? process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT
|
||||
const location = evt.provider.options.aisdk.provider.location ?? process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"
|
||||
if (project) evt.provider.options.aisdk.provider.project = project
|
||||
evt.provider.options.aisdk.provider.location = location
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
||||
evt.sdk = mod.createVertexAnthropic({
|
||||
...evt.options,
|
||||
project:
|
||||
typeof evt.options.project === "string"
|
||||
? evt.options.project
|
||||
: (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT),
|
||||
location:
|
||||
typeof evt.options.location === "string"
|
||||
? evt.options.location
|
||||
: (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"),
|
||||
})
|
||||
}),
|
||||
"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.apiID).trim())
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
15
packages/core/src/plugin/provider/google.ts
Normal file
15
packages/core/src/plugin/provider/google.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const GooglePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("google"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
|
||||
evt.sdk = mod.createGoogleGenerativeAI(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
15
packages/core/src/plugin/provider/groq.ts
Normal file
15
packages/core/src/plugin/provider/groq.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const GroqPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("groq"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/groq") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
|
||||
evt.sdk = mod.createGroq(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
67
packages/core/src/plugin/provider/index.ts
Normal file
67
packages/core/src/plugin/provider/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { AlibabaPlugin } from "./alibaba"
|
||||
import { AmazonBedrockPlugin } from "./amazon-bedrock"
|
||||
import { AnthropicPlugin } from "./anthropic"
|
||||
import { AzureCognitiveServicesPlugin, AzurePlugin } from "./azure"
|
||||
import { CerebrasPlugin } from "./cerebras"
|
||||
import { CloudflareAIGatewayPlugin } from "./cloudflare-ai-gateway"
|
||||
import { CloudflareWorkersAIPlugin } from "./cloudflare-workers-ai"
|
||||
import { CoherePlugin } from "./cohere"
|
||||
import { DeepInfraPlugin } from "./deepinfra"
|
||||
import { DynamicProviderPlugin } from "./dynamic"
|
||||
import { GatewayPlugin } from "./gateway"
|
||||
import { GithubCopilotPlugin } from "./github-copilot"
|
||||
import { GitLabPlugin } from "./gitlab"
|
||||
import { GooglePlugin } from "./google"
|
||||
import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./google-vertex"
|
||||
import { GroqPlugin } from "./groq"
|
||||
import { KiloPlugin } from "./kilo"
|
||||
import { LLMGatewayPlugin } from "./llmgateway"
|
||||
import { MistralPlugin } from "./mistral"
|
||||
import { NvidiaPlugin } from "./nvidia"
|
||||
import { OpenAIPlugin } from "./openai"
|
||||
import { OpenAICompatiblePlugin } from "./openai-compatible"
|
||||
import { OpencodePlugin } from "./opencode"
|
||||
import { OpenRouterPlugin } from "./openrouter"
|
||||
import { PerplexityPlugin } from "./perplexity"
|
||||
import { SapAICorePlugin } from "./sap-ai-core"
|
||||
import { TogetherAIPlugin } from "./togetherai"
|
||||
import { VercelPlugin } from "./vercel"
|
||||
import { VenicePlugin } from "./venice"
|
||||
import { XAIPlugin } from "./xai"
|
||||
import { ZenmuxPlugin } from "./zenmux"
|
||||
|
||||
export const ProviderPlugins = [
|
||||
AlibabaPlugin,
|
||||
AmazonBedrockPlugin,
|
||||
AnthropicPlugin,
|
||||
AzureCognitiveServicesPlugin,
|
||||
AzurePlugin,
|
||||
CerebrasPlugin,
|
||||
CloudflareAIGatewayPlugin,
|
||||
CloudflareWorkersAIPlugin,
|
||||
CoherePlugin,
|
||||
DeepInfraPlugin,
|
||||
GatewayPlugin,
|
||||
GithubCopilotPlugin,
|
||||
GitLabPlugin,
|
||||
GooglePlugin,
|
||||
GoogleVertexAnthropicPlugin,
|
||||
GoogleVertexPlugin,
|
||||
GroqPlugin,
|
||||
KiloPlugin,
|
||||
LLMGatewayPlugin,
|
||||
MistralPlugin,
|
||||
NvidiaPlugin,
|
||||
OpencodePlugin,
|
||||
OpenAICompatiblePlugin,
|
||||
OpenAIPlugin,
|
||||
OpenRouterPlugin,
|
||||
PerplexityPlugin,
|
||||
SapAICorePlugin,
|
||||
TogetherAIPlugin,
|
||||
VercelPlugin,
|
||||
VenicePlugin,
|
||||
XAIPlugin,
|
||||
ZenmuxPlugin,
|
||||
DynamicProviderPlugin,
|
||||
]
|
||||
16
packages/core/src/plugin/provider/kilo.ts
Normal file
16
packages/core/src/plugin/provider/kilo.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const KiloPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("kilo"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("kilo")) return
|
||||
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
evt.provider.options.headers["X-Title"] = "opencode"
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
18
packages/core/src/plugin/provider/llmgateway.ts
Normal file
18
packages/core/src/plugin/provider/llmgateway.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const LLMGatewayPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("llmgateway"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("llmgateway")) return
|
||||
if (evt.provider.enabled === false) return
|
||||
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
evt.provider.options.headers["X-Title"] = "opencode"
|
||||
evt.provider.options.headers["X-Source"] = "opencode"
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
15
packages/core/src/plugin/provider/mistral.ts
Normal file
15
packages/core/src/plugin/provider/mistral.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const MistralPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("mistral"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/mistral") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
|
||||
evt.sdk = mod.createMistral(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
16
packages/core/src/plugin/provider/nvidia.ts
Normal file
16
packages/core/src/plugin/provider/nvidia.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const NvidiaPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("nvidia"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("nvidia")) return
|
||||
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
evt.provider.options.headers["X-Title"] = "opencode"
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
17
packages/core/src/plugin/provider/openai-compatible.ts
Normal file
17
packages/core/src/plugin/provider/openai-compatible.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const OpenAICompatiblePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("openai-compatible"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
if (!evt.package.includes("@ai-sdk/openai-compatible")) return
|
||||
if (evt.options.includeUsage !== false) evt.options.includeUsage = true
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
|
||||
evt.sdk = mod.createOpenAICompatible(evt.options as any)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
27
packages/core/src/plugin/provider/openai.ts
Normal file
27
packages/core/src/plugin/provider/openai.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const OpenAIPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("openai"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/openai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
|
||||
evt.sdk = mod.createOpenAI(evt.options)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||
evt.language = evt.sdk.responses(evt.model.apiID)
|
||||
}),
|
||||
"model.update": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
|
||||
// chat-completions-only model, so remove it only from OpenAI's catalog.
|
||||
if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
27
packages/core/src/plugin/provider/opencode.ts
Normal file
27
packages/core/src/plugin/provider/opencode.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const OpencodePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("opencode"),
|
||||
effect: Effect.gen(function* () {
|
||||
let hasKey = false
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.opencode) return
|
||||
hasKey = Boolean(
|
||||
process.env.OPENCODE_API_KEY ||
|
||||
evt.provider.env.some((item) => process.env[item]) ||
|
||||
evt.provider.options.aisdk.provider.apiKey ||
|
||||
(evt.provider.enabled && evt.provider.enabled.via === "auth"),
|
||||
)
|
||||
if (!hasKey) evt.provider.options.aisdk.provider.apiKey = "public"
|
||||
}),
|
||||
"model.update": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.opencode) return
|
||||
if (hasKey) return
|
||||
if (evt.model.cost.some((item) => item.input > 0)) evt.cancel = true
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
29
packages/core/src/plugin/provider/openrouter.ts
Normal file
29
packages/core/src/plugin/provider/openrouter.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const OpenRouterPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("openrouter"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.openrouter) return
|
||||
evt.provider.options.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
evt.provider.options.headers["X-Title"] = "opencode"
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@openrouter/ai-sdk-provider") return
|
||||
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
|
||||
evt.sdk = mod.createOpenRouter(evt.options)
|
||||
}),
|
||||
"model.update": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.openrouter) return
|
||||
// These are OpenRouter-specific OpenAI chat aliases that do not work on
|
||||
// the generic path. Keep custom providers with matching IDs untouched.
|
||||
if (evt.model.id === ModelV2.ID.make("gpt-5-chat-latest")) evt.cancel = true
|
||||
if (evt.model.id === ModelV2.ID.make("openai/gpt-5-chat")) evt.cancel = true
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
15
packages/core/src/plugin/provider/perplexity.ts
Normal file
15
packages/core/src/plugin/provider/perplexity.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const PerplexityPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("perplexity"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/perplexity") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
|
||||
evt.sdk = mod.createPerplexity(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
40
packages/core/src/plugin/provider/sap-ai-core.ts
Normal file
40
packages/core/src/plugin/provider/sap-ai-core.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Npm } from "../../npm"
|
||||
import { Effect, Option } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const SapAICorePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("sap-ai-core"),
|
||||
effect: Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
const serviceKey =
|
||||
process.env.AICORE_SERVICE_KEY ??
|
||||
(typeof evt.options.serviceKey === "string" ? evt.options.serviceKey : undefined)
|
||||
if (serviceKey && !process.env.AICORE_SERVICE_KEY) process.env.AICORE_SERVICE_KEY = serviceKey
|
||||
|
||||
const installedPath = evt.package.startsWith("file://")
|
||||
? evt.package
|
||||
: Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
|
||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||
|
||||
const mod = yield* Effect.promise(async () => {
|
||||
return (await import(
|
||||
installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
|
||||
)) as Record<string, (options: any) => any>
|
||||
}).pipe(Effect.orDie)
|
||||
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
||||
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
|
||||
|
||||
evt.sdk = mod[match](serviceKey ? { deploymentId: process.env.AICORE_DEPLOYMENT_ID, resourceGroup: process.env.AICORE_RESOURCE_GROUP } : {})
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
evt.language = evt.sdk(evt.model.apiID)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
15
packages/core/src/plugin/provider/togetherai.ts
Normal file
15
packages/core/src/plugin/provider/togetherai.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const TogetherAIPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("togetherai"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/togetherai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
|
||||
evt.sdk = mod.createTogetherAI(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
15
packages/core/src/plugin/provider/venice.ts
Normal file
15
packages/core/src/plugin/provider/venice.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
|
||||
export const VenicePlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("venice"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "venice-ai-sdk-provider") return
|
||||
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
|
||||
evt.sdk = mod.createVenice(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
21
packages/core/src/plugin/provider/vercel.ts
Normal file
21
packages/core/src/plugin/provider/vercel.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const VercelPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("vercel"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("vercel")) return
|
||||
evt.provider.options.headers["http-referer"] = "https://opencode.ai/"
|
||||
evt.provider.options.headers["x-title"] = "opencode"
|
||||
}),
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/vercel") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
|
||||
evt.sdk = mod.createVercel(evt.options)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
20
packages/core/src/plugin/provider/xai.ts
Normal file
20
packages/core/src/plugin/provider/xai.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const XAIPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("xai"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/xai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
||||
evt.sdk = mod.createXai(evt.options)
|
||||
}),
|
||||
"aisdk.language": Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
||||
evt.language = evt.sdk.responses(evt.model.apiID)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
16
packages/core/src/plugin/provider/zenmux.ts
Normal file
16
packages/core/src/plugin/provider/zenmux.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Effect } from "effect"
|
||||
import { PluginV2 } from "../../plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const ZenmuxPlugin = PluginV2.define({
|
||||
id: PluginV2.ID.make("zenmux"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"provider.update": Effect.fn(function* (evt) {
|
||||
if (evt.provider.id !== ProviderV2.ID.make("zenmux")) return
|
||||
evt.provider.options.headers["HTTP-Referer"] ??= "https://opencode.ai/"
|
||||
evt.provider.options.headers["X-Title"] ??= "opencode"
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user