Update provider configuration and server handling
🤖 Generated with [OpenCode](https://opencode.ai) Co-Authored-By: OpenCode <noreply@opencode.ai>
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,69 +29,16 @@ 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,
|
||||||
@@ -110,25 +57,24 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return env("ANTHROPIC_API_KEY")(provider)
|
|
||||||
},
|
},
|
||||||
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 [providerID, info] of Object.entries(await Auth.all())) {
|
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
||||||
if (info.type === "api") {
|
const existing = parsed.models[modelID]
|
||||||
mergeProvider(providerID, { apiKey: info.key }, "api")
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// load env
|
||||||
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
|
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,
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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()),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user