1a734adb4d
Moves effect logging, observability, runtime utilities, flags, installation version info, and process utilities from opencode to core package. This enables better code sharing across packages and establishes core as the single source of truth for foundational utilities. All internal imports updated to use @opencode-ai/core paths for consistency.
175 lines
5.1 KiB
TypeScript
175 lines
5.1 KiB
TypeScript
import { Global } from "../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()
|
|
}
|