Compare commits

...

4 Commits

Author SHA1 Message Date
Colin McDonnell 45fb07b34f bump action to 0.0.190
Made-with: Cursor
2026-04-03 18:56:00 +00:00
Colin McDonnell cbcc83806f fall back mimo-v2-pro-free to big-pickle
Made-with: Cursor
2026-04-03 18:53:34 +00:00
Colin McDonnell 536fae692a update snapshot for google/gemma-4-31b release
Made-with: Cursor
2026-04-03 18:49:24 +00:00
Colin McDonnell b8c4d5b716 add deprecated model fallback chain resolution
models can now be marked `deprecated: true` with a `fallback` slug
pointing to a replacement. `resolveCliModel` follows the chain
recursively (with cycle detection) until it finds a non-deprecated
model. this keeps deprecated models in the registry for backward
compatibility instead of removing them.

marks opencode/mimo-v2-pro-free as deprecated with fallback to
opencode/nemotron-3-super-free.

Made-with: Cursor
2026-04-03 18:45:47 +00:00
7 changed files with 68 additions and 28 deletions
+1 -5
View File
@@ -13,7 +13,7 @@
* security is enforced at the tool layer, not the process layer. * security is enforced at the tool layer, not the process layer.
*/ */
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { mkdirSync, rmSync } from "node:fs"; import { mkdirSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { performance } from "node:perf_hooks"; import { performance } from "node:perf_hooks";
import { ghPullfrogMcpName } from "../external.ts"; import { ghPullfrogMcpName } from "../external.ts";
@@ -627,10 +627,6 @@ export const opentoad = agent({
agent: "opencode", 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"]; const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
// OPENCODE_PERMISSION has absolute highest precedence (merged after managed/MDM configs). // OPENCODE_PERMISSION has absolute highest precedence (merged after managed/MDM configs).
+18 -10
View File
@@ -107842,7 +107842,8 @@ var providers = {
displayName: "MiMo V2 Pro", displayName: "MiMo V2 Pro",
resolve: "opencode/mimo-v2-pro-free", resolve: "opencode/mimo-v2-pro-free",
envVars: [], envVars: [],
isFree: true isFree: true,
fallback: "opencode/big-pickle"
}, },
"minimax-m2.5-free": { "minimax-m2.5-free": {
displayName: "MiniMax M2.5", displayName: "MiniMax M2.5",
@@ -107951,14 +107952,23 @@ var modelAliases = Object.entries(providers).flatMap(
resolve: def.resolve, resolve: def.resolve,
openRouterResolve: def.openRouterResolve, openRouterResolve: def.openRouterResolve,
preferred: def.preferred ?? false, preferred: def.preferred ?? false,
isFree: def.isFree ?? false isFree: def.isFree ?? false,
fallback: def.fallback
})) }))
); );
function resolveModelSlug(slug) { var MAX_FALLBACK_DEPTH = 10;
return modelAliases.find((a) => a.slug === slug)?.resolve;
}
function resolveCliModel(slug) { 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.fallback) return alias.resolve;
current = alias.fallback;
}
return void 0;
} }
// utils/buildPullfrogFooter.ts // utils/buildPullfrogFooter.ts
@@ -144696,7 +144706,7 @@ var import_semver = __toESM(require_semver2(), 1);
// package.json // package.json
var package_default = { var package_default = {
name: "@pullfrog/pullfrog", name: "@pullfrog/pullfrog",
version: "0.0.188", version: "0.0.190",
type: "module", type: "module",
files: [ files: [
"index.js", "index.js",
@@ -149541,7 +149551,7 @@ var claude = agent({
// agents/opentoad.ts // agents/opentoad.ts
import { execFileSync as execFileSync3 } from "node:child_process"; 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 { join as join11 } from "node:path";
import { performance as performance7 } from "node:perf_hooks"; import { performance as performance7 } from "node:perf_hooks";
async function installOpencodeCli() { async function installOpencodeCli() {
@@ -149925,8 +149935,6 @@ var opentoad = agent({
env: homeEnv, env: homeEnv,
agent: "opencode" 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 args2 = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
const permissionOverride = JSON.stringify({ const permissionOverride = JSON.stringify({
external_directory: { "*": "deny", "/tmp/*": "allow" } external_directory: { "*": "deny", "/tmp/*": "allow" }
+24 -2
View File
@@ -22,6 +22,8 @@ export interface ModelAlias {
preferred: boolean; preferred: boolean;
/** whether this alias is free and requires no API key */ /** whether this alias is free and requires no API key */
isFree: boolean; isFree: boolean;
/** slug of a replacement model — presence implies this model is deprecated */
fallback: string | undefined;
} }
interface ModelDef { interface ModelDef {
@@ -33,6 +35,8 @@ interface ModelDef {
preferred?: boolean; preferred?: boolean;
envVars?: readonly string[]; envVars?: readonly string[];
isFree?: boolean; isFree?: boolean;
/** slug of a replacement model — presence implies this model is deprecated */
fallback?: string;
} }
export interface ProviderConfig { export interface ProviderConfig {
@@ -221,6 +225,7 @@ export const providers = {
resolve: "opencode/mimo-v2-pro-free", resolve: "opencode/mimo-v2-pro-free",
envVars: [], envVars: [],
isFree: true, isFree: true,
fallback: "opencode/big-pickle",
}, },
"minimax-m2.5-free": { "minimax-m2.5-free": {
displayName: "MiniMax M2.5", displayName: "MiniMax M2.5",
@@ -348,6 +353,7 @@ export const modelAliases: ModelAlias[] = Object.entries(providers).flatMap(
openRouterResolve: def.openRouterResolve, openRouterResolve: def.openRouterResolve,
preferred: def.preferred ?? false, preferred: def.preferred ?? false,
isFree: def.isFree ?? false, isFree: def.isFree ?? false,
fallback: def.fallback,
})) }))
); );
@@ -358,7 +364,23 @@ export function resolveModelSlug(slug: string): string | undefined {
return modelAliases.find((a) => a.slug === slug)?.resolve; 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 { export function resolveCliModel(slug: string): string | undefined {
return resolveModelSlug(slug); let current = slug;
const visited = new Set<string>();
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.fallback) return alias.resolve;
current = alias.fallback;
}
return undefined;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@pullfrog/pullfrog", "name": "@pullfrog/pullfrog",
"version": "0.0.188", "version": "0.0.190",
"type": "module", "type": "module",
"files": [ "files": [
"index.js", "index.js",
+5 -3
View File
@@ -37686,7 +37686,8 @@ var providers = {
displayName: "MiMo V2 Pro", displayName: "MiMo V2 Pro",
resolve: "opencode/mimo-v2-pro-free", resolve: "opencode/mimo-v2-pro-free",
envVars: [], envVars: [],
isFree: true isFree: true,
fallback: "opencode/big-pickle"
}, },
"minimax-m2.5-free": { "minimax-m2.5-free": {
displayName: "MiniMax M2.5", displayName: "MiniMax M2.5",
@@ -37773,7 +37774,8 @@ var modelAliases = Object.entries(providers).flatMap(
resolve: def.resolve, resolve: def.resolve,
openRouterResolve: def.openRouterResolve, openRouterResolve: def.openRouterResolve,
preferred: def.preferred ?? false, preferred: def.preferred ?? false,
isFree: def.isFree ?? false isFree: def.isFree ?? false,
fallback: def.fallback
})) }))
); );
@@ -41557,7 +41559,7 @@ var core3 = __toESM(require_core(), 1);
// package.json // package.json
var package_default = { var package_default = {
name: "@pullfrog/pullfrog", name: "@pullfrog/pullfrog",
version: "0.0.188", version: "0.0.190",
type: "module", type: "module",
files: [ files: [
"index.js", "index.js",
+2 -2
View File
@@ -11,8 +11,8 @@ exports[`latest model per provider snapshot > matches snapshot 1`] = `
"releaseDate": "2025-12-01", "releaseDate": "2025-12-01",
}, },
"google": { "google": {
"modelId": "gemini-3.1-flash-lite-preview", "modelId": "gemma-4-31b",
"releaseDate": "2026-03-03", "releaseDate": "2026-04-02",
}, },
"moonshotai": { "moonshotai": {
"modelId": "kimi-k2.5", "modelId": "kimi-k2.5",
+17 -5
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; 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 = { type ModelsDevModel = {
name: string; name: string;
@@ -38,10 +38,22 @@ describe("models.dev validity", async () => {
).toBeDefined(); ).toBeDefined();
}); });
it(`${alias.resolve} is not deprecated`, () => { if (!alias.fallback) {
const model = data[parsed.provider]?.models[parsed.modelId]; it(`${alias.resolve} is not deprecated`, () => {
if (!model) return; // covered by existence test above const model = data[parsed.provider]?.models[parsed.modelId];
expect(model.status, `${alias.resolve} is deprecated on models.dev`).not.toBe("deprecated"); if (!model) return; // covered by existence test above
expect(model.status, `${alias.resolve} is deprecated on models.dev`).not.toBe("deprecated");
});
}
}
for (const alias of modelAliases.filter((a) => a.fallback)) {
it(`${alias.slug} fallback chain resolves to a non-deprecated model`, () => {
const resolved = resolveCliModel(alias.slug);
expect(
resolved,
`fallback chain for "${alias.slug}" does not resolve to a non-deprecated model`
).toBeDefined();
}); });
} }
}); });