diff --git a/agents/opentoad.ts b/agents/opentoad.ts index d251ca9..db8bb02 100644 --- a/agents/opentoad.ts +++ b/agents/opentoad.ts @@ -13,7 +13,7 @@ * security is enforced at the tool layer, not the process layer. */ import { execFileSync } from "node:child_process"; -import { mkdirSync, rmSync } from "node:fs"; +import { mkdirSync } from "node:fs"; import { join } from "node:path"; import { performance } from "node:perf_hooks"; import { ghPullfrogMcpName } from "../external.ts"; @@ -627,10 +627,6 @@ export const opentoad = agent({ agent: "opencode", }); - // remove untrusted project plugins/tools before agent starts (no env var to disable loading) - rmSync(join(process.cwd(), ".opencode", "plugins"), { recursive: true, force: true }); - rmSync(join(process.cwd(), ".opencode", "tools"), { recursive: true, force: true }); - const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"]; // OPENCODE_PERMISSION has absolute highest precedence (merged after managed/MDM configs). diff --git a/entry b/entry index 295ecbd..dc6e235 100755 --- a/entry +++ b/entry @@ -107842,7 +107842,9 @@ var providers = { displayName: "MiMo V2 Pro", resolve: "opencode/mimo-v2-pro-free", envVars: [], - isFree: true + isFree: true, + deprecated: true, + fallback: "opencode/nemotron-3-super-free" }, "minimax-m2.5-free": { displayName: "MiniMax M2.5", @@ -107951,14 +107953,25 @@ var modelAliases = Object.entries(providers).flatMap( resolve: def.resolve, openRouterResolve: def.openRouterResolve, preferred: def.preferred ?? false, - isFree: def.isFree ?? false + isFree: def.isFree ?? false, + deprecated: def.deprecated ?? false, + fallback: def.fallback })) ); -function resolveModelSlug(slug) { - return modelAliases.find((a) => a.slug === slug)?.resolve; -} +var MAX_FALLBACK_DEPTH = 10; function resolveCliModel(slug) { - return resolveModelSlug(slug); + let current = slug; + const visited = /* @__PURE__ */ new Set(); + for (let i = 0; i < MAX_FALLBACK_DEPTH; i++) { + if (visited.has(current)) return void 0; + visited.add(current); + const alias = modelAliases.find((a) => a.slug === current); + if (!alias) return void 0; + if (!alias.deprecated) return alias.resolve; + if (!alias.fallback) return void 0; + current = alias.fallback; + } + return void 0; } // utils/buildPullfrogFooter.ts @@ -144696,7 +144709,7 @@ var import_semver = __toESM(require_semver2(), 1); // package.json var package_default = { name: "@pullfrog/pullfrog", - version: "0.0.188", + version: "0.0.189", type: "module", files: [ "index.js", @@ -149541,7 +149554,7 @@ var claude = agent({ // agents/opentoad.ts import { execFileSync as execFileSync3 } from "node:child_process"; -import { mkdirSync as mkdirSync4, rmSync } from "node:fs"; +import { mkdirSync as mkdirSync4 } from "node:fs"; import { join as join11 } from "node:path"; import { performance as performance7 } from "node:perf_hooks"; async function installOpencodeCli() { @@ -149925,8 +149938,6 @@ var opentoad = agent({ env: homeEnv, agent: "opencode" }); - rmSync(join11(process.cwd(), ".opencode", "plugins"), { recursive: true, force: true }); - rmSync(join11(process.cwd(), ".opencode", "tools"), { recursive: true, force: true }); const args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"]; const permissionOverride = JSON.stringify({ external_directory: { "*": "deny", "/tmp/*": "allow" } diff --git a/models.ts b/models.ts index b488b45..a7c8d89 100644 --- a/models.ts +++ b/models.ts @@ -22,6 +22,10 @@ export interface ModelAlias { preferred: boolean; /** whether this alias is free and requires no API key */ isFree: boolean; + /** model is deprecated on models.dev — resolve via fallback chain instead */ + deprecated: boolean; + /** slug of another model to resolve to when this one is deprecated */ + fallback: string | undefined; } interface ModelDef { @@ -33,6 +37,10 @@ interface ModelDef { preferred?: boolean; envVars?: readonly string[]; isFree?: boolean; + /** model is deprecated on models.dev — kept for backward compatibility, resolved via fallback */ + deprecated?: boolean; + /** slug of another model to fall back to (e.g. "opencode/nemotron-3-super-free") */ + fallback?: string; } export interface ProviderConfig { @@ -221,6 +229,8 @@ export const providers = { resolve: "opencode/mimo-v2-pro-free", envVars: [], isFree: true, + deprecated: true, + fallback: "opencode/nemotron-3-super-free", }, "minimax-m2.5-free": { displayName: "MiniMax M2.5", @@ -348,6 +358,8 @@ export const modelAliases: ModelAlias[] = Object.entries(providers).flatMap( openRouterResolve: def.openRouterResolve, preferred: def.preferred ?? false, isFree: def.isFree ?? false, + deprecated: def.deprecated ?? false, + fallback: def.fallback, })) ); @@ -358,7 +370,24 @@ export function resolveModelSlug(slug: string): string | undefined { return modelAliases.find((a) => a.slug === slug)?.resolve; } -/** resolve a model slug to the CLI-ready model string (full models.dev specifier) */ +const MAX_FALLBACK_DEPTH = 10; + +/** + * resolve a model slug to the CLI-ready model string, following the fallback + * chain when a model is deprecated. returns the first non-deprecated resolve + * target, or undefined if the chain is exhausted or broken. + */ export function resolveCliModel(slug: string): string | undefined { - return resolveModelSlug(slug); + let current = slug; + const visited = new Set(); + for (let i = 0; i < MAX_FALLBACK_DEPTH; i++) { + if (visited.has(current)) return undefined; + visited.add(current); + const alias = modelAliases.find((a) => a.slug === current); + if (!alias) return undefined; + if (!alias.deprecated) return alias.resolve; + if (!alias.fallback) return undefined; + current = alias.fallback; + } + return undefined; } diff --git a/package.json b/package.json index 4f9e863..4660533 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pullfrog/pullfrog", - "version": "0.0.188", + "version": "0.0.189", "type": "module", "files": [ "index.js", diff --git a/post b/post index 67f7fe2..183f6e3 100755 --- a/post +++ b/post @@ -37686,7 +37686,9 @@ var providers = { displayName: "MiMo V2 Pro", resolve: "opencode/mimo-v2-pro-free", envVars: [], - isFree: true + isFree: true, + deprecated: true, + fallback: "opencode/nemotron-3-super-free" }, "minimax-m2.5-free": { displayName: "MiniMax M2.5", @@ -37773,7 +37775,9 @@ var modelAliases = Object.entries(providers).flatMap( resolve: def.resolve, openRouterResolve: def.openRouterResolve, preferred: def.preferred ?? false, - isFree: def.isFree ?? false + isFree: def.isFree ?? false, + deprecated: def.deprecated ?? false, + fallback: def.fallback })) ); @@ -41557,7 +41561,7 @@ var core3 = __toESM(require_core(), 1); // package.json var package_default = { name: "@pullfrog/pullfrog", - version: "0.0.188", + version: "0.0.189", type: "module", files: [ "index.js", diff --git a/test/models.test.ts b/test/models.test.ts index d5eabd3..04b01f1 100644 --- a/test/models.test.ts +++ b/test/models.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { type ModelProvider, modelAliases, providers } from "../models.ts"; +import { type ModelProvider, modelAliases, providers, resolveCliModel } from "../models.ts"; type ModelsDevModel = { name: string; @@ -38,10 +38,31 @@ describe("models.dev validity", async () => { ).toBeDefined(); }); - it(`${alias.resolve} is not deprecated`, () => { - const model = data[parsed.provider]?.models[parsed.modelId]; - if (!model) return; // covered by existence test above - expect(model.status, `${alias.resolve} is deprecated on models.dev`).not.toBe("deprecated"); + if (!alias.deprecated) { + it(`${alias.resolve} is not deprecated`, () => { + const model = data[parsed.provider]?.models[parsed.modelId]; + if (!model) return; // covered by existence test above + expect(model.status, `${alias.resolve} is deprecated on models.dev`).not.toBe("deprecated"); + }); + } + } + + // deprecated models must have a fallback that resolves to a non-deprecated model + const deprecatedAliases = modelAliases.filter((a) => a.deprecated); + for (const alias of deprecatedAliases) { + it(`${alias.slug} (deprecated) has a fallback`, () => { + expect( + alias.fallback, + `deprecated model "${alias.slug}" is missing a fallback` + ).toBeDefined(); + }); + + it(`${alias.slug} (deprecated) fallback chain resolves`, () => { + const resolved = resolveCliModel(alias.slug); + expect( + resolved, + `fallback chain for "${alias.slug}" does not resolve to a non-deprecated model` + ).toBeDefined(); }); } });