fix: call models.dev once instead of twice on start (#20765)

pull/20426/merge
Luke Parker 2026-04-03 11:26:53 +10:00 committed by GitHub
parent 8e9e79d276
commit eb6f1dada8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 15 deletions

View File

@ -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)
}

View File

@ -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()

View File

@ -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<string, Provider>
}
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()
}
}
}