diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts index 3e3672926d..4670aa5f2f 100644 --- a/packages/opencode/src/cli/cmd/models.ts +++ b/packages/opencode/src/cli/cmd/models.ts @@ -28,7 +28,7 @@ export const ModelsCommand = cmd({ }, handler: async (args) => { if (args.refresh) { - await ModelsDev.refresh() + await ModelsDev.refresh(true) UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL) } diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 581809e90e..1ab0ecc7bc 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -303,7 +303,7 @@ export const ProvidersLoginCommand = cmd({ prompts.outro("Done") return } - await ModelsDev.refresh().catch(() => {}) + await ModelsDev.refresh(true).catch(() => {}) const config = await Config.get() diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 30901ea74f..c6ab5d8365 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -6,6 +6,8 @@ import { Installation } from "../installation" import { Flag } from "../flag/flag" import { lazy } from "@/util/lazy" import { Filesystem } from "../util/filesystem" +import { Flock } from "@/util/flock" +import { Hash } from "@/util/hash" // Try to import bundled snapshot (generated at build time) // Falls back to undefined in dev mode when snapshot doesn't exist @@ -13,7 +15,12 @@ import { Filesystem } from "../util/filesystem" export namespace ModelsDev { const log = Log.create({ service: "models.dev" }) - const filepath = path.join(Global.Path.cache, "models.json") + 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 export const Model = z.object({ id: z.string(), @@ -85,6 +92,22 @@ export namespace ModelsDev { 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 @@ -94,8 +117,17 @@ export namespace ModelsDev { .catch(() => undefined) if (snapshot) return snapshot if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {} - const json = await fetch(`${url()}/api.json`).then((x) => x.text()) - return JSON.parse(json) + 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() { @@ -103,21 +135,19 @@ export namespace ModelsDev { return result as Record } - export async function refresh() { - const result = await fetch(`${url()}/api.json`, { - headers: { - "User-Agent": Installation.USER_AGENT, - }, - signal: AbortSignal.timeout(10 * 1000), + export async function refresh(force = false) { + if (skip(force)) return ModelsDev.Data.reset() + await Flock.withLock(`models-dev:${filepath}`, async () => { + if (skip(force)) return ModelsDev.Data.reset() + const result = await fetchApi() + if (!result.ok) return + await Filesystem.write(filepath, result.text) + ModelsDev.Data.reset() }).catch((e) => { log.error("Failed to fetch models.dev", { error: e, }) }) - if (result && result.ok) { - await Filesystem.write(filepath, await result.text()) - ModelsDev.Data.reset() - } } }