Update provider configuration and server handling

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
This commit is contained in:
Dax Raad
2025-06-12 23:10:03 -04:00
parent e8c3abc369
commit 442e1b52ad
4 changed files with 123 additions and 103 deletions

View File

@@ -2,6 +2,7 @@ import { Log } from "../util/log"
import { z } from "zod" import { z } from "zod"
import { App } from "../app/app" import { App } from "../app/app"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { ModelsDev } from "../provider/models"
export namespace Config { export namespace Config {
const log = Log.create({ service: "config" }) const log = Log.create({ service: "config" })
@@ -49,7 +50,14 @@ export namespace Config {
export const Info = z export const Info = z
.object({ .object({
provider: z.record(z.string(), z.record(z.string(), z.any())).optional(), provider: z
.record(
ModelsDev.Provider.partial().extend({
models: z.record(ModelsDev.Model.partial()),
options: z.record(z.any()).optional(),
}),
)
.optional(),
tool: z tool: z
.object({ .object({
provider: z.record(z.string(), z.string().array()).optional(), provider: z.record(z.string(), z.string().array()).optional(),

View File

@@ -1,17 +1,45 @@
import { Global } from "../global" import { Global } from "../global"
import { Log } from "../util/log" import { Log } from "../util/log"
import path from "path" import path from "path"
import { z } from "zod"
export namespace ModelsDev { export namespace ModelsDev {
const log = Log.create({ service: "models.dev" }) const log = Log.create({ service: "models.dev" })
const filepath = path.join(Global.Path.cache, "models.json") const filepath = path.join(Global.Path.cache, "models.json")
export const Model = z.object({
name: z.string(),
attachment: z.boolean(),
reasoning: z.boolean(),
temperature: z.boolean(),
cost: z.object({
input: z.number(),
output: z.number(),
inputCached: z.number(),
outputCached: z.number(),
}),
limit: z.object({
context: z.number(),
output: z.number(),
}),
id: z.string(),
})
export type Model = z.infer<typeof Model>
export const Provider = z.object({
name: z.string(),
env: z.array(z.string()),
id: z.string(),
models: z.record(Model),
})
export type Provider = z.infer<typeof Provider>
export async function get() { export async function get() {
const file = Bun.file(filepath) const file = Bun.file(filepath)
const result = await file.json().catch(() => {}) const result = await file.json().catch(() => {})
if (result) { if (result) {
refresh() refresh()
return result return result as Record<string, Provider>
} }
await refresh() await refresh()
return get() return get()

View File

@@ -1,7 +1,7 @@
import z from "zod" import z from "zod"
import { App } from "../app/app" import { App } from "../app/app"
import { Config } from "../config/config" import { Config } from "../config/config"
import { mergeDeep, sortBy } from "remeda" import { mergeDeep, pipe, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai" import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log" import { Log } from "../util/log"
import path from "path" import path from "path"
@@ -29,106 +29,52 @@ import { TaskTool } from "../tool/task"
export namespace Provider { export namespace Provider {
const log = Log.create({ service: "provider" }) const log = Log.create({ service: "provider" })
export const Model = z type CustomLoader = (
.object({ provider: ModelsDev.Provider,
id: z.string(), ) => Promise<Record<string, any> | false>
name: z.string().optional(),
attachment: z.boolean(),
reasoning: z.boolean().optional(),
cost: z.object({
input: z.number(),
inputCached: z.number(),
output: z.number(),
outputCached: z.number(),
}),
limit: z.object({
context: z.number(),
output: z.number(),
}),
})
.openapi({
ref: "Provider.Model",
})
export type Model = z.output<typeof Model>
export const Info = z type Source = "env" | "config" | "custom"
.object({
id: z.string(),
name: z.string(),
models: z.record(z.string(), Model),
})
.openapi({
ref: "Provider.Info",
})
export type Info = z.output<typeof Info>
type Autodetector = (provider: Info) => Promise< const CUSTOM_LOADERS: Record<string, CustomLoader> = {
| {
source: Source
options: Record<string, any>
}
| false
>
function env(...keys: string[]) {
const result: Autodetector = async () => {
for (const key of keys) {
if (process.env[key])
return {
source: "env",
options: {},
}
}
return false
}
return result
}
type Source = "oauth" | "env" | "config" | "api"
const AUTODETECT: Record<string, Autodetector> = {
async anthropic(provider) { async anthropic(provider) {
const access = await AuthAnthropic.access() const access = await AuthAnthropic.access()
if (access) { if (!access) return false
// claude sub doesn't have usage cost for (const model of Object.values(provider.models)) {
for (const model of Object.values(provider.models)) { model.cost = {
model.cost = { input: 0,
input: 0, inputCached: 0,
inputCached: 0, output: 0,
output: 0, outputCached: 0,
outputCached: 0,
}
}
return {
source: "oauth",
options: {
apiKey: "",
headers: {
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
},
},
} }
} }
return env("ANTHROPIC_API_KEY")(provider) return {
source: "oauth",
options: {
apiKey: "",
headers: {
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
},
},
}
}, },
google: env("GOOGLE_GENERATIVE_AI_API_KEY"),
openai: env("OPENAI_API_KEY"),
} }
const state = App.state("provider", async () => { const state = App.state("provider", async () => {
const config = await Config.get() const config = await Config.get()
const database: Record<string, Provider.Info> = await ModelsDev.get() const database = await ModelsDev.get()
const providers: { const providers: {
[providerID: string]: { [providerID: string]: {
source: Source source: Source
info: Provider.Info info: ModelsDev.Provider
options: Record<string, any> options: Record<string, any>
} }
} = {} } = {}
const models = new Map<string, { info: Model; language: LanguageModel }>() const models = new Map<
string,
{ info: ModelsDev.Model; language: LanguageModel }
>()
const sdk = new Map<string, SDK>() const sdk = new Map<string, SDK>()
log.info("loading") log.info("loading")
@@ -142,11 +88,7 @@ export namespace Provider {
if (!provider) { if (!provider) {
providers[id] = { providers[id] = {
source, source,
info: database[id] ?? { info: database[id],
id,
name: id,
models: [],
},
options, options,
} }
return return
@@ -155,22 +97,63 @@ export namespace Provider {
provider.source = source provider.source = source
} }
for (const [providerID, fn] of Object.entries(AUTODETECT)) { for (const [providerID, provider] of Object.entries(
const provider = database[providerID] config.provider ?? {},
if (!provider) continue )) {
const result = await fn(provider) const existing = database[providerID]
if (!result) continue const parsed: ModelsDev.Provider = {
mergeProvider(providerID, result.options, result.source) id: providerID,
name: provider.name ?? existing?.name ?? providerID,
env: provider.env ?? existing?.env ?? [],
models: existing?.models ?? {},
}
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
const existing = parsed.models[modelID]
const parsedModel: ModelsDev.Model = {
id: modelID,
name: model.name ?? existing?.name ?? modelID,
attachment: model.attachment ?? existing?.attachment ?? false,
reasoning: model.reasoning ?? existing?.reasoning ?? false,
temperature: model.temperature ?? existing?.temperature ?? false,
cost: model.cost ??
existing?.cost ?? {
input: 0,
output: 0,
inputCached: 0,
outputCached: 0,
},
limit: model.limit ??
existing?.limit ?? {
context: 0,
output: 0,
},
}
parsed.models[modelID] = parsedModel
}
database[providerID] = parsed
} }
for (const [providerID, info] of Object.entries(await Auth.all())) { // load env
if (info.type === "api") { for (const [providerID, provider] of Object.entries(database)) {
mergeProvider(providerID, { apiKey: info.key }, "api") if (provider.env.some((item) => process.env[item])) {
mergeProvider(providerID, {}, "env")
} }
} }
for (const [providerID, options] of Object.entries(config.provider ?? {})) { // load custom
mergeProvider(providerID, options, "config") for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
const result = await fn(database[providerID])
if (result) {
mergeProvider(providerID, result, "custom")
}
}
// load config
for (const [providerID, provider] of Object.entries(
config.provider ?? {},
)) {
mergeProvider(providerID, provider.options ?? {}, "config")
} }
for (const providerID of Object.keys(providers)) { for (const providerID of Object.keys(providers)) {
@@ -261,7 +244,7 @@ export namespace Provider {
} }
const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"] const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"]
export function sort(models: Model[]) { export function sort(models: ModelsDev.Model[]) {
return sortBy( return sortBy(
models, models,
[ [

View File

@@ -13,6 +13,7 @@ import { Global } from "../global"
import { mapValues } from "remeda" import { mapValues } from "remeda"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { Fzf } from "../external/fzf" import { Fzf } from "../external/fzf"
import { ModelsDev } from "../provider/models"
const ERRORS = { const ERRORS = {
400: { 400: {
@@ -406,7 +407,7 @@ export namespace Server {
"application/json": { "application/json": {
schema: resolver( schema: resolver(
z.object({ z.object({
providers: Provider.Info.array(), providers: ModelsDev.Provider.array(),
default: z.record(z.string(), z.string()), default: z.record(z.string(), z.string()),
}), }),
), ),