Files
neuron/packages/opencode/src/provider/models.ts
T
Dax Raad 705f792e87 core: move Global module to @opencode-ai/core for centralized path management
Move the Global module from packages/opencode/src/global to packages/core/src/global
to provide a unified location for managing XDG directories and application paths.
This eliminates duplicate path definitions across packages and ensures consistent
access to data, config, cache, state, log, and bin directories throughout the codebase.
2026-04-25 13:52:32 -04:00

175 lines
5.1 KiB
TypeScript

import { Global } from "@opencode-ai/core/global"
import { Log } from "../util"
import path from "path"
import { Schema } from "effect"
import { Installation } from "../installation"
import { Flag } from "@opencode-ai/core/flag/flag"
import { lazy } from "@/util/lazy"
import { Filesystem } from "../util"
import { Flock } from "@opencode-ai/core/util/flock"
import { Hash } from "@opencode-ai/core/util/hash"
// Try to import bundled snapshot (generated at build time)
// Falls back to undefined in dev mode when snapshot doesn't exist
/* @ts-ignore */
const log = Log.create({ service: "models.dev" })
const source = url()
const filepath = path.join(
Global.Path.cache,
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
)
const ttl = 5 * 60 * 1000
const Cost = Schema.Struct({
input: Schema.Number,
output: Schema.Number,
cache_read: Schema.optional(Schema.Number),
cache_write: Schema.optional(Schema.Number),
context_over_200k: Schema.optional(
Schema.Struct({
input: Schema.Number,
output: Schema.Number,
cache_read: Schema.optional(Schema.Number),
cache_write: Schema.optional(Schema.Number),
}),
),
})
export const Model = Schema.Struct({
id: Schema.String,
name: Schema.String,
family: Schema.optional(Schema.String),
release_date: Schema.String,
attachment: Schema.Boolean,
reasoning: Schema.Boolean,
temperature: Schema.Boolean,
tool_call: Schema.Boolean,
interleaved: Schema.optional(
Schema.Union([
Schema.Literal(true),
Schema.Struct({
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
}),
]),
),
cost: Schema.optional(Cost),
limit: Schema.Struct({
context: Schema.Number,
input: Schema.optional(Schema.Number),
output: Schema.Number,
}),
modalities: Schema.optional(
Schema.Struct({
input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])),
}),
),
experimental: Schema.optional(
Schema.Struct({
modes: Schema.optional(
Schema.Record(
Schema.String,
Schema.Struct({
cost: Schema.optional(Cost),
provider: Schema.optional(
Schema.Struct({
body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
}),
),
}),
),
),
}),
),
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])),
provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
),
})
export type Model = Schema.Schema.Type<typeof Model>
export const Provider = Schema.Struct({
api: Schema.optional(Schema.String),
name: Schema.String,
env: Schema.Array(Schema.String),
id: Schema.String,
npm: Schema.optional(Schema.String),
models: Schema.Record(Schema.String, Model),
})
export type Provider = Schema.Schema.Type<typeof Provider>
function url() {
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
}
function fresh() {
return Date.now() - Number(Filesystem.stat(filepath)?.mtimeMs ?? 0) < ttl
}
function skip(force: boolean) {
return !force && fresh()
}
const fetchApi = async () => {
const result = await fetch(`${url()}/api.json`, {
headers: { "User-Agent": Installation.USER_AGENT },
signal: AbortSignal.timeout(10000),
})
return { ok: result.ok, text: await result.text() }
}
export const Data = lazy(async () => {
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
if (result) return result
// @ts-ignore
const snapshot = await import("./models-snapshot.js")
.then((m) => m.snapshot as Record<string, unknown>)
.catch(() => undefined)
if (snapshot) return snapshot
if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {}
return Flock.withLock(`models-dev:${filepath}`, async () => {
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
if (result) return result
const result2 = await fetchApi()
if (result2.ok) {
await Filesystem.write(filepath, result2.text).catch((e) => {
log.error("Failed to write models cache", { error: e })
})
}
return JSON.parse(result2.text)
})
})
export async function get() {
const result = await Data()
return result as Record<string, Provider>
}
export async function refresh(force = false) {
if (skip(force)) return Data.reset()
await Flock.withLock(`models-dev:${filepath}`, async () => {
if (skip(force)) return Data.reset()
const result = await fetchApi()
if (!result.ok) return
await Filesystem.write(filepath, result.text)
Data.reset()
}).catch((e) => {
log.error("Failed to fetch models.dev", {
error: e,
})
})
}
if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
void refresh()
setInterval(
async () => {
await refresh()
},
60 * 1000 * 60,
).unref()
}