mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-09 10:24:11 +00:00
Compare commits
13 Commits
release-no
...
provider-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bedf95e12 | ||
|
|
c999c3d9e5 | ||
|
|
425ec87b7f | ||
|
|
42aadadf7a | ||
|
|
9c898cd958 | ||
|
|
19c3b25bea | ||
|
|
bbbffbf928 | ||
|
|
c57216f22d | ||
|
|
c3327fc0e4 | ||
|
|
2dbb029472 | ||
|
|
8b1c55f9fa | ||
|
|
a844eb2429 | ||
|
|
48cf07d32a |
@@ -456,9 +456,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
|
||||
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
|
||||
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
|
||||
<Show when={i.release_date}>
|
||||
<Show when={false}>
|
||||
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
|
||||
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
|
||||
{DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -224,6 +224,7 @@ export namespace Agent {
|
||||
export async function generate(input: { description: string }) {
|
||||
const defaultModel = await Provider.defaultModel()
|
||||
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
const system = SystemPrompt.header(defaultModel.providerID)
|
||||
system.push(PROMPT_GENERATE)
|
||||
const existing = await list()
|
||||
@@ -241,7 +242,7 @@ export namespace Agent {
|
||||
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
|
||||
},
|
||||
],
|
||||
model: model.language,
|
||||
model: language,
|
||||
schema: z.object({
|
||||
identifier: z.string(),
|
||||
whenToUse: z.string(),
|
||||
|
||||
@@ -38,7 +38,7 @@ export const ModelsCommand = cmd({
|
||||
|
||||
function printModels(providerID: string, verbose?: boolean) {
|
||||
const provider = providers[providerID]
|
||||
const sortedModels = Object.entries(provider.info.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
||||
for (const [modelID, model] of sortedModels) {
|
||||
process.stdout.write(`${providerID}/${modelID}`)
|
||||
process.stdout.write(EOL)
|
||||
|
||||
@@ -470,6 +470,42 @@ export namespace Config {
|
||||
})
|
||||
export type Layout = z.infer<typeof Layout>
|
||||
|
||||
export const Provider = ModelsDev.Provider.partial()
|
||||
.extend({
|
||||
whitelist: z.array(z.string()).optional(),
|
||||
blacklist: z.array(z.string()).optional(),
|
||||
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
||||
options: z
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
baseURL: z.string().optional(),
|
||||
enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
|
||||
setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
|
||||
timeout: z
|
||||
.union([
|
||||
z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe(
|
||||
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||
),
|
||||
z.literal(false).describe("Disable timeout for this provider entirely."),
|
||||
])
|
||||
.optional()
|
||||
.describe(
|
||||
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||
),
|
||||
})
|
||||
.catchall(z.any())
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
ref: "ProviderConfig",
|
||||
})
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
@@ -536,43 +572,7 @@ export namespace Config {
|
||||
.optional()
|
||||
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
|
||||
provider: z
|
||||
.record(
|
||||
z.string(),
|
||||
ModelsDev.Provider.partial()
|
||||
.extend({
|
||||
whitelist: z.array(z.string()).optional(),
|
||||
blacklist: z.array(z.string()).optional(),
|
||||
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
||||
options: z
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
baseURL: z.string().optional(),
|
||||
enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
|
||||
setCacheKey: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable promptCacheKey for this provider (default false)"),
|
||||
timeout: z
|
||||
.union([
|
||||
z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe(
|
||||
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||
),
|
||||
z.literal(false).describe("Disable timeout for this provider entirely."),
|
||||
])
|
||||
.optional()
|
||||
.describe(
|
||||
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
||||
),
|
||||
})
|
||||
.catchall(z.any())
|
||||
.optional(),
|
||||
})
|
||||
.strict(),
|
||||
)
|
||||
.record(z.string(), Provider)
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
|
||||
@@ -9,16 +9,16 @@ export namespace ModelsDev {
|
||||
const log = Log.create({ service: "models.dev" })
|
||||
const filepath = path.join(Global.Path.cache, "models.json")
|
||||
|
||||
export const Model = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
release_date: z.string(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
temperature: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
cost: z.object({
|
||||
export const Model = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
release_date: z.string(),
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
temperature: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
cost: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache_read: z.number().optional(),
|
||||
@@ -31,40 +31,34 @@ export namespace ModelsDev {
|
||||
cache_write: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
output: z.number(),
|
||||
}),
|
||||
modalities: z
|
||||
.object({
|
||||
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z.boolean().optional(),
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
provider: z.object({ npm: z.string() }).optional(),
|
||||
})
|
||||
.meta({
|
||||
ref: "Model",
|
||||
})
|
||||
})
|
||||
.optional(),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
output: z.number(),
|
||||
}),
|
||||
modalities: z
|
||||
.object({
|
||||
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z.boolean().optional(),
|
||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
provider: z.object({ npm: z.string() }).optional(),
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
|
||||
export const Provider = z
|
||||
.object({
|
||||
api: z.string().optional(),
|
||||
name: z.string(),
|
||||
env: z.array(z.string()),
|
||||
id: z.string(),
|
||||
npm: z.string().optional(),
|
||||
models: z.record(z.string(), Model),
|
||||
})
|
||||
.meta({
|
||||
ref: "Provider",
|
||||
})
|
||||
export const Provider = z.object({
|
||||
api: z.string().optional(),
|
||||
name: z.string(),
|
||||
env: z.array(z.string()),
|
||||
id: z.string(),
|
||||
npm: z.string().optional(),
|
||||
models: z.record(z.string(), Model),
|
||||
})
|
||||
|
||||
export type Provider = z.infer<typeof Provider>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import z from "zod"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { Config } from "../config/config"
|
||||
import { mergeDeep, sortBy } from "remeda"
|
||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||
import { mapValues, mergeDeep, sortBy } from "remeda"
|
||||
import { NoSuchModelError, type Provider as SDK } from "ai"
|
||||
import { Log } from "../util/log"
|
||||
import { BunProc } from "../bun"
|
||||
import { Plugin } from "../plugin"
|
||||
@@ -23,7 +23,7 @@ import { createVertex } from "@ai-sdk/google-vertex"
|
||||
import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||
import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
|
||||
import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
|
||||
|
||||
export namespace Provider {
|
||||
@@ -43,14 +43,13 @@ export namespace Provider {
|
||||
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
||||
}
|
||||
|
||||
type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
|
||||
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
||||
type CustomLoader = (provider: Info) => Promise<{
|
||||
autoload: boolean
|
||||
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
||||
getModel?: CustomModelLoader
|
||||
options?: Record<string, any>
|
||||
}>
|
||||
|
||||
type Source = "env" | "config" | "custom" | "api"
|
||||
|
||||
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
||||
async anthropic() {
|
||||
return {
|
||||
@@ -280,7 +279,7 @@ export namespace Provider {
|
||||
project,
|
||||
location,
|
||||
},
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
async getModel(sdk, modelID) {
|
||||
const id = String(modelID).trim()
|
||||
return sdk.languageModel(id)
|
||||
},
|
||||
@@ -299,10 +298,155 @@ export namespace Provider {
|
||||
},
|
||||
}
|
||||
|
||||
export const Model = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
providerID: z.string(),
|
||||
api: z.object({
|
||||
id: z.string(),
|
||||
url: z.string(),
|
||||
npm: z.string(),
|
||||
}),
|
||||
name: z.string(),
|
||||
capabilities: z.object({
|
||||
temperature: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
attachment: z.boolean(),
|
||||
toolcall: z.boolean(),
|
||||
input: z.object({
|
||||
text: z.boolean(),
|
||||
audio: z.boolean(),
|
||||
image: z.boolean(),
|
||||
video: z.boolean(),
|
||||
pdf: z.boolean(),
|
||||
}),
|
||||
output: z.object({
|
||||
text: z.boolean(),
|
||||
audio: z.boolean(),
|
||||
image: z.boolean(),
|
||||
video: z.boolean(),
|
||||
pdf: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
cost: z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache: z.object({
|
||||
read: z.number(),
|
||||
write: z.number(),
|
||||
}),
|
||||
experimentalOver200K: z
|
||||
.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
cache: z.object({
|
||||
read: z.number(),
|
||||
write: z.number(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
limit: z.object({
|
||||
context: z.number(),
|
||||
output: z.number(),
|
||||
}),
|
||||
status: z.enum(["alpha", "beta", "deprecated", "active"]),
|
||||
options: z.record(z.string(), z.any()),
|
||||
headers: z.record(z.string(), z.string()),
|
||||
})
|
||||
.meta({
|
||||
ref: "Model",
|
||||
})
|
||||
export type Model = z.infer<typeof Model>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
source: z.enum(["env", "config", "custom", "api"]),
|
||||
env: z.string().array(),
|
||||
key: z.string().optional(),
|
||||
options: z.record(z.string(), z.any()),
|
||||
models: z.record(z.string(), Model),
|
||||
})
|
||||
.meta({
|
||||
ref: "Provider",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||
return {
|
||||
id: model.id,
|
||||
providerID: provider.id,
|
||||
name: model.name,
|
||||
api: {
|
||||
id: model.id,
|
||||
url: provider.api!,
|
||||
npm: model.provider?.npm ?? provider.npm ?? provider.id,
|
||||
},
|
||||
status: model.status ?? "active",
|
||||
headers: model.headers ?? {},
|
||||
options: model.options ?? {},
|
||||
cost: {
|
||||
input: model.cost?.input ?? 0,
|
||||
output: model.cost?.output ?? 0,
|
||||
cache: {
|
||||
read: model.cost?.cache_read ?? 0,
|
||||
write: model.cost?.cache_write ?? 0,
|
||||
},
|
||||
experimentalOver200K: model.cost?.context_over_200k
|
||||
? {
|
||||
cache: {
|
||||
read: model.cost.context_over_200k.cache_read ?? 0,
|
||||
write: model.cost.context_over_200k.cache_write ?? 0,
|
||||
},
|
||||
input: model.cost.context_over_200k.input,
|
||||
output: model.cost.context_over_200k.output,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
limit: {
|
||||
context: model.limit.context,
|
||||
output: model.limit.output,
|
||||
},
|
||||
capabilities: {
|
||||
temperature: model.temperature,
|
||||
reasoning: model.reasoning,
|
||||
attachment: model.attachment,
|
||||
toolcall: model.tool_call,
|
||||
input: {
|
||||
text: model.modalities?.input?.includes("text") ?? false,
|
||||
audio: model.modalities?.input?.includes("audio") ?? false,
|
||||
image: model.modalities?.input?.includes("image") ?? false,
|
||||
video: model.modalities?.input?.includes("video") ?? false,
|
||||
pdf: model.modalities?.input?.includes("pdf") ?? false,
|
||||
},
|
||||
output: {
|
||||
text: model.modalities?.output?.includes("text") ?? false,
|
||||
audio: model.modalities?.output?.includes("audio") ?? false,
|
||||
image: model.modalities?.output?.includes("image") ?? false,
|
||||
video: model.modalities?.output?.includes("video") ?? false,
|
||||
pdf: model.modalities?.output?.includes("pdf") ?? false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||
return {
|
||||
id: provider.id,
|
||||
source: "custom",
|
||||
name: provider.name,
|
||||
env: provider.env ?? [],
|
||||
options: {},
|
||||
models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)),
|
||||
}
|
||||
}
|
||||
|
||||
const state = Instance.state(async () => {
|
||||
using _ = log.time("state")
|
||||
const config = await Config.get()
|
||||
const database = await ModelsDev.get()
|
||||
const database = mapValues(await ModelsDev.get(), fromModelsDevProvider)
|
||||
|
||||
const disabled = new Set(config.disabled_providers ?? [])
|
||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
|
||||
@@ -313,54 +457,15 @@ export namespace Provider {
|
||||
return true
|
||||
}
|
||||
|
||||
const providers: {
|
||||
[providerID: string]: {
|
||||
source: Source
|
||||
info: ModelsDev.Provider
|
||||
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
const providers: { [providerID: string]: Info } = {}
|
||||
const languages = new Map<string, LanguageModelV2>()
|
||||
const modelLoaders: {
|
||||
[providerID: string]: CustomModelLoader
|
||||
} = {}
|
||||
const models = new Map<
|
||||
string,
|
||||
{
|
||||
providerID: string
|
||||
modelID: string
|
||||
info: ModelsDev.Model
|
||||
language: LanguageModel
|
||||
npm?: string
|
||||
}
|
||||
>()
|
||||
const sdk = new Map<number, SDK>()
|
||||
// Maps `${provider}/${key}` to the provider’s actual model ID for custom aliases.
|
||||
const realIdByKey = new Map<string, string>()
|
||||
|
||||
log.info("init")
|
||||
|
||||
function mergeProvider(
|
||||
id: string,
|
||||
options: Record<string, any>,
|
||||
source: Source,
|
||||
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>,
|
||||
) {
|
||||
const provider = providers[id]
|
||||
if (!provider) {
|
||||
const info = database[id]
|
||||
if (!info) return
|
||||
if (info.api && !options["baseURL"]) options["baseURL"] = info.api
|
||||
providers[id] = {
|
||||
source,
|
||||
info,
|
||||
options,
|
||||
getModel,
|
||||
}
|
||||
return
|
||||
}
|
||||
provider.options = mergeDeep(provider.options, options)
|
||||
provider.source = source
|
||||
provider.getModel = getModel ?? provider.getModel
|
||||
}
|
||||
|
||||
const configProviders = Object.entries(config.provider ?? {})
|
||||
|
||||
// Add GitHub Copilot Enterprise provider that inherits from GitHub Copilot
|
||||
@@ -370,19 +475,31 @@ export namespace Provider {
|
||||
...githubCopilot,
|
||||
id: "github-copilot-enterprise",
|
||||
name: "GitHub Copilot Enterprise",
|
||||
// Enterprise uses a different API endpoint - will be set dynamically based on auth
|
||||
api: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function mergeProvider(providerID: string, provider: Partial<Info>) {
|
||||
const existing = providers[providerID]
|
||||
if (existing) {
|
||||
// @ts-expect-error
|
||||
providers[providerID] = mergeDeep(existing, provider)
|
||||
return
|
||||
}
|
||||
const match = database[providerID]
|
||||
if (!match) return
|
||||
// @ts-expect-error
|
||||
providers[providerID] = mergeDeep(match, provider)
|
||||
}
|
||||
|
||||
// extend database from config
|
||||
for (const [providerID, provider] of configProviders) {
|
||||
const existing = database[providerID]
|
||||
const parsed: ModelsDev.Provider = {
|
||||
const parsed: Info = {
|
||||
id: providerID,
|
||||
npm: provider.npm ?? existing?.npm,
|
||||
name: provider.name ?? existing?.name ?? providerID,
|
||||
env: provider.env ?? existing?.env ?? [],
|
||||
api: provider.api ?? existing?.api,
|
||||
options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
|
||||
source: "config",
|
||||
models: existing?.models ?? {},
|
||||
}
|
||||
|
||||
@@ -393,51 +510,53 @@ export namespace Provider {
|
||||
if (model.id && model.id !== modelID) return modelID
|
||||
return existing?.name ?? modelID
|
||||
})
|
||||
const parsedModel: ModelsDev.Model = {
|
||||
const parsedModel: Model = {
|
||||
id: modelID,
|
||||
name,
|
||||
release_date: model.release_date ?? existing?.release_date,
|
||||
attachment: model.attachment ?? existing?.attachment ?? false,
|
||||
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
||||
temperature: model.temperature ?? existing?.temperature ?? false,
|
||||
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
||||
cost:
|
||||
!model.cost && !existing?.cost
|
||||
? {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
}
|
||||
: {
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
...existing?.cost,
|
||||
...model.cost,
|
||||
},
|
||||
options: {
|
||||
...existing?.options,
|
||||
...model.options,
|
||||
api: {
|
||||
id: model.id ?? existing?.api.id ?? modelID,
|
||||
npm: model.provider?.npm ?? provider.npm ?? existing?.api.npm ?? providerID,
|
||||
url: provider?.api ?? existing?.api.url,
|
||||
},
|
||||
limit: model.limit ??
|
||||
existing?.limit ?? {
|
||||
context: 0,
|
||||
output: 0,
|
||||
status: model.status ?? existing?.status ?? "active",
|
||||
name,
|
||||
providerID,
|
||||
capabilities: {
|
||||
temperature: model.temperature ?? existing?.capabilities.temperature ?? false,
|
||||
reasoning: model.reasoning ?? existing?.capabilities.reasoning ?? false,
|
||||
attachment: model.attachment ?? existing?.capabilities.attachment ?? false,
|
||||
toolcall: model.tool_call ?? existing?.capabilities.toolcall ?? true,
|
||||
input: {
|
||||
text: model.modalities?.input?.includes("text") ?? existing?.capabilities.input.text ?? true,
|
||||
audio: model.modalities?.input?.includes("audio") ?? existing?.capabilities.input.audio ?? false,
|
||||
image: model.modalities?.input?.includes("image") ?? existing?.capabilities.input.image ?? false,
|
||||
video: model.modalities?.input?.includes("video") ?? existing?.capabilities.input.video ?? false,
|
||||
pdf: model.modalities?.input?.includes("pdf") ?? existing?.capabilities.input.pdf ?? false,
|
||||
},
|
||||
modalities: model.modalities ??
|
||||
existing?.modalities ?? {
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
output: {
|
||||
text: model.modalities?.output?.includes("text") ?? existing?.capabilities.output.text ?? true,
|
||||
audio: model.modalities?.output?.includes("audio") ?? existing?.capabilities.output.audio ?? false,
|
||||
image: model.modalities?.output?.includes("image") ?? existing?.capabilities.output.image ?? false,
|
||||
video: model.modalities?.output?.includes("video") ?? existing?.capabilities.output.video ?? false,
|
||||
pdf: model.modalities?.output?.includes("pdf") ?? existing?.capabilities.output.pdf ?? false,
|
||||
},
|
||||
headers: model.headers,
|
||||
provider: model.provider ?? existing?.provider,
|
||||
}
|
||||
if (model.id && model.id !== modelID) {
|
||||
realIdByKey.set(`${providerID}/${modelID}`, model.id)
|
||||
},
|
||||
cost: {
|
||||
input: model?.cost?.input ?? existing?.cost?.input ?? 0,
|
||||
output: model?.cost?.output ?? existing?.cost?.output ?? 0,
|
||||
cache: {
|
||||
read: model?.cost?.cache_read ?? existing?.cost?.cache.read ?? 0,
|
||||
write: model?.cost?.cache_write ?? existing?.cost?.cache.write ?? 0,
|
||||
},
|
||||
},
|
||||
options: mergeDeep(existing?.options ?? {}, model.options ?? {}),
|
||||
limit: {
|
||||
context: model.limit?.context ?? existing?.limit?.context ?? 0,
|
||||
output: model.limit?.output ?? existing?.limit?.output ?? 0,
|
||||
},
|
||||
headers: mergeDeep(existing?.headers ?? {}, model.headers ?? {}),
|
||||
}
|
||||
parsed.models[modelID] = parsedModel
|
||||
}
|
||||
|
||||
database[providerID] = parsed
|
||||
}
|
||||
|
||||
@@ -447,19 +566,20 @@ export namespace Provider {
|
||||
if (disabled.has(providerID)) continue
|
||||
const apiKey = provider.env.map((item) => env[item]).find(Boolean)
|
||||
if (!apiKey) continue
|
||||
mergeProvider(
|
||||
providerID,
|
||||
// only include apiKey if there's only one potential option
|
||||
provider.env.length === 1 ? { apiKey } : {},
|
||||
"env",
|
||||
)
|
||||
mergeProvider(providerID, {
|
||||
source: "env",
|
||||
key: provider.env.length === 1 ? apiKey : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// load apikeys
|
||||
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
||||
if (disabled.has(providerID)) continue
|
||||
if (provider.type === "api") {
|
||||
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
||||
mergeProvider(providerID, {
|
||||
source: "api",
|
||||
key: provider.key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +605,10 @@ export namespace Provider {
|
||||
// Load for the main provider if auth exists
|
||||
if (auth) {
|
||||
const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
|
||||
mergeProvider(plugin.auth.provider, options ?? {}, "custom")
|
||||
mergeProvider(plugin.auth.provider, {
|
||||
source: "custom",
|
||||
options: options,
|
||||
})
|
||||
}
|
||||
|
||||
// If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
|
||||
@@ -498,7 +621,10 @@ export namespace Provider {
|
||||
() => Auth.get(enterpriseProviderID) as any,
|
||||
database[enterpriseProviderID],
|
||||
)
|
||||
mergeProvider(enterpriseProviderID, enterpriseOptions ?? {}, "custom")
|
||||
mergeProvider(enterpriseProviderID, {
|
||||
source: "custom",
|
||||
options: enterpriseOptions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -508,13 +634,21 @@ export namespace Provider {
|
||||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
||||
if (result.getModel) modelLoaders[providerID] = result.getModel
|
||||
mergeProvider(providerID, {
|
||||
source: "custom",
|
||||
options: result.options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// load config
|
||||
for (const [providerID, provider] of configProviders) {
|
||||
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||
const partial: Partial<Info> = { source: "config" }
|
||||
if (provider.env) partial.env = provider.env
|
||||
if (provider.name) partial.name = provider.name
|
||||
if (provider.options) partial.options = provider.options
|
||||
mergeProvider(providerID, partial)
|
||||
}
|
||||
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
@@ -524,49 +658,43 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") {
|
||||
provider.info.npm = "@ai-sdk/github-copilot"
|
||||
provider.models = mapValues(provider.models, (model) => ({
|
||||
...model,
|
||||
api: {
|
||||
...model.api,
|
||||
npm: "@ai-sdk/github-copilot",
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const configProvider = config.provider?.[providerID]
|
||||
const filteredModels = Object.fromEntries(
|
||||
Object.entries(provider.info.models)
|
||||
// Filter out blacklisted models
|
||||
.filter(
|
||||
([modelID]) =>
|
||||
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
|
||||
)
|
||||
// Filter out experimental models
|
||||
.filter(
|
||||
([, model]) =>
|
||||
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
||||
model.status !== "deprecated",
|
||||
)
|
||||
// Filter by provider's whitelist/blacklist from config
|
||||
.filter(([modelID]) => {
|
||||
if (!configProvider) return true
|
||||
|
||||
return (
|
||||
(!configProvider.blacklist || !configProvider.blacklist.includes(modelID)) &&
|
||||
(!configProvider.whitelist || configProvider.whitelist.includes(modelID))
|
||||
)
|
||||
}),
|
||||
)
|
||||
for (const [modelID, model] of Object.entries(provider.models)) {
|
||||
model.api.id = model.api.id ?? model.id ?? modelID
|
||||
if (modelID === "gpt-5-chat-latest" || (providerID === "openrouter" && modelID === "openai/gpt-5-chat"))
|
||||
delete provider.models[modelID]
|
||||
if ((model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) || model.status === "deprecated")
|
||||
delete provider.models[modelID]
|
||||
if (
|
||||
(configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
|
||||
(configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
|
||||
)
|
||||
delete provider.models[modelID]
|
||||
}
|
||||
|
||||
provider.info.models = filteredModels
|
||||
|
||||
if (Object.keys(provider.info.models).length === 0) {
|
||||
if (Object.keys(provider.models).length === 0) {
|
||||
delete providers[providerID]
|
||||
continue
|
||||
}
|
||||
|
||||
log.info("found", { providerID, npm: provider.info.npm })
|
||||
log.info("found", { providerID })
|
||||
}
|
||||
|
||||
return {
|
||||
models,
|
||||
models: languages,
|
||||
providers,
|
||||
sdk,
|
||||
realIdByKey,
|
||||
modelLoaders,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -574,19 +702,28 @@ export namespace Provider {
|
||||
return state().then((state) => state.providers)
|
||||
}
|
||||
|
||||
async function getSDK(provider: ModelsDev.Provider, model: ModelsDev.Model) {
|
||||
return (async () => {
|
||||
async function getSDK(model: Model) {
|
||||
try {
|
||||
using _ = log.time("getSDK", {
|
||||
providerID: provider.id,
|
||||
providerID: model.providerID,
|
||||
})
|
||||
const s = await state()
|
||||
const pkg = model.provider?.npm ?? provider.npm ?? provider.id
|
||||
const options = { ...s.providers[provider.id]?.options }
|
||||
if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
|
||||
const provider = s.providers[model.providerID]
|
||||
const options = { ...provider.options }
|
||||
|
||||
if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
|
||||
options["includeUsage"] = true
|
||||
}
|
||||
|
||||
const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
|
||||
if (!options["baseURL"]) options["baseURL"] = model.api.url
|
||||
if (!options["apiKey"]) options["apiKey"] = provider.key
|
||||
if (model.headers)
|
||||
options["headers"] = {
|
||||
...options["headers"],
|
||||
...model.headers,
|
||||
}
|
||||
|
||||
const key = Bun.hash.xxHash32(JSON.stringify({ npm: model.api.npm, options }))
|
||||
const existing = s.sdk.get(key)
|
||||
if (existing) return existing
|
||||
|
||||
@@ -615,12 +752,13 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
// Special case: google-vertex-anthropic uses a subpath import
|
||||
const bundledKey = provider.id === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : pkg
|
||||
const bundledKey =
|
||||
model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm
|
||||
const bundledFn = BUNDLED_PROVIDERS[bundledKey]
|
||||
if (bundledFn) {
|
||||
log.info("using bundled provider", { providerID: provider.id, pkg: bundledKey })
|
||||
log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey })
|
||||
const loaded = bundledFn({
|
||||
name: provider.id,
|
||||
name: model.providerID,
|
||||
...options,
|
||||
})
|
||||
s.sdk.set(key, loaded)
|
||||
@@ -628,25 +766,25 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
let installedPath: string
|
||||
if (!pkg.startsWith("file://")) {
|
||||
installedPath = await BunProc.install(pkg, "latest")
|
||||
if (!model.api.npm.startsWith("file://")) {
|
||||
installedPath = await BunProc.install(model.api.npm, "latest")
|
||||
} else {
|
||||
log.info("loading local provider", { pkg })
|
||||
installedPath = pkg
|
||||
log.info("loading local provider", { pkg: model.api.npm })
|
||||
installedPath = model.api.npm
|
||||
}
|
||||
|
||||
const mod = await import(installedPath)
|
||||
|
||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||
const loaded = fn({
|
||||
name: provider.id,
|
||||
name: model.providerID,
|
||||
...options,
|
||||
})
|
||||
s.sdk.set(key, loaded)
|
||||
return loaded as SDK
|
||||
})().catch((e) => {
|
||||
throw new InitError({ providerID: provider.id }, { cause: e })
|
||||
})
|
||||
} catch (e) {
|
||||
throw new InitError({ providerID: model.providerID }, { cause: e })
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProvider(providerID: string) {
|
||||
@@ -654,15 +792,7 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
export async function getModel(providerID: string, modelID: string) {
|
||||
const key = `${providerID}/${modelID}`
|
||||
const s = await state()
|
||||
if (s.models.has(key)) return s.models.get(key)!
|
||||
|
||||
log.info("getModel", {
|
||||
providerID,
|
||||
modelID,
|
||||
})
|
||||
|
||||
const provider = s.providers[providerID]
|
||||
if (!provider) {
|
||||
const availableProviders = Object.keys(s.providers)
|
||||
@@ -671,43 +801,36 @@ export namespace Provider {
|
||||
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||
}
|
||||
|
||||
const info = provider.info.models[modelID]
|
||||
const info = provider.models[modelID]
|
||||
if (!info) {
|
||||
const availableModels = Object.keys(provider.info.models)
|
||||
const availableModels = Object.keys(provider.models)
|
||||
const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 })
|
||||
const suggestions = matches.map((m) => m.target)
|
||||
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
const sdk = await getSDK(provider.info, info)
|
||||
export async function getLanguage(model: Model) {
|
||||
const s = await state()
|
||||
const key = `${model.providerID}/${model.id}`
|
||||
if (s.models.has(key)) return s.models.get(key)!
|
||||
|
||||
const provider = s.providers[model.providerID]
|
||||
const sdk = await getSDK(model)
|
||||
|
||||
try {
|
||||
const keyReal = `${providerID}/${modelID}`
|
||||
const realID = s.realIdByKey.get(keyReal) ?? info.id
|
||||
const language = provider.getModel
|
||||
? await provider.getModel(sdk, realID, provider.options)
|
||||
: sdk.languageModel(realID)
|
||||
log.info("found", { providerID, modelID })
|
||||
s.models.set(key, {
|
||||
providerID,
|
||||
modelID,
|
||||
info,
|
||||
language,
|
||||
npm: info.provider?.npm ?? provider.info.npm,
|
||||
})
|
||||
return {
|
||||
modelID,
|
||||
providerID,
|
||||
info,
|
||||
language,
|
||||
npm: info.provider?.npm ?? provider.info.npm,
|
||||
}
|
||||
const language = s.modelLoaders[model.providerID]
|
||||
? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
|
||||
: sdk.languageModel(model.api.id)
|
||||
s.models.set(key, language)
|
||||
return language
|
||||
} catch (e) {
|
||||
if (e instanceof NoSuchModelError)
|
||||
throw new ModelNotFoundError(
|
||||
{
|
||||
modelID: modelID,
|
||||
providerID,
|
||||
modelID: model.id,
|
||||
providerID: model.providerID,
|
||||
},
|
||||
{ cause: e },
|
||||
)
|
||||
@@ -720,7 +843,7 @@ export namespace Provider {
|
||||
const provider = s.providers[providerID]
|
||||
if (!provider) return undefined
|
||||
for (const item of query) {
|
||||
for (const modelID of Object.keys(provider.info.models)) {
|
||||
for (const modelID of Object.keys(provider.models)) {
|
||||
if (modelID.includes(item))
|
||||
return {
|
||||
providerID,
|
||||
@@ -756,7 +879,7 @@ export namespace Provider {
|
||||
priority = ["gpt-5-nano"]
|
||||
}
|
||||
for (const item of priority) {
|
||||
for (const model of Object.keys(provider.info.models)) {
|
||||
for (const model of Object.keys(provider.models)) {
|
||||
if (model.includes(item)) return getModel(providerID, model)
|
||||
}
|
||||
}
|
||||
@@ -764,7 +887,7 @@ export namespace Provider {
|
||||
|
||||
// Check if opencode provider is available before using it
|
||||
const opencodeProvider = await state().then((state) => state.providers["opencode"])
|
||||
if (opencodeProvider && opencodeProvider.info.models["gpt-5-nano"]) {
|
||||
if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) {
|
||||
return getModel("opencode", "gpt-5-nano")
|
||||
}
|
||||
|
||||
@@ -772,7 +895,7 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
|
||||
export function sort(models: ModelsDev.Model[]) {
|
||||
export function sort(models: Model[]) {
|
||||
return sortBy(
|
||||
models,
|
||||
[(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
|
||||
@@ -787,12 +910,12 @@ export namespace Provider {
|
||||
|
||||
const provider = await list()
|
||||
.then((val) => Object.values(val))
|
||||
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
|
||||
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)))
|
||||
if (!provider) throw new Error("no providers found")
|
||||
const [model] = sort(Object.values(provider.info.models))
|
||||
const [model] = sort(Object.values(provider.models))
|
||||
if (!model) throw new Error("no models found")
|
||||
return {
|
||||
providerID: provider.info.id,
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { APICallError, ModelMessage } from "ai"
|
||||
import { unique } from "remeda"
|
||||
import type { JSONSchema } from "zod/v4/core"
|
||||
import type { Provider } from "./provider"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
function normalizeMessages(msgs: ModelMessage[], providerID: string, modelID: string): ModelMessage[] {
|
||||
if (modelID.includes("claude")) {
|
||||
function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
|
||||
if (model.api.id.includes("claude")) {
|
||||
return msgs.map((msg) => {
|
||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.map((part) => {
|
||||
@@ -20,7 +21,7 @@ export namespace ProviderTransform {
|
||||
return msg
|
||||
})
|
||||
}
|
||||
if (providerID === "mistral" || modelID.toLowerCase().includes("mistral")) {
|
||||
if (model.providerID === "mistral" || model.api.id.toLowerCase().includes("mistral")) {
|
||||
const result: ModelMessage[] = []
|
||||
for (let i = 0; i < msgs.length; i++) {
|
||||
const msg = msgs[i]
|
||||
@@ -107,67 +108,68 @@ export namespace ProviderTransform {
|
||||
return msgs
|
||||
}
|
||||
|
||||
export function message(msgs: ModelMessage[], providerID: string, modelID: string) {
|
||||
msgs = normalizeMessages(msgs, providerID, modelID)
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic") || modelID.includes("claude")) {
|
||||
msgs = applyCaching(msgs, providerID)
|
||||
export function message(msgs: ModelMessage[], model: Provider.Model) {
|
||||
msgs = normalizeMessages(msgs, model)
|
||||
if (model.providerID === "anthropic" || model.api.id.includes("anthropic") || model.api.id.includes("claude")) {
|
||||
msgs = applyCaching(msgs, model.providerID)
|
||||
}
|
||||
|
||||
return msgs
|
||||
}
|
||||
|
||||
export function temperature(_providerID: string, modelID: string) {
|
||||
if (modelID.toLowerCase().includes("qwen")) return 0.55
|
||||
if (modelID.toLowerCase().includes("claude")) return undefined
|
||||
if (modelID.toLowerCase().includes("gemini-3-pro")) return 1.0
|
||||
export function temperature(model: Provider.Model) {
|
||||
if (model.api.id.toLowerCase().includes("qwen")) return 0.55
|
||||
if (model.api.id.toLowerCase().includes("claude")) return undefined
|
||||
if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
|
||||
return 0
|
||||
}
|
||||
|
||||
export function topP(_providerID: string, modelID: string) {
|
||||
if (modelID.toLowerCase().includes("qwen")) return 1
|
||||
export function topP(model: Provider.Model) {
|
||||
if (model.api.id.toLowerCase().includes("qwen")) return 1
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function options(
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
npm: string,
|
||||
model: Provider.Model,
|
||||
sessionID: string,
|
||||
providerOptions?: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
// switch to providerID later, for now use this
|
||||
if (npm === "@openrouter/ai-sdk-provider") {
|
||||
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||
result["usage"] = {
|
||||
include: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (providerID === "openai" || providerOptions?.setCacheKey) {
|
||||
if (model.providerID === "openai" || providerOptions?.setCacheKey) {
|
||||
result["promptCacheKey"] = sessionID
|
||||
}
|
||||
|
||||
if (providerID === "google" || (providerID.startsWith("opencode") && modelID.includes("gemini-3"))) {
|
||||
if (
|
||||
model.providerID === "google" ||
|
||||
(model.providerID.startsWith("opencode") && model.api.id.includes("gemini-3"))
|
||||
) {
|
||||
result["thinkingConfig"] = {
|
||||
includeThoughts: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
|
||||
if (modelID.includes("codex")) {
|
||||
if (model.providerID.includes("gpt-5") && !model.api.id.includes("gpt-5-chat")) {
|
||||
if (model.providerID.includes("codex")) {
|
||||
result["store"] = false
|
||||
}
|
||||
|
||||
if (!modelID.includes("codex") && !modelID.includes("gpt-5-pro")) {
|
||||
if (!model.api.id.includes("codex") && !model.api.id.includes("gpt-5-pro")) {
|
||||
result["reasoningEffort"] = "medium"
|
||||
}
|
||||
|
||||
if (modelID.endsWith("gpt-5.1") && providerID !== "azure") {
|
||||
if (model.api.id.endsWith("gpt-5.1") && model.providerID !== "azure") {
|
||||
result["textVerbosity"] = "low"
|
||||
}
|
||||
|
||||
if (providerID.startsWith("opencode")) {
|
||||
if (model.providerID.startsWith("opencode")) {
|
||||
result["promptCacheKey"] = sessionID
|
||||
result["include"] = ["reasoning.encrypted_content"]
|
||||
result["reasoningSummary"] = "auto"
|
||||
@@ -176,17 +178,17 @@ export namespace ProviderTransform {
|
||||
return result
|
||||
}
|
||||
|
||||
export function smallOptions(input: { providerID: string; modelID: string }) {
|
||||
export function smallOptions(model: Provider.Model) {
|
||||
const options: Record<string, any> = {}
|
||||
|
||||
if (input.providerID === "openai" || input.modelID.includes("gpt-5")) {
|
||||
if (input.modelID.includes("5.1")) {
|
||||
if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
|
||||
if (model.api.id.includes("5.1")) {
|
||||
options["reasoningEffort"] = "low"
|
||||
} else {
|
||||
options["reasoningEffort"] = "minimal"
|
||||
}
|
||||
}
|
||||
if (input.providerID === "google") {
|
||||
if (model.providerID === "google") {
|
||||
options["thinkingConfig"] = {
|
||||
thinkingBudget: 0,
|
||||
}
|
||||
@@ -254,7 +256,7 @@ export namespace ProviderTransform {
|
||||
return standardLimit
|
||||
}
|
||||
|
||||
export function schema(providerID: string, modelID: string, schema: JSONSchema.BaseSchema) {
|
||||
export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema) {
|
||||
/*
|
||||
if (["openai", "azure"].includes(providerID)) {
|
||||
if (schema.type === "object" && schema.properties) {
|
||||
@@ -274,7 +276,7 @@ export namespace ProviderTransform {
|
||||
*/
|
||||
|
||||
// Convert integer enums to string enums for Google/Gemini
|
||||
if (providerID === "google" || modelID.includes("gemini")) {
|
||||
if (model.providerID === "google" || model.api.id.includes("gemini")) {
|
||||
const sanitizeGemini = (obj: any): any => {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
|
||||
@@ -8,7 +8,7 @@ import { proxy } from "hono/proxy"
|
||||
import { Session } from "../session"
|
||||
import z from "zod"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { mapValues } from "remeda"
|
||||
import { mapValues, pipe } from "remeda"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
@@ -296,8 +296,8 @@ export namespace Server {
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { provider, model } = c.req.valid("query")
|
||||
const tools = await ToolRegistry.tools(provider, model)
|
||||
const { provider } = c.req.valid("query")
|
||||
const tools = await ToolRegistry.tools(provider)
|
||||
return c.json(
|
||||
tools.map((t) => ({
|
||||
id: t.id,
|
||||
@@ -1025,7 +1025,7 @@ export namespace Server {
|
||||
async (c) => {
|
||||
c.status(204)
|
||||
c.header("Content-Type", "application/json")
|
||||
return stream(c, async (stream) => {
|
||||
return stream(c, async () => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
SessionPrompt.prompt({ ...body, sessionID })
|
||||
@@ -1231,7 +1231,7 @@ export namespace Server {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z.object({
|
||||
providers: ModelsDev.Provider.array(),
|
||||
providers: Provider.Info.array(),
|
||||
default: z.record(z.string(), z.string()),
|
||||
}),
|
||||
),
|
||||
@@ -1242,7 +1242,7 @@ export namespace Server {
|
||||
}),
|
||||
async (c) => {
|
||||
using _ = log.time("providers")
|
||||
const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
|
||||
const providers = await Provider.list().then((x) => mapValues(x, (item) => item))
|
||||
return c.json({
|
||||
providers: Object.values(providers),
|
||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||
@@ -1272,7 +1272,10 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const providers = await ModelsDev.get()
|
||||
const providers = pipe(
|
||||
await ModelsDev.get(),
|
||||
mapValues((x) => Provider.fromModelsDevProvider(x)),
|
||||
)
|
||||
const connected = await Provider.list().then((x) => Object.keys(x))
|
||||
return c.json({
|
||||
all: Object.values(providers),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { streamText, wrapLanguageModel, type ModelMessage } from "ai"
|
||||
import { wrapLanguageModel, type ModelMessage } from "ai"
|
||||
import { Session } from "."
|
||||
import { Identifier } from "../id/id"
|
||||
import { Instance } from "../project/instance"
|
||||
@@ -7,7 +7,6 @@ import { MessageV2 } from "./message-v2"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { Bus } from "../bus"
|
||||
import z from "zod"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
import { SessionPrompt } from "./prompt"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Token } from "../util/token"
|
||||
@@ -29,7 +28,7 @@ export namespace SessionCompaction {
|
||||
),
|
||||
}
|
||||
|
||||
export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: ModelsDev.Model }) {
|
||||
export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
|
||||
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false
|
||||
const context = input.model.limit.context
|
||||
if (context === 0) return false
|
||||
@@ -98,6 +97,7 @@ export namespace SessionCompaction {
|
||||
auto: boolean
|
||||
}) {
|
||||
const model = await Provider.getModel(input.model.providerID, input.model.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
const system = [...SystemPrompt.compaction(model.providerID)]
|
||||
const msg = (await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
@@ -126,79 +126,72 @@ export namespace SessionCompaction {
|
||||
const processor = SessionProcessor.create({
|
||||
assistantMessage: msg,
|
||||
sessionID: input.sessionID,
|
||||
providerID: input.model.providerID,
|
||||
model: model.info,
|
||||
model: model,
|
||||
abort: input.abort,
|
||||
})
|
||||
const result = await processor.process(() =>
|
||||
streamText({
|
||||
onError(error) {
|
||||
log.error("stream error", {
|
||||
error,
|
||||
})
|
||||
},
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
providerOptions: ProviderTransform.providerOptions(
|
||||
model.npm,
|
||||
model.providerID,
|
||||
pipe(
|
||||
{},
|
||||
mergeDeep(ProviderTransform.options(model.providerID, model.modelID, model.npm ?? "", input.sessionID)),
|
||||
mergeDeep(model.info.options),
|
||||
),
|
||||
const result = await processor.process({
|
||||
onError(error) {
|
||||
log.error("stream error", {
|
||||
error,
|
||||
})
|
||||
},
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
providerOptions: ProviderTransform.providerOptions(
|
||||
model.api.npm,
|
||||
model.providerID,
|
||||
pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
|
||||
),
|
||||
headers: model.headers,
|
||||
abortSignal: input.abort,
|
||||
tools: model.capabilities.toolcall ? {} : undefined,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
headers: model.info.headers,
|
||||
abortSignal: input.abort,
|
||||
tools: model.info.tool_call ? {} : undefined,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
...MessageV2.toModelMessage(
|
||||
input.messages.filter((m) => {
|
||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
...MessageV2.toModelMessage(
|
||||
input.messages.filter((m) => {
|
||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}),
|
||||
),
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
model: wrapLanguageModel({
|
||||
model: model.language,
|
||||
middleware: [
|
||||
return false
|
||||
}),
|
||||
),
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
type: "text",
|
||||
text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
model: wrapLanguageModel({
|
||||
model: language,
|
||||
middleware: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
if (result === "continue" && input.auto) {
|
||||
const continueMsg = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
|
||||
@@ -6,8 +6,7 @@ import { Config } from "../config/config"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Installation } from "../installation"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
import { Share } from "../share/share"
|
||||
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Log } from "../util/log"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
@@ -16,7 +15,8 @@ import { SessionPrompt } from "./prompt"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -223,6 +223,7 @@ export namespace Session {
|
||||
}
|
||||
|
||||
if (cfg.enterprise?.url) {
|
||||
const { ShareNext } = await import("@/share/share-next")
|
||||
const share = await ShareNext.create(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = {
|
||||
@@ -233,6 +234,7 @@ export namespace Session {
|
||||
|
||||
const session = await get(id)
|
||||
if (session.share) return session.share
|
||||
const { Share } = await import("../share/share")
|
||||
const share = await Share.create(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = {
|
||||
@@ -253,6 +255,7 @@ export namespace Session {
|
||||
export const unshare = fn(Identifier.schema("session"), async (id) => {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.enterprise?.url) {
|
||||
const { ShareNext } = await import("@/share/share-next")
|
||||
await ShareNext.remove(id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = undefined
|
||||
@@ -264,6 +267,7 @@ export namespace Session {
|
||||
await update(id, (draft) => {
|
||||
draft.share = undefined
|
||||
})
|
||||
const { Share } = await import("../share/share")
|
||||
await Share.remove(id, share.secret)
|
||||
})
|
||||
|
||||
@@ -389,7 +393,7 @@ export namespace Session {
|
||||
|
||||
export const getUsage = fn(
|
||||
z.object({
|
||||
model: z.custom<ModelsDev.Model>(),
|
||||
model: z.custom<Provider.Model>(),
|
||||
usage: z.custom<LanguageModelUsage>(),
|
||||
metadata: z.custom<ProviderMetadata>().optional(),
|
||||
}),
|
||||
@@ -420,16 +424,16 @@ export namespace Session {
|
||||
}
|
||||
|
||||
const costInfo =
|
||||
input.model.cost?.context_over_200k && tokens.input + tokens.cache.read > 200_000
|
||||
? input.model.cost.context_over_200k
|
||||
input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
|
||||
? input.model.cost.experimentalOver200K
|
||||
: input.model.cost
|
||||
return {
|
||||
cost: safe(
|
||||
new Decimal(0)
|
||||
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache_read ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache_write ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache.read ?? 0).div(1_000_000))
|
||||
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache.write ?? 0).div(1_000_000))
|
||||
// TODO: update models.dev to have better pricing model, for now:
|
||||
// charge reasoning tokens at the same rate as output tokens
|
||||
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ModelsDev } from "@/provider/models"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { type StreamTextResult, type Tool as AITool, APICallError } from "ai"
|
||||
import { streamText } from "ai"
|
||||
import { Log } from "@/util/log"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Session } from "."
|
||||
@@ -11,6 +10,7 @@ import { SessionSummary } from "./summary"
|
||||
import { Bus } from "@/bus"
|
||||
import { SessionRetry } from "./retry"
|
||||
import { SessionStatus } from "./status"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
|
||||
export namespace SessionProcessor {
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
@@ -19,11 +19,19 @@ export namespace SessionProcessor {
|
||||
export type Info = Awaited<ReturnType<typeof create>>
|
||||
export type Result = Awaited<ReturnType<Info["process"]>>
|
||||
|
||||
export type StreamInput = Parameters<typeof streamText>[0]
|
||||
|
||||
export type TBD = {
|
||||
model: {
|
||||
modelID: string
|
||||
providerID: string
|
||||
}
|
||||
}
|
||||
|
||||
export function create(input: {
|
||||
assistantMessage: MessageV2.Assistant
|
||||
sessionID: string
|
||||
providerID: string
|
||||
model: ModelsDev.Model
|
||||
model: Provider.Model
|
||||
abort: AbortSignal
|
||||
}) {
|
||||
const toolcalls: Record<string, MessageV2.ToolPart> = {}
|
||||
@@ -38,13 +46,13 @@ export namespace SessionProcessor {
|
||||
partFromToolCall(toolCallID: string) {
|
||||
return toolcalls[toolCallID]
|
||||
},
|
||||
async process(fn: () => StreamTextResult<Record<string, AITool>, never>) {
|
||||
async process(streamInput: StreamInput) {
|
||||
log.info("process")
|
||||
while (true) {
|
||||
try {
|
||||
let currentText: MessageV2.TextPart | undefined
|
||||
let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
|
||||
const stream = fn()
|
||||
const stream = streamText(streamInput)
|
||||
|
||||
for await (const value of stream.fullStream) {
|
||||
input.abort.throwIfAborted()
|
||||
@@ -328,11 +336,12 @@ export namespace SessionProcessor {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
log.error("process", {
|
||||
error: e,
|
||||
stack: JSON.stringify(e.stack),
|
||||
})
|
||||
const error = MessageV2.fromError(e, { providerID: input.providerID })
|
||||
const error = MessageV2.fromError(e, { providerID: input.sessionID })
|
||||
const retry = SessionRetry.retryable(error)
|
||||
if (retry !== undefined) {
|
||||
attempt++
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Agent } from "../agent/agent"
|
||||
import { Provider } from "../provider/provider"
|
||||
import {
|
||||
generateText,
|
||||
streamText,
|
||||
type ModelMessage,
|
||||
type Tool as AITool,
|
||||
tool,
|
||||
@@ -288,6 +287,7 @@ export namespace SessionPrompt {
|
||||
})
|
||||
|
||||
const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
const task = tasks.pop()
|
||||
|
||||
// pending subtask
|
||||
@@ -311,7 +311,7 @@ export namespace SessionPrompt {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: model.modelID,
|
||||
modelID: model.id,
|
||||
providerID: model.providerID,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -408,7 +408,7 @@ export namespace SessionPrompt {
|
||||
agent: lastUser.agent,
|
||||
model: {
|
||||
providerID: model.providerID,
|
||||
modelID: model.modelID,
|
||||
modelID: model.id,
|
||||
},
|
||||
sessionID,
|
||||
auto: task.auto,
|
||||
@@ -421,7 +421,7 @@ export namespace SessionPrompt {
|
||||
if (
|
||||
lastFinished &&
|
||||
lastFinished.summary !== true &&
|
||||
SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model: model.info })
|
||||
SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
|
||||
) {
|
||||
await SessionCompaction.create({
|
||||
sessionID,
|
||||
@@ -455,7 +455,7 @@ export namespace SessionPrompt {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: model.modelID,
|
||||
modelID: model.id,
|
||||
providerID: model.providerID,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -463,20 +463,18 @@ export namespace SessionPrompt {
|
||||
sessionID,
|
||||
})) as MessageV2.Assistant,
|
||||
sessionID: sessionID,
|
||||
model: model.info,
|
||||
providerID: model.providerID,
|
||||
model,
|
||||
abort,
|
||||
})
|
||||
const system = await resolveSystemPrompt({
|
||||
providerID: model.providerID,
|
||||
modelID: model.info.id,
|
||||
model,
|
||||
agent,
|
||||
system: lastUser.system,
|
||||
})
|
||||
const tools = await resolveTools({
|
||||
agent,
|
||||
sessionID,
|
||||
model: lastUser.model,
|
||||
model,
|
||||
tools: lastUser.tools,
|
||||
processor,
|
||||
})
|
||||
@@ -486,21 +484,19 @@ export namespace SessionPrompt {
|
||||
{
|
||||
sessionID: sessionID,
|
||||
agent: lastUser.agent,
|
||||
model: model.info,
|
||||
model: model,
|
||||
provider,
|
||||
message: lastUser,
|
||||
},
|
||||
{
|
||||
temperature: model.info.temperature
|
||||
? (agent.temperature ?? ProviderTransform.temperature(model.providerID, model.modelID))
|
||||
temperature: model.capabilities.temperature
|
||||
? (agent.temperature ?? ProviderTransform.temperature(model))
|
||||
: undefined,
|
||||
topP: agent.topP ?? ProviderTransform.topP(model.providerID, model.modelID),
|
||||
topP: agent.topP ?? ProviderTransform.topP(model),
|
||||
options: pipe(
|
||||
{},
|
||||
mergeDeep(
|
||||
ProviderTransform.options(model.providerID, model.modelID, model.npm ?? "", sessionID, provider?.options),
|
||||
),
|
||||
mergeDeep(model.info.options),
|
||||
mergeDeep(ProviderTransform.options(model, sessionID, provider?.options)),
|
||||
mergeDeep(model.options),
|
||||
mergeDeep(agent.options),
|
||||
),
|
||||
},
|
||||
@@ -513,113 +509,111 @@ export namespace SessionPrompt {
|
||||
})
|
||||
}
|
||||
|
||||
const result = await processor.process(() =>
|
||||
streamText({
|
||||
onError(error) {
|
||||
log.error("stream error", {
|
||||
error,
|
||||
const result = await processor.process({
|
||||
onError(error) {
|
||||
log.error("stream error", {
|
||||
error,
|
||||
})
|
||||
},
|
||||
async experimental_repairToolCall(input) {
|
||||
const lower = input.toolCall.toolName.toLowerCase()
|
||||
if (lower !== input.toolCall.toolName && tools[lower]) {
|
||||
log.info("repairing tool call", {
|
||||
tool: input.toolCall.toolName,
|
||||
repaired: lower,
|
||||
})
|
||||
},
|
||||
async experimental_repairToolCall(input) {
|
||||
const lower = input.toolCall.toolName.toLowerCase()
|
||||
if (lower !== input.toolCall.toolName && tools[lower]) {
|
||||
log.info("repairing tool call", {
|
||||
tool: input.toolCall.toolName,
|
||||
repaired: lower,
|
||||
})
|
||||
return {
|
||||
...input.toolCall,
|
||||
toolName: lower,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...input.toolCall,
|
||||
input: JSON.stringify({
|
||||
tool: input.toolCall.toolName,
|
||||
error: input.error.message,
|
||||
}),
|
||||
toolName: "invalid",
|
||||
toolName: lower,
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
...(model.providerID.startsWith("opencode")
|
||||
? {
|
||||
"x-opencode-project": Instance.project.id,
|
||||
"x-opencode-session": sessionID,
|
||||
"x-opencode-request": lastUser.id,
|
||||
}
|
||||
: undefined),
|
||||
...model.info.headers,
|
||||
},
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
||||
maxOutputTokens: ProviderTransform.maxOutputTokens(
|
||||
model.providerID,
|
||||
params.options,
|
||||
model.info.limit.output,
|
||||
OUTPUT_TOKEN_MAX,
|
||||
}
|
||||
return {
|
||||
...input.toolCall,
|
||||
input: JSON.stringify({
|
||||
tool: input.toolCall.toolName,
|
||||
error: input.error.message,
|
||||
}),
|
||||
toolName: "invalid",
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
...(model.providerID.startsWith("opencode")
|
||||
? {
|
||||
"x-opencode-project": Instance.project.id,
|
||||
"x-opencode-session": sessionID,
|
||||
"x-opencode-request": lastUser.id,
|
||||
}
|
||||
: undefined),
|
||||
...model.headers,
|
||||
},
|
||||
// set to 0, we handle loop
|
||||
maxRetries: 0,
|
||||
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
||||
maxOutputTokens: ProviderTransform.maxOutputTokens(
|
||||
model.api.npm,
|
||||
params.options,
|
||||
model.limit.output,
|
||||
OUTPUT_TOKEN_MAX,
|
||||
),
|
||||
abortSignal: abort,
|
||||
providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options),
|
||||
stopWhen: stepCountIs(1),
|
||||
temperature: params.temperature,
|
||||
topP: params.topP,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
abortSignal: abort,
|
||||
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
|
||||
stopWhen: stepCountIs(1),
|
||||
temperature: params.temperature,
|
||||
topP: params.topP,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
...MessageV2.toModelMessage(
|
||||
msgs.filter((m) => {
|
||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
...MessageV2.toModelMessage(
|
||||
msgs.filter((m) => {
|
||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}),
|
||||
),
|
||||
],
|
||||
tools: model.info.tool_call === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
model: model.language,
|
||||
middleware: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
|
||||
}
|
||||
// Transform tool schemas for provider compatibility
|
||||
if (args.params.tools && Array.isArray(args.params.tools)) {
|
||||
args.params.tools = args.params.tools.map((tool: any) => {
|
||||
// Tools at middleware level have inputSchema, not parameters
|
||||
if (tool.inputSchema && typeof tool.inputSchema === "object") {
|
||||
// Transform the inputSchema for provider compatibility
|
||||
return {
|
||||
...tool,
|
||||
inputSchema: ProviderTransform.schema(model.providerID, model.modelID, tool.inputSchema),
|
||||
}
|
||||
return false
|
||||
}),
|
||||
),
|
||||
],
|
||||
tools: model.capabilities.toolcall === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
model: language,
|
||||
middleware: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
// @ts-expect-error - prompt types are compatible at runtime
|
||||
args.params.prompt = ProviderTransform.message(args.params.prompt, model)
|
||||
}
|
||||
// Transform tool schemas for provider compatibility
|
||||
if (args.params.tools && Array.isArray(args.params.tools)) {
|
||||
args.params.tools = args.params.tools.map((tool: any) => {
|
||||
// Tools at middleware level have inputSchema, not parameters
|
||||
if (tool.inputSchema && typeof tool.inputSchema === "object") {
|
||||
// Transform the inputSchema for provider compatibility
|
||||
return {
|
||||
...tool,
|
||||
inputSchema: ProviderTransform.schema(model, tool.inputSchema),
|
||||
}
|
||||
// If no inputSchema, return tool unchanged
|
||||
return tool
|
||||
})
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
}
|
||||
// If no inputSchema, return tool unchanged
|
||||
return tool
|
||||
})
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
if (result === "stop") break
|
||||
continue
|
||||
}
|
||||
@@ -642,18 +636,13 @@ export namespace SessionPrompt {
|
||||
return Provider.defaultModel()
|
||||
}
|
||||
|
||||
async function resolveSystemPrompt(input: {
|
||||
system?: string
|
||||
agent: Agent.Info
|
||||
providerID: string
|
||||
modelID: string
|
||||
}) {
|
||||
let system = SystemPrompt.header(input.providerID)
|
||||
async function resolveSystemPrompt(input: { system?: string; agent: Agent.Info; model: Provider.Model }) {
|
||||
let system = SystemPrompt.header(input.model.providerID)
|
||||
system.push(
|
||||
...(() => {
|
||||
if (input.system) return [input.system]
|
||||
if (input.agent.prompt) return [input.agent.prompt]
|
||||
return SystemPrompt.provider(input.modelID)
|
||||
return SystemPrompt.provider(input.model)
|
||||
})(),
|
||||
)
|
||||
system.push(...(await SystemPrompt.environment()))
|
||||
@@ -666,10 +655,7 @@ export namespace SessionPrompt {
|
||||
|
||||
async function resolveTools(input: {
|
||||
agent: Agent.Info
|
||||
model: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
model: Provider.Model
|
||||
sessionID: string
|
||||
tools?: Record<string, boolean>
|
||||
processor: SessionProcessor.Info
|
||||
@@ -677,16 +663,12 @@ export namespace SessionPrompt {
|
||||
const tools: Record<string, AITool> = {}
|
||||
const enabledTools = pipe(
|
||||
input.agent.tools,
|
||||
mergeDeep(await ToolRegistry.enabled(input.model.providerID, input.model.modelID, input.agent)),
|
||||
mergeDeep(await ToolRegistry.enabled(input.agent)),
|
||||
mergeDeep(input.tools ?? {}),
|
||||
)
|
||||
for (const item of await ToolRegistry.tools(input.model.providerID, input.model.modelID)) {
|
||||
for (const item of await ToolRegistry.tools(input.model.providerID)) {
|
||||
if (Wildcard.all(item.id, enabledTools) === false) continue
|
||||
const schema = ProviderTransform.schema(
|
||||
input.model.providerID,
|
||||
input.model.modelID,
|
||||
z.toJSONSchema(item.parameters),
|
||||
)
|
||||
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
|
||||
tools[item.id] = tool({
|
||||
id: item.id as any,
|
||||
description: item.description,
|
||||
@@ -1437,25 +1419,18 @@ export namespace SessionPrompt {
|
||||
if (!isFirst) return
|
||||
const small =
|
||||
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
|
||||
const language = await Provider.getLanguage(small)
|
||||
const provider = await Provider.getProvider(small.providerID)
|
||||
const options = pipe(
|
||||
{},
|
||||
mergeDeep(
|
||||
ProviderTransform.options(
|
||||
small.providerID,
|
||||
small.modelID,
|
||||
small.npm ?? "",
|
||||
input.session.id,
|
||||
provider?.options,
|
||||
),
|
||||
),
|
||||
mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, modelID: small.modelID })),
|
||||
mergeDeep(small.info.options),
|
||||
mergeDeep(ProviderTransform.options(small, input.session.id, provider?.options)),
|
||||
mergeDeep(ProviderTransform.smallOptions(small)),
|
||||
mergeDeep(small.options),
|
||||
)
|
||||
await generateText({
|
||||
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
|
||||
maxOutputTokens: small.info.reasoning ? 3000 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
|
||||
maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
||||
messages: [
|
||||
...SystemPrompt.title(small.providerID).map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -1486,8 +1461,8 @@ export namespace SessionPrompt {
|
||||
},
|
||||
]),
|
||||
],
|
||||
headers: small.info.headers,
|
||||
model: small.language,
|
||||
headers: small.headers,
|
||||
model: language,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.text)
|
||||
@@ -1504,7 +1479,7 @@ export namespace SessionPrompt {
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to generate title", { error, model: small.info.id })
|
||||
log.error("failed to generate title", { error, model: small.id })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,19 +76,20 @@ export namespace SessionSummary {
|
||||
const small =
|
||||
(await Provider.getSmallModel(assistantMsg.providerID)) ??
|
||||
(await Provider.getModel(assistantMsg.providerID, assistantMsg.modelID))
|
||||
const language = await Provider.getLanguage(small)
|
||||
|
||||
const options = pipe(
|
||||
{},
|
||||
mergeDeep(ProviderTransform.options(small.providerID, small.modelID, small.npm ?? "", assistantMsg.sessionID)),
|
||||
mergeDeep(ProviderTransform.smallOptions({ providerID: small.providerID, modelID: small.modelID })),
|
||||
mergeDeep(small.info.options),
|
||||
mergeDeep(ProviderTransform.options(small, assistantMsg.sessionID)),
|
||||
mergeDeep(ProviderTransform.smallOptions(small)),
|
||||
mergeDeep(small.options),
|
||||
)
|
||||
|
||||
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
|
||||
if (textPart && !userMsg.summary?.title) {
|
||||
const result = await generateText({
|
||||
maxOutputTokens: small.info.reasoning ? 1500 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
|
||||
maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
||||
messages: [
|
||||
...SystemPrompt.title(small.providerID).map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -106,8 +107,8 @@ export namespace SessionSummary {
|
||||
`,
|
||||
},
|
||||
],
|
||||
headers: small.info.headers,
|
||||
model: small.language,
|
||||
headers: small.headers,
|
||||
model: language,
|
||||
})
|
||||
log.info("title", { title: result.text })
|
||||
userMsg.summary.title = result.text
|
||||
@@ -132,9 +133,9 @@ export namespace SessionSummary {
|
||||
}
|
||||
}
|
||||
const result = await generateText({
|
||||
model: small.language,
|
||||
model: language,
|
||||
maxOutputTokens: 100,
|
||||
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
|
||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
||||
messages: [
|
||||
...SystemPrompt.summarize(small.providerID).map(
|
||||
(x): ModelMessage => ({
|
||||
@@ -148,7 +149,7 @@ export namespace SessionSummary {
|
||||
content: `Summarize the above conversation according to your system prompts.`,
|
||||
},
|
||||
],
|
||||
headers: small.info.headers,
|
||||
headers: small.headers,
|
||||
}).catch(() => {})
|
||||
if (result) summary = result.text
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
|
||||
import PROMPT_TITLE from "./prompt/title.txt"
|
||||
import PROMPT_CODEX from "./prompt/codex.txt"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
|
||||
export namespace SystemPrompt {
|
||||
export function header(providerID: string) {
|
||||
@@ -24,12 +25,13 @@ export namespace SystemPrompt {
|
||||
return []
|
||||
}
|
||||
|
||||
export function provider(modelID: string) {
|
||||
if (modelID.includes("gpt-5")) return [PROMPT_CODEX]
|
||||
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
|
||||
if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
|
||||
if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
|
||||
if (modelID.includes("polaris-alpha")) return [PROMPT_POLARIS]
|
||||
export function provider(model: Provider.Model) {
|
||||
if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
|
||||
if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
|
||||
return [PROMPT_BEAST]
|
||||
if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
|
||||
if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
|
||||
if (model.api.id.includes("polaris-alpha")) return [PROMPT_POLARIS]
|
||||
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { ulid } from "ulid"
|
||||
import type { ModelsDev } from "@/provider/models"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Session } from "@/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
@@ -36,7 +35,7 @@ export namespace ShareNext {
|
||||
type: "model",
|
||||
data: [
|
||||
await Provider.getModel(evt.properties.info.model.providerID, evt.properties.info.model.modelID).then(
|
||||
(m) => m.info,
|
||||
(m) => m,
|
||||
),
|
||||
],
|
||||
},
|
||||
@@ -105,7 +104,7 @@ export namespace ShareNext {
|
||||
}
|
||||
| {
|
||||
type: "model"
|
||||
data: ModelsDev.Model[]
|
||||
data: SDK.Model[]
|
||||
}
|
||||
|
||||
const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
|
||||
@@ -171,7 +170,7 @@ export namespace ShareNext {
|
||||
messages
|
||||
.filter((m) => m.info.role === "user")
|
||||
.map((m) => (m.info as SDK.UserMessage).model)
|
||||
.map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m.info)),
|
||||
.map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m)),
|
||||
)
|
||||
await sync(sessionID, [
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ export const BatchTool = Tool.define("batch", async () => {
|
||||
const discardedCalls = params.tool_calls.slice(10)
|
||||
|
||||
const { ToolRegistry } = await import("./registry")
|
||||
const availableTools = await ToolRegistry.tools("", "")
|
||||
const availableTools = await ToolRegistry.tools("")
|
||||
const toolMap = new Map(availableTools.map((t) => [t.id, t]))
|
||||
|
||||
const executeCall = async (call: (typeof toolCalls)[0]) => {
|
||||
|
||||
@@ -101,7 +101,7 @@ export const ReadTool = Tool.define("read", {
|
||||
const modelID = ctx.extra["modelID"] as string
|
||||
const model = await Provider.getModel(providerID, modelID).catch(() => undefined)
|
||||
if (!model) return false
|
||||
return model.info.modalities?.input?.includes("image") ?? false
|
||||
return model.capabilities.input.image
|
||||
})()
|
||||
if (isImage) {
|
||||
if (!supportsImages) {
|
||||
|
||||
@@ -108,7 +108,7 @@ export namespace ToolRegistry {
|
||||
return all().then((x) => x.map((t) => t.id))
|
||||
}
|
||||
|
||||
export async function tools(providerID: string, _modelID: string) {
|
||||
export async function tools(providerID: string) {
|
||||
const tools = await all()
|
||||
const result = await Promise.all(
|
||||
tools
|
||||
@@ -124,11 +124,7 @@ export namespace ToolRegistry {
|
||||
return result
|
||||
}
|
||||
|
||||
export async function enabled(
|
||||
_providerID: string,
|
||||
_modelID: string,
|
||||
agent: Agent.Info,
|
||||
): Promise<Record<string, boolean>> {
|
||||
export async function enabled(agent: Agent.Info): Promise<Record<string, boolean>> {
|
||||
const result: Record<string, boolean> = {}
|
||||
|
||||
if (agent.permission.edit === "deny") {
|
||||
|
||||
@@ -132,7 +132,7 @@ test("model whitelist filters models for provider", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["anthropic"]).toBeDefined()
|
||||
const models = Object.keys(providers["anthropic"].info.models)
|
||||
const models = Object.keys(providers["anthropic"].models)
|
||||
expect(models).toContain("claude-sonnet-4-20250514")
|
||||
expect(models.length).toBe(1)
|
||||
},
|
||||
@@ -163,7 +163,7 @@ test("model blacklist excludes specific models", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["anthropic"]).toBeDefined()
|
||||
const models = Object.keys(providers["anthropic"].info.models)
|
||||
const models = Object.keys(providers["anthropic"].models)
|
||||
expect(models).not.toContain("claude-sonnet-4-20250514")
|
||||
},
|
||||
})
|
||||
@@ -198,8 +198,8 @@ test("custom model alias via config", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["anthropic"]).toBeDefined()
|
||||
expect(providers["anthropic"].info.models["my-alias"]).toBeDefined()
|
||||
expect(providers["anthropic"].info.models["my-alias"].name).toBe("My Custom Alias")
|
||||
expect(providers["anthropic"].models["my-alias"]).toBeDefined()
|
||||
expect(providers["anthropic"].models["my-alias"].name).toBe("My Custom Alias")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -241,8 +241,8 @@ test("custom provider with npm package", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["custom-provider"]).toBeDefined()
|
||||
expect(providers["custom-provider"].info.name).toBe("Custom Provider")
|
||||
expect(providers["custom-provider"].info.models["custom-model"]).toBeDefined()
|
||||
expect(providers["custom-provider"].name).toBe("Custom Provider")
|
||||
expect(providers["custom-provider"].models["custom-model"]).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -299,8 +299,9 @@ test("getModel returns model for valid provider/model", async () => {
|
||||
const model = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
|
||||
expect(model).toBeDefined()
|
||||
expect(model.providerID).toBe("anthropic")
|
||||
expect(model.modelID).toBe("claude-sonnet-4-20250514")
|
||||
expect(model.language).toBeDefined()
|
||||
expect(model.id).toBe("claude-sonnet-4-20250514")
|
||||
const language = await Provider.getLanguage(model)
|
||||
expect(language).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -478,11 +479,11 @@ test("model cost defaults to zero when not specified", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const model = providers["test-provider"].info.models["test-model"]
|
||||
const model = providers["test-provider"].models["test-model"]
|
||||
expect(model.cost.input).toBe(0)
|
||||
expect(model.cost.output).toBe(0)
|
||||
expect(model.cost.cache_read).toBe(0)
|
||||
expect(model.cost.cache_write).toBe(0)
|
||||
expect(model.cost.cache.read).toBe(0)
|
||||
expect(model.cost.cache.write).toBe(0)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -516,7 +517,7 @@ test("model options are merged from existing model", async () => {
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
|
||||
const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
|
||||
expect(model.options.customOption).toBe("custom-value")
|
||||
},
|
||||
})
|
||||
@@ -623,17 +624,17 @@ test("getModel uses realIdByKey for aliased models", async () => {
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["anthropic"].info.models["my-sonnet"]).toBeDefined()
|
||||
expect(providers["anthropic"].models["my-sonnet"]).toBeDefined()
|
||||
|
||||
const model = await Provider.getModel("anthropic", "my-sonnet")
|
||||
expect(model).toBeDefined()
|
||||
expect(model.modelID).toBe("my-sonnet")
|
||||
expect(model.info.name).toBe("My Sonnet Alias")
|
||||
expect(model.id).toBe("my-sonnet")
|
||||
expect(model.name).toBe("My Sonnet Alias")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("provider api field sets default baseURL", async () => {
|
||||
test("provider api field sets model api.url", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
@@ -666,7 +667,8 @@ test("provider api field sets default baseURL", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["custom-api"].options.baseURL).toBe("https://api.example.com/v1")
|
||||
// api field is stored on model.api.url, used by getSDK to set baseURL
|
||||
expect(providers["custom-api"].models["model-1"].api.url).toBe("https://api.example.com/v1")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -737,10 +739,10 @@ test("model inherits properties from existing database model", async () => {
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
|
||||
const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
|
||||
expect(model.name).toBe("Custom Name for Sonnet")
|
||||
expect(model.tool_call).toBe(true)
|
||||
expect(model.attachment).toBe(true)
|
||||
expect(model.capabilities.toolcall).toBe(true)
|
||||
expect(model.capabilities.attachment).toBe(true)
|
||||
expect(model.limit.context).toBeGreaterThan(0)
|
||||
},
|
||||
})
|
||||
@@ -820,7 +822,7 @@ test("whitelist and blacklist can be combined", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["anthropic"]).toBeDefined()
|
||||
const models = Object.keys(providers["anthropic"].info.models)
|
||||
const models = Object.keys(providers["anthropic"].models)
|
||||
expect(models).toContain("claude-sonnet-4-20250514")
|
||||
expect(models).not.toContain("claude-opus-4-20250514")
|
||||
expect(models.length).toBe(1)
|
||||
@@ -858,11 +860,9 @@ test("model modalities default correctly", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const model = providers["test-provider"].info.models["test-model"]
|
||||
expect(model.modalities).toEqual({
|
||||
input: ["text"],
|
||||
output: ["text"],
|
||||
})
|
||||
const model = providers["test-provider"].models["test-model"]
|
||||
expect(model.capabilities.input.text).toBe(true)
|
||||
expect(model.capabilities.output.text).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -903,11 +903,11 @@ test("model with custom cost values", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const model = providers["test-provider"].info.models["test-model"]
|
||||
const model = providers["test-provider"].models["test-model"]
|
||||
expect(model.cost.input).toBe(5)
|
||||
expect(model.cost.output).toBe(15)
|
||||
expect(model.cost.cache_read).toBe(2.5)
|
||||
expect(model.cost.cache_write).toBe(7.5)
|
||||
expect(model.cost.cache.read).toBe(2.5)
|
||||
expect(model.cost.cache.write).toBe(7.5)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -931,7 +931,7 @@ test("getSmallModel returns appropriate small model", async () => {
|
||||
fn: async () => {
|
||||
const model = await Provider.getSmallModel("anthropic")
|
||||
expect(model).toBeDefined()
|
||||
expect(model?.modelID).toContain("haiku")
|
||||
expect(model?.id).toContain("haiku")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -957,7 +957,7 @@ test("getSmallModel respects config small_model override", async () => {
|
||||
const model = await Provider.getSmallModel("anthropic")
|
||||
expect(model).toBeDefined()
|
||||
expect(model?.providerID).toBe("anthropic")
|
||||
expect(model?.modelID).toBe("claude-sonnet-4-20250514")
|
||||
expect(model?.id).toBe("claude-sonnet-4-20250514")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1046,7 +1046,7 @@ test("provider with custom npm package", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["local-llm"]).toBeDefined()
|
||||
expect(providers["local-llm"].info.npm).toBe("@ai-sdk/openai-compatible")
|
||||
expect(providers["local-llm"].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible")
|
||||
expect(providers["local-llm"].options.baseURL).toBe("http://localhost:11434/v1")
|
||||
},
|
||||
})
|
||||
@@ -1082,7 +1082,7 @@ test("model alias name defaults to alias key when id differs", async () => {
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["anthropic"].info.models["sonnet"].name).toBe("sonnet")
|
||||
expect(providers["anthropic"].models["sonnet"].name).toBe("sonnet")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1123,8 +1123,8 @@ test("provider with multiple env var options only includes apiKey when single en
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["multi-env"]).toBeDefined()
|
||||
// When multiple env options exist, apiKey should NOT be auto-set
|
||||
expect(providers["multi-env"].options.apiKey).toBeUndefined()
|
||||
// When multiple env options exist, key should NOT be auto-set
|
||||
expect(providers["multi-env"].key).toBeUndefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1165,8 +1165,8 @@ test("provider with single env var includes apiKey automatically", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["single-env"]).toBeDefined()
|
||||
// Single env option should auto-set apiKey
|
||||
expect(providers["single-env"].options.apiKey).toBe("my-api-key")
|
||||
// Single env option should auto-set key
|
||||
expect(providers["single-env"].key).toBe("my-api-key")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1201,7 +1201,7 @@ test("model cost overrides existing cost values", async () => {
|
||||
},
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const model = providers["anthropic"].info.models["claude-sonnet-4-20250514"]
|
||||
const model = providers["anthropic"].models["claude-sonnet-4-20250514"]
|
||||
expect(model.cost.input).toBe(999)
|
||||
expect(model.cost.output).toBe(888)
|
||||
},
|
||||
@@ -1249,11 +1249,11 @@ test("completely new provider not in database can be configured", async () => {
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["brand-new-provider"]).toBeDefined()
|
||||
expect(providers["brand-new-provider"].info.name).toBe("Brand New")
|
||||
const model = providers["brand-new-provider"].info.models["new-model"]
|
||||
expect(model.reasoning).toBe(true)
|
||||
expect(model.attachment).toBe(true)
|
||||
expect(model.modalities?.input).toContain("image")
|
||||
expect(providers["brand-new-provider"].name).toBe("Brand New")
|
||||
const model = providers["brand-new-provider"].models["new-model"]
|
||||
expect(model.capabilities.reasoning).toBe(true)
|
||||
expect(model.capabilities.attachment).toBe(true)
|
||||
expect(model.capabilities.input.image).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1322,7 +1322,7 @@ test("model with tool_call false", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["no-tools"].info.models["basic-model"].tool_call).toBe(false)
|
||||
expect(providers["no-tools"].models["basic-model"].capabilities.toolcall).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1357,7 +1357,7 @@ test("model defaults tool_call to true when not specified", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["default-tools"].info.models["model"].tool_call).toBe(true)
|
||||
expect(providers["default-tools"].models["model"].capabilities.toolcall).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1396,7 +1396,7 @@ test("model headers are preserved", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const model = providers["headers-provider"].info.models["model"]
|
||||
const model = providers["headers-provider"].models["model"]
|
||||
expect(model.headers).toEqual({
|
||||
"X-Custom-Header": "custom-value",
|
||||
Authorization: "Bearer special-token",
|
||||
@@ -1465,8 +1465,8 @@ test("getModel returns consistent results", async () => {
|
||||
const model1 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
|
||||
const model2 = await Provider.getModel("anthropic", "claude-sonnet-4-20250514")
|
||||
expect(model1.providerID).toEqual(model2.providerID)
|
||||
expect(model1.modelID).toEqual(model2.modelID)
|
||||
expect(model1.info).toEqual(model2.info)
|
||||
expect(model1.id).toEqual(model2.id)
|
||||
expect(model1).toEqual(model2)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1501,7 +1501,7 @@ test("provider name defaults to id when not in database", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
expect(providers["my-custom-id"].info.name).toBe("my-custom-id")
|
||||
expect(providers["my-custom-id"].name).toBe("my-custom-id")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1601,7 +1601,7 @@ test("getProvider returns provider info", async () => {
|
||||
fn: async () => {
|
||||
const provider = await Provider.getProvider("anthropic")
|
||||
expect(provider).toBeDefined()
|
||||
expect(provider?.info.id).toBe("anthropic")
|
||||
expect(provider?.id).toBe("anthropic")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1684,7 +1684,7 @@ test("model limit defaults to zero when not specified", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const providers = await Provider.list()
|
||||
const model = providers["no-limit"].info.models["model"]
|
||||
const model = providers["no-limit"].models["model"]
|
||||
expect(model.limit.context).toBe(0)
|
||||
expect(model.limit.output).toBe(0)
|
||||
},
|
||||
|
||||
@@ -942,6 +942,75 @@ export type AgentConfig = {
|
||||
| undefined
|
||||
}
|
||||
|
||||
export type ProviderConfig = {
|
||||
api?: string
|
||||
name?: string
|
||||
env?: Array<string>
|
||||
id?: string
|
||||
npm?: string
|
||||
models?: {
|
||||
[key: string]: {
|
||||
id?: string
|
||||
name?: string
|
||||
release_date?: string
|
||||
attachment?: boolean
|
||||
reasoning?: boolean
|
||||
temperature?: boolean
|
||||
tool_call?: boolean
|
||||
cost?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
context_over_200k?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
}
|
||||
}
|
||||
limit?: {
|
||||
context: number
|
||||
output: number
|
||||
}
|
||||
modalities?: {
|
||||
input: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
output: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
}
|
||||
experimental?: boolean
|
||||
status?: "alpha" | "beta" | "deprecated"
|
||||
options?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
headers?: {
|
||||
[key: string]: string
|
||||
}
|
||||
provider?: {
|
||||
npm: string
|
||||
}
|
||||
}
|
||||
}
|
||||
whitelist?: Array<string>
|
||||
blacklist?: Array<string>
|
||||
options?: {
|
||||
apiKey?: string
|
||||
baseURL?: string
|
||||
/**
|
||||
* GitHub Enterprise URL for copilot authentication
|
||||
*/
|
||||
enterpriseUrl?: string
|
||||
/**
|
||||
* Enable promptCacheKey for this provider (default false)
|
||||
*/
|
||||
setCacheKey?: boolean
|
||||
/**
|
||||
* Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
|
||||
*/
|
||||
timeout?: number | false
|
||||
[key: string]: unknown | string | boolean | (number | false) | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export type McpLocalConfig = {
|
||||
/**
|
||||
* Type of MCP server connection
|
||||
@@ -1100,74 +1169,7 @@ export type Config = {
|
||||
* Custom provider configurations and model overrides
|
||||
*/
|
||||
provider?: {
|
||||
[key: string]: {
|
||||
api?: string
|
||||
name?: string
|
||||
env?: Array<string>
|
||||
id?: string
|
||||
npm?: string
|
||||
models?: {
|
||||
[key: string]: {
|
||||
id?: string
|
||||
name?: string
|
||||
release_date?: string
|
||||
attachment?: boolean
|
||||
reasoning?: boolean
|
||||
temperature?: boolean
|
||||
tool_call?: boolean
|
||||
cost?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
context_over_200k?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
}
|
||||
}
|
||||
limit?: {
|
||||
context: number
|
||||
output: number
|
||||
}
|
||||
modalities?: {
|
||||
input: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
output: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
}
|
||||
experimental?: boolean
|
||||
status?: "alpha" | "beta" | "deprecated"
|
||||
options?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
headers?: {
|
||||
[key: string]: string
|
||||
}
|
||||
provider?: {
|
||||
npm: string
|
||||
}
|
||||
}
|
||||
}
|
||||
whitelist?: Array<string>
|
||||
blacklist?: Array<string>
|
||||
options?: {
|
||||
apiKey?: string
|
||||
baseURL?: string
|
||||
/**
|
||||
* GitHub Enterprise URL for copilot authentication
|
||||
*/
|
||||
enterpriseUrl?: string
|
||||
/**
|
||||
* Enable promptCacheKey for this provider (default false)
|
||||
*/
|
||||
setCacheKey?: boolean
|
||||
/**
|
||||
* Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
|
||||
*/
|
||||
timeout?: number | false
|
||||
[key: string]: unknown | string | boolean | (number | false) | undefined
|
||||
}
|
||||
}
|
||||
[key: string]: ProviderConfig
|
||||
}
|
||||
/**
|
||||
* MCP (Model Context Protocol) server configurations
|
||||
@@ -1354,51 +1356,71 @@ export type Command = {
|
||||
|
||||
export type Model = {
|
||||
id: string
|
||||
providerID: string
|
||||
api: {
|
||||
id: string
|
||||
url: string
|
||||
npm: string
|
||||
}
|
||||
name: string
|
||||
release_date: string
|
||||
attachment: boolean
|
||||
reasoning: boolean
|
||||
temperature: boolean
|
||||
tool_call: boolean
|
||||
capabilities: {
|
||||
temperature: boolean
|
||||
reasoning: boolean
|
||||
attachment: boolean
|
||||
toolcall: boolean
|
||||
input: {
|
||||
text: boolean
|
||||
audio: boolean
|
||||
image: boolean
|
||||
video: boolean
|
||||
pdf: boolean
|
||||
}
|
||||
output: {
|
||||
text: boolean
|
||||
audio: boolean
|
||||
image: boolean
|
||||
video: boolean
|
||||
pdf: boolean
|
||||
}
|
||||
}
|
||||
cost: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
context_over_200k?: {
|
||||
cache: {
|
||||
read: number
|
||||
write: number
|
||||
}
|
||||
experimentalOver200K?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
cache: {
|
||||
read: number
|
||||
write: number
|
||||
}
|
||||
}
|
||||
}
|
||||
limit: {
|
||||
context: number
|
||||
output: number
|
||||
}
|
||||
modalities?: {
|
||||
input: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
output: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
}
|
||||
experimental?: boolean
|
||||
status?: "alpha" | "beta" | "deprecated"
|
||||
status: "alpha" | "beta" | "deprecated" | "active"
|
||||
options: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
headers?: {
|
||||
headers: {
|
||||
[key: string]: string
|
||||
}
|
||||
provider?: {
|
||||
npm: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Provider = {
|
||||
api?: string
|
||||
name: string
|
||||
env: Array<string>
|
||||
id: string
|
||||
npm?: string
|
||||
name: string
|
||||
source: "env" | "config" | "custom" | "api"
|
||||
env: Array<string>
|
||||
key?: string
|
||||
options: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
models: {
|
||||
[key: string]: Model
|
||||
}
|
||||
@@ -2665,7 +2687,55 @@ export type ProviderListResponses = {
|
||||
* List of providers
|
||||
*/
|
||||
200: {
|
||||
all: Array<Provider>
|
||||
all: Array<{
|
||||
api?: string
|
||||
name: string
|
||||
env: Array<string>
|
||||
id: string
|
||||
npm?: string
|
||||
models: {
|
||||
[key: string]: {
|
||||
id: string
|
||||
name: string
|
||||
release_date: string
|
||||
attachment: boolean
|
||||
reasoning: boolean
|
||||
temperature: boolean
|
||||
tool_call: boolean
|
||||
cost?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
context_over_200k?: {
|
||||
input: number
|
||||
output: number
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
}
|
||||
}
|
||||
limit: {
|
||||
context: number
|
||||
output: number
|
||||
}
|
||||
modalities?: {
|
||||
input: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
output: Array<"text" | "audio" | "image" | "video" | "pdf">
|
||||
}
|
||||
experimental?: boolean
|
||||
status?: "alpha" | "beta" | "deprecated"
|
||||
options: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
headers?: {
|
||||
[key: string]: string
|
||||
}
|
||||
provider?: {
|
||||
npm: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}>
|
||||
default: {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user