705f792e87
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.
175 lines
5.1 KiB
TypeScript
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()
|
|
}
|