bump model registry for deepseek v4, kimi k2.6, claude opus 4.7 (#554)

* bump model registry for deepseek v4, kimi k2.6, claude opus 4.7

deepseek released v4 (pro/flash) on 2026-04-24 as a generational replacement
for v3-era reasoner/chat. deepseek will fully retire deepseek-chat and
deepseek-reasoner on 2026-07-24 — both already route server-side to v4-flash.
introduce deepseek-pro (preferred) and deepseek-flash slugs and mark the
legacy aliases deprecated via fallback so existing users transparently
upgrade. mirror on the openrouter side.

also bump moonshotai/kimi to k2.6 (from k2.5, 2026-04-21 release) and bump
the anthropic claude-opus openrouter resolves to 4.7 (we'd already moved the
native side to claude-opus-4-7 but openrouter resolves still pointed at 4.6).
update OSS_PROXY_MODEL fallback and stale doc reference accordingly.

snapshot regenerated; all 111 catalog tests + 66 unit tests pass.

* walk fallback chain when resolving the OSS proxy model

the OSS proxy path in run-context/route.ts read alias.openRouterResolve
directly, bypassing the fallback chain. so an OSS repo configured with
deepseek/deepseek-reasoner kept proxying to openrouter/deepseek/deepseek-v3.2
instead of resolving through the new fallback to openrouter/deepseek-v4-pro.
that worked today (v3.2 routes server-side to V4-Flash) but breaks when
deepseek and openrouter retire v3.2 alongside the 2026-07-24 deprecation.

extract the chain walk into a private resolveTerminalAlias helper and add
resolveOpenRouterModel that mirrors resolveCliModel but returns
openRouterResolve. fallback semantics now apply uniformly across both
runtime resolution paths.

* hide deprecated aliases from model selector dropdowns

aliases with a fallback (currently deepseek-reasoner / deepseek-chat /
openrouter/deepseek-chat) should not be selectable from the model dropdown
or the interactive cli model picker — they're a transition path, not a
choice. but if a repo already has a deprecated slug stored in the db, the
selector trigger still resolves it against the full alias registry so the
display name renders correctly until the user opens the menu and picks a
new model.

verified manually: deepseek submenu shows pro+flash only, openrouter submenu
shows pro+flash but no chat, and a deprecated stored value still renders
its full display name in the trigger.

* ci: run models-live on PRs that touch resolution files

Previously the per-alias smoke matrix only fired on push-to-main, so
resolution-affecting PRs (this one included) shipped without ever
exercising the agent harness against the real provider for each alias.

Loosen the gate on the `aliases` step in the `changes` job to fire
whenever the `models` paths-filter matches (action/models.ts,
action/package.json, action/agents/**) — same set that already drives
the comment about "resolution-affecting files". `models-live` itself
is unchanged: it still keys on a non-empty matrix.

`models-catalog` stays gated to main-push intentionally — its existing
comment justifies that (transient upstream catalog drift shouldn't
block PRs).

* relabel codex aliases as GPT, bump to 5.5 family, add gpt-pro

OpenAI retired the "-codex" model suffix on 2026-07-23 (gpt-5.3-codex,
gpt-5.1-codex-mini, gpt-5.2-codex et al all shut down) and unified the
codex+gpt lines into a single family at gpt-5.4. Per OpenAI's own
deprecation table, every "-codex" substitute is plain gpt-5.x — no
future Codex-suffixed frontier models are coming.

Keep the existing slugs for DB stability (no migration needed) but roll
displayName + resolve forward across openai, opencode, and openrouter:

- openai/gpt-codex       → "GPT"      → openai/gpt-5.5
- openai/gpt-codex-mini  → "GPT Mini" → openai/gpt-5.4-mini
- openai/gpt-pro (new)   → "GPT Pro"  → openai/gpt-5.5-pro

Same relabel + new gpt-pro slug for opencode/* and openrouter/*.
gpt-5.5 (and gpt-5.5-pro) hit the OpenAI public API on 2026-04-24,
day after launch — both are live on OpenRouter as well.

There's no gpt-5.5-mini yet (analysts speculate late June – mid August
based on the gpt-5.4-mini cycle), so "GPT Mini" stays at gpt-5.4-mini
for now; one-line bump when the smaller variant ships.

Also pick up unrelated upstream catalog drift in the snapshot
(xai/grok-4.3 released 2026-05-01, openrouter/poolside laguna).

* deprecate gpt-codex aliases, mint gpt/gpt-pro/gpt-mini, render terminal alias in UI

The previous commit relabeled gpt-codex/gpt-codex-mini in place ("GPT" /
"GPT Mini") so a single slug carried two different identities. That worked
but was self-contradictory: the slug name no longer described the model.

Switch to the same shape we use for the deepseek V3→V4 transition:

- Mint new live slugs: openai/gpt, openai/gpt-pro, openai/gpt-mini
  (mirrored on opencode/* and openrouter/*)
- Restore honest deprecated state on gpt-codex/gpt-codex-mini —
  displayName "GPT Codex" / "GPT Codex Mini", original 5.3-codex /
  5.1-codex-mini resolves, fallback set to the new gpt / gpt-mini slugs
- resolveCliModel + resolveOpenRouterModel walk the chain (existing
  machinery), so DB rows holding "openai/gpt-codex" transparently route
  to gpt-5.5 with no migration

UI render contract: display sites resolve to the *terminal* alias so a
deprecated stored slug shows the model the user is actually running, not
the historical name. Three call sites updated:

- components/ModelSelector.tsx (dropdown trigger label + provider label)
- action/utils/buildPullfrogFooter.ts (PR-comment "Using `X`" footer)
- action/commands/init.ts ("using model X" startup line)

Promoted internal resolveTerminalAlias → exported resolveDisplayAlias so
all three sites use the same primitive (also re-exported from external.ts
+ internal/index.ts so the Next.js app can import it).

Selectable lists (dropdown options, init picker) still filter on
!a.fallback so deprecated slugs never appear as fresh choices — only
deprecated stored values render.

wiki/model-resolution.md: replaced the muddled "slug names outlive
product names" bullet with a clear decision table for in-place bump
(generational, e.g. Opus 4.6 → 4.7) vs. deprecate+replace (vendor
restructures, e.g. codex → unified GPT, deepseek V3 → V4). Documents
the UI render contract too.

models-live CI matrix will smoke-test all 6 new slugs (gpt, gpt-pro,
gpt-mini × openai/opencode/openrouter) plus the 6 deprecated codex slugs
(which resolve through fallback to the same terminal targets) — 12 jobs
total against real provider APIs.

* wiki: slugs are evergreen, resolves are versioned

Document the slug-naming rule explicitly so future entries don't repeat
the deepseek-chat/deepseek-reasoner mistake (mirroring an upstream's
versioned/product-line-specific ID into the slug). Slugs should track
brand-style tier names that survive major version bumps; embedding
versions is the resolve string's job.
This commit is contained in:
Colin McDonnell
2026-05-03 20:03:50 +00:00
committed by pullfrog[bot]
parent 6607112d0b
commit 3bacf01e48
7 changed files with 230 additions and 37 deletions
+7 -3
View File
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
import * as p from "@clack/prompts"; import * as p from "@clack/prompts";
import arg from "arg"; import arg from "arg";
import pc from "picocolors"; import pc from "picocolors";
import { modelAliases, type ProviderConfig, providers } from "../models.ts"; import { modelAliases, type ProviderConfig, providers, resolveDisplayAlias } from "../models.ts";
const PULLFROG_API_URL = (process.env.PULLFROG_API_URL || "https://pullfrog.com").replace( const PULLFROG_API_URL = (process.env.PULLFROG_API_URL || "https://pullfrog.com").replace(
/\/+$/, /\/+$/,
@@ -24,7 +24,7 @@ function buildProviders(): CliProvider[] {
return Object.entries(providers) return Object.entries(providers)
.filter(([key]) => key !== "opencode" && key !== "openrouter") .filter(([key]) => key !== "opencode" && key !== "openrouter")
.map(([key, config]: [string, ProviderConfig]) => { .map(([key, config]: [string, ProviderConfig]) => {
const aliases = modelAliases.filter((a) => a.provider === key); const aliases = modelAliases.filter((a) => a.provider === key && !a.fallback);
const recommended = aliases.find((a) => a.preferred); const recommended = aliases.find((a) => a.preferred);
const sorted = [...aliases].sort((a, b) => { const sorted = [...aliases].sort((a, b) => {
if (a.preferred && !b.preferred) return -1; if (a.preferred && !b.preferred) return -1;
@@ -796,8 +796,12 @@ async function main() {
const resolved = resolveModelProvider(secrets.model); const resolved = resolveModelProvider(secrets.model);
if (!resolved) bail(`unknown model provider: ${secrets.model}`); if (!resolved) bail(`unknown model provider: ${secrets.model}`);
provider = resolved; provider = resolved;
// walk the fallback chain so a deprecated stored slug shows the model
// the run will actually execute against (e.g. "GPT", not "GPT Codex").
const displayAlias = resolveDisplayAlias(secrets.model);
const label = displayAlias ? displayAlias.displayName : secrets.model;
spin.start(""); spin.start("");
spin.stop(`using model ${pc.cyan(secrets.model)}`); spin.stop(`using model ${pc.cyan(label)}`);
} else { } else {
const providerId = await p.select({ const providerId = await p.select({
message: "select your preferred model provider", message: "select your preferred model provider",
+2
View File
@@ -36,7 +36,9 @@ export {
parseModel, parseModel,
providers, providers,
resolveCliModel, resolveCliModel,
resolveDisplayAlias,
resolveModelSlug, resolveModelSlug,
resolveOpenRouterModel,
} from "./models.ts"; } from "./models.ts";
// tool permission types shared with server dispatch // tool permission types shared with server dispatch
+2
View File
@@ -24,7 +24,9 @@ export {
providers, providers,
pullfrogMcpName, pullfrogMcpName,
resolveCliModel, resolveCliModel,
resolveDisplayAlias,
resolveModelSlug, resolveModelSlug,
resolveOpenRouterModel,
} from "../external.ts"; } from "../external.ts";
export type { Mode } from "../modes.ts"; export type { Mode } from "../modes.ts";
export { modes } from "../modes.ts"; export { modes } from "../modes.ts";
+78 -3
View File
@@ -6,7 +6,9 @@ import {
parseModel, parseModel,
providers, providers,
resolveCliModel, resolveCliModel,
resolveDisplayAlias,
resolveModelSlug, resolveModelSlug,
resolveOpenRouterModel,
} from "./models.ts"; } from "./models.ts";
describe("parseModel", () => { describe("parseModel", () => {
@@ -28,7 +30,7 @@ describe("parseModel", () => {
describe("getModelProvider", () => { describe("getModelProvider", () => {
it("extracts provider from slug", () => { it("extracts provider from slug", () => {
expect(getModelProvider("anthropic/claude-opus")).toBe("anthropic"); expect(getModelProvider("anthropic/claude-opus")).toBe("anthropic");
expect(getModelProvider("openai/gpt-codex")).toBe("openai"); expect(getModelProvider("openai/gpt")).toBe("openai");
expect(getModelProvider("google/gemini-pro")).toBe("google"); expect(getModelProvider("google/gemini-pro")).toBe("google");
}); });
}); });
@@ -71,8 +73,12 @@ describe("resolveModelSlug", () => {
}); });
it("resolves openai alias", () => { it("resolves openai alias", () => {
const resolved = resolveModelSlug("openai/gpt-codex"); const resolved = resolveModelSlug("openai/gpt");
expect(resolved).toBe("openai/gpt-5.3-codex"); expect(resolved).toBe("openai/gpt-5.5");
});
it("returns the raw resolve for deprecated aliases (does not walk fallback)", () => {
expect(resolveModelSlug("openai/gpt-codex")).toBe("openai/gpt-5.3-codex");
}); });
it("returns undefined for unknown slug", () => { it("returns undefined for unknown slug", () => {
@@ -89,6 +95,75 @@ describe("resolveCliModel", () => {
it("returns undefined for unknown slug", () => { it("returns undefined for unknown slug", () => {
expect(resolveCliModel("bogus/nope")).toBeUndefined(); expect(resolveCliModel("bogus/nope")).toBeUndefined();
}); });
it("walks fallback chain for deprecated deepseek aliases", () => {
expect(resolveCliModel("deepseek/deepseek-reasoner")).toBe("deepseek/deepseek-v4-pro");
expect(resolveCliModel("deepseek/deepseek-chat")).toBe("deepseek/deepseek-v4-flash");
});
it("walks fallback chain for deprecated openai codex aliases", () => {
expect(resolveCliModel("openai/gpt-codex")).toBe("openai/gpt-5.5");
expect(resolveCliModel("openai/gpt-codex-mini")).toBe("openai/gpt-5.4-mini");
expect(resolveCliModel("opencode/gpt-codex")).toBe("opencode/gpt-5.5");
expect(resolveCliModel("openrouter/gpt-codex")).toBe("openrouter/openai/gpt-5.5");
});
});
describe("resolveDisplayAlias", () => {
it("returns the alias itself for a non-deprecated slug", () => {
const alias = resolveDisplayAlias("anthropic/claude-opus");
expect(alias?.slug).toBe("anthropic/claude-opus");
expect(alias?.displayName).toBe("Claude Opus");
});
it("walks fallback chain to terminal alias for deprecated slug", () => {
const alias = resolveDisplayAlias("openai/gpt-codex");
expect(alias?.slug).toBe("openai/gpt");
expect(alias?.displayName).toBe("GPT");
});
it("walks fallback chain for deepseek-reasoner -> deepseek-pro", () => {
const alias = resolveDisplayAlias("deepseek/deepseek-reasoner");
expect(alias?.slug).toBe("deepseek/deepseek-pro");
expect(alias?.displayName).toBe("DeepSeek Pro");
});
it("returns undefined for unknown slug", () => {
expect(resolveDisplayAlias("bogus/nope")).toBeUndefined();
});
});
describe("resolveOpenRouterModel", () => {
it("returns the openrouter specifier for a non-deprecated alias", () => {
expect(resolveOpenRouterModel("anthropic/claude-opus")).toBe(
"openrouter/anthropic/claude-opus-4.7"
);
});
it("walks fallback chain for deprecated deepseek aliases", () => {
expect(resolveOpenRouterModel("deepseek/deepseek-reasoner")).toBe(
"openrouter/deepseek/deepseek-v4-pro"
);
expect(resolveOpenRouterModel("deepseek/deepseek-chat")).toBe(
"openrouter/deepseek/deepseek-v4-flash"
);
expect(resolveOpenRouterModel("openrouter/deepseek-chat")).toBe(
"openrouter/deepseek/deepseek-v4-flash"
);
});
it("walks fallback chain for deprecated openai codex aliases", () => {
expect(resolveOpenRouterModel("openai/gpt-codex")).toBe("openrouter/openai/gpt-5.5");
expect(resolveOpenRouterModel("openai/gpt-codex-mini")).toBe("openrouter/openai/gpt-5.4-mini");
});
it("returns undefined for free opencode models with no openrouter equivalent", () => {
expect(resolveOpenRouterModel("opencode/big-pickle")).toBeUndefined();
});
it("returns undefined for unknown slug", () => {
expect(resolveOpenRouterModel("bogus/nope")).toBeUndefined();
});
}); });
describe("modelAliases registry", () => { describe("modelAliases registry", () => {
+125 -17
View File
@@ -59,7 +59,7 @@ export const providers = {
"claude-opus": { "claude-opus": {
displayName: "Claude Opus", displayName: "Claude Opus",
resolve: "anthropic/claude-opus-4-7", resolve: "anthropic/claude-opus-4-7",
openRouterResolve: "openrouter/anthropic/claude-opus-4.6", openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
preferred: true, preferred: true,
}, },
"claude-sonnet": { "claude-sonnet": {
@@ -78,16 +78,38 @@ export const providers = {
displayName: "OpenAI", displayName: "OpenAI",
envVars: ["OPENAI_API_KEY"], envVars: ["OPENAI_API_KEY"],
models: { models: {
gpt: {
displayName: "GPT",
resolve: "openai/gpt-5.5",
openRouterResolve: "openrouter/openai/gpt-5.5",
preferred: true,
},
"gpt-pro": {
displayName: "GPT Pro",
resolve: "openai/gpt-5.5-pro",
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
},
"gpt-mini": {
displayName: "GPT Mini",
resolve: "openai/gpt-5.4-mini",
openRouterResolve: "openrouter/openai/gpt-5.4-mini",
},
// legacy aliases — openai unified the codex line into the main GPT family
// and is shutting down every "-codex" snapshot on 2026-07-23. transparently
// upgrade existing users via the fallback chain. UI display sites resolve
// to the terminal alias's label (so dropdown trigger + PR footers show
// "GPT" / "GPT Mini", not the historical name).
"gpt-codex": { "gpt-codex": {
displayName: "GPT Codex", displayName: "GPT Codex",
resolve: "openai/gpt-5.3-codex", resolve: "openai/gpt-5.3-codex",
openRouterResolve: "openrouter/openai/gpt-5.3-codex", openRouterResolve: "openrouter/openai/gpt-5.3-codex",
preferred: true, fallback: "openai/gpt",
}, },
"gpt-codex-mini": { "gpt-codex-mini": {
displayName: "GPT Codex Mini", displayName: "GPT Codex Mini",
resolve: "openai/gpt-5.1-codex-mini", resolve: "openai/gpt-5.1-codex-mini",
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini", openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
fallback: "openai/gpt-mini",
}, },
o3: { o3: {
displayName: "O3", displayName: "O3",
@@ -138,16 +160,30 @@ export const providers = {
displayName: "DeepSeek", displayName: "DeepSeek",
envVars: ["DEEPSEEK_API_KEY"], envVars: ["DEEPSEEK_API_KEY"],
models: { models: {
"deepseek-pro": {
displayName: "DeepSeek Pro",
resolve: "deepseek/deepseek-v4-pro",
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro",
preferred: true,
},
"deepseek-flash": {
displayName: "DeepSeek Flash",
resolve: "deepseek/deepseek-v4-flash",
openRouterResolve: "openrouter/deepseek/deepseek-v4-flash",
},
// legacy aliases — deepseek retires these on 2026-07-24; transparently
// upgrade existing users to the v4 family via the fallback chain.
"deepseek-reasoner": { "deepseek-reasoner": {
displayName: "DeepSeek Reasoner", displayName: "DeepSeek Reasoner",
resolve: "deepseek/deepseek-reasoner", resolve: "deepseek/deepseek-reasoner",
openRouterResolve: "openrouter/deepseek/deepseek-v3.2", openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
preferred: true, fallback: "deepseek/deepseek-pro",
}, },
"deepseek-chat": { "deepseek-chat": {
displayName: "DeepSeek Chat", displayName: "DeepSeek Chat",
resolve: "deepseek/deepseek-chat", resolve: "deepseek/deepseek-chat",
openRouterResolve: "openrouter/deepseek/deepseek-v3.2", openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
fallback: "deepseek/deepseek-flash",
}, },
}, },
}), }),
@@ -157,8 +193,8 @@ export const providers = {
models: { models: {
"kimi-k2": { "kimi-k2": {
displayName: "Kimi K2", displayName: "Kimi K2",
resolve: "moonshotai/kimi-k2.5", resolve: "moonshotai/kimi-k2.6",
openRouterResolve: "openrouter/moonshotai/kimi-k2.5", openRouterResolve: "openrouter/moonshotai/kimi-k2.6",
preferred: true, preferred: true,
}, },
}, },
@@ -177,7 +213,7 @@ export const providers = {
"claude-opus": { "claude-opus": {
displayName: "Claude Opus", displayName: "Claude Opus",
resolve: "opencode/claude-opus-4-7", resolve: "opencode/claude-opus-4-7",
openRouterResolve: "openrouter/anthropic/claude-opus-4.6", openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
}, },
"claude-sonnet": { "claude-sonnet": {
displayName: "Claude Sonnet", displayName: "Claude Sonnet",
@@ -189,15 +225,33 @@ export const providers = {
resolve: "opencode/claude-haiku-4-5", resolve: "opencode/claude-haiku-4-5",
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5", openRouterResolve: "openrouter/anthropic/claude-haiku-4.5",
}, },
gpt: {
displayName: "GPT",
resolve: "opencode/gpt-5.5",
openRouterResolve: "openrouter/openai/gpt-5.5",
},
"gpt-pro": {
displayName: "GPT Pro",
resolve: "opencode/gpt-5.5-pro",
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
},
"gpt-mini": {
displayName: "GPT Mini",
resolve: "opencode/gpt-5.4-mini",
openRouterResolve: "openrouter/openai/gpt-5.4-mini",
},
// legacy aliases — see openai provider above for context.
"gpt-codex": { "gpt-codex": {
displayName: "GPT Codex", displayName: "GPT Codex",
resolve: "opencode/gpt-5.3-codex", resolve: "opencode/gpt-5.3-codex",
openRouterResolve: "openrouter/openai/gpt-5.3-codex", openRouterResolve: "openrouter/openai/gpt-5.3-codex",
fallback: "opencode/gpt",
}, },
"gpt-codex-mini": { "gpt-codex-mini": {
displayName: "GPT Codex Mini", displayName: "GPT Codex Mini",
resolve: "opencode/gpt-5.1-codex-mini", resolve: "opencode/gpt-5.1-codex-mini",
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini", openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
fallback: "opencode/gpt-mini",
}, },
"gemini-pro": { "gemini-pro": {
displayName: "Gemini Pro", displayName: "Gemini Pro",
@@ -211,8 +265,8 @@ export const providers = {
}, },
"kimi-k2": { "kimi-k2": {
displayName: "Kimi K2", displayName: "Kimi K2",
resolve: "opencode/kimi-k2.5", resolve: "opencode/kimi-k2.6",
openRouterResolve: "openrouter/moonshotai/kimi-k2.5", openRouterResolve: "openrouter/moonshotai/kimi-k2.6",
}, },
"gpt-5-nano": { "gpt-5-nano": {
displayName: "GPT Nano", displayName: "GPT Nano",
@@ -247,8 +301,8 @@ export const providers = {
models: { models: {
"claude-opus": { "claude-opus": {
displayName: "Claude Opus", displayName: "Claude Opus",
resolve: "openrouter/anthropic/claude-opus-4.6", resolve: "openrouter/anthropic/claude-opus-4.7",
openRouterResolve: "openrouter/anthropic/claude-opus-4.6", openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
preferred: true, preferred: true,
}, },
"claude-sonnet": { "claude-sonnet": {
@@ -261,15 +315,33 @@ export const providers = {
resolve: "openrouter/anthropic/claude-haiku-4.5", resolve: "openrouter/anthropic/claude-haiku-4.5",
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5", openRouterResolve: "openrouter/anthropic/claude-haiku-4.5",
}, },
gpt: {
displayName: "GPT",
resolve: "openrouter/openai/gpt-5.5",
openRouterResolve: "openrouter/openai/gpt-5.5",
},
"gpt-pro": {
displayName: "GPT Pro",
resolve: "openrouter/openai/gpt-5.5-pro",
openRouterResolve: "openrouter/openai/gpt-5.5-pro",
},
"gpt-mini": {
displayName: "GPT Mini",
resolve: "openrouter/openai/gpt-5.4-mini",
openRouterResolve: "openrouter/openai/gpt-5.4-mini",
},
// legacy aliases — see openai provider for context.
"gpt-codex": { "gpt-codex": {
displayName: "GPT Codex", displayName: "GPT Codex",
resolve: "openrouter/openai/gpt-5.3-codex", resolve: "openrouter/openai/gpt-5.3-codex",
openRouterResolve: "openrouter/openai/gpt-5.3-codex", openRouterResolve: "openrouter/openai/gpt-5.3-codex",
fallback: "openrouter/gpt",
}, },
"gpt-codex-mini": { "gpt-codex-mini": {
displayName: "GPT Codex Mini", displayName: "GPT Codex Mini",
resolve: "openrouter/openai/gpt-5.1-codex-mini", resolve: "openrouter/openai/gpt-5.1-codex-mini",
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini", openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
fallback: "openrouter/gpt-mini",
}, },
"o4-mini": { "o4-mini": {
displayName: "O4 Mini", displayName: "O4 Mini",
@@ -291,15 +363,28 @@ export const providers = {
resolve: "openrouter/x-ai/grok-4", resolve: "openrouter/x-ai/grok-4",
openRouterResolve: "openrouter/x-ai/grok-4", openRouterResolve: "openrouter/x-ai/grok-4",
}, },
"deepseek-pro": {
displayName: "DeepSeek Pro",
resolve: "openrouter/deepseek/deepseek-v4-pro",
openRouterResolve: "openrouter/deepseek/deepseek-v4-pro",
},
"deepseek-flash": {
displayName: "DeepSeek Flash",
resolve: "openrouter/deepseek/deepseek-v4-flash",
openRouterResolve: "openrouter/deepseek/deepseek-v4-flash",
},
// legacy alias — deepseek retires this on 2026-07-24; transparently
// upgrade existing users to the v4 family via the fallback chain.
"deepseek-chat": { "deepseek-chat": {
displayName: "DeepSeek Chat", displayName: "DeepSeek Chat",
resolve: "openrouter/deepseek/deepseek-v3.2", resolve: "openrouter/deepseek/deepseek-v3.2",
openRouterResolve: "openrouter/deepseek/deepseek-v3.2", openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
fallback: "openrouter/deepseek-flash",
}, },
"kimi-k2": { "kimi-k2": {
displayName: "Kimi K2", displayName: "Kimi K2",
resolve: "openrouter/moonshotai/kimi-k2.5", resolve: "openrouter/moonshotai/kimi-k2.6",
openRouterResolve: "openrouter/moonshotai/kimi-k2.5", openRouterResolve: "openrouter/moonshotai/kimi-k2.6",
}, },
}, },
}), }),
@@ -367,11 +452,15 @@ export function resolveModelSlug(slug: string): string | undefined {
const MAX_FALLBACK_DEPTH = 10; const MAX_FALLBACK_DEPTH = 10;
/** /**
* resolve a model slug to the CLI-ready model string, following the fallback * walk the fallback chain to the terminal (non-deprecated) alias.
* chain when a model is deprecated. returns the first non-deprecated resolve * returns undefined if the chain is broken, exhausted, or cyclic.
* target, or undefined if the chain is exhausted or broken. *
* use this in UI display sites (dropdown trigger labels, PR-comment footers,
* etc.) so a deprecated stored slug renders as the model the user actually
* runs against — not the historical name. selectable lists should still hide
* deprecated aliases by filtering on `!a.fallback`.
*/ */
export function resolveCliModel(slug: string): string | undefined { export function resolveDisplayAlias(slug: string): ModelAlias | undefined {
let current = slug; let current = slug;
const visited = new Set<string>(); const visited = new Set<string>();
for (let i = 0; i < MAX_FALLBACK_DEPTH; i++) { for (let i = 0; i < MAX_FALLBACK_DEPTH; i++) {
@@ -379,8 +468,27 @@ export function resolveCliModel(slug: string): string | undefined {
visited.add(current); visited.add(current);
const alias = modelAliases.find((a) => a.slug === current); const alias = modelAliases.find((a) => a.slug === current);
if (!alias) return undefined; if (!alias) return undefined;
if (!alias.fallback) return alias.resolve; if (!alias.fallback) return alias;
current = alias.fallback; current = alias.fallback;
} }
return undefined; return undefined;
} }
/**
* 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 resolveDisplayAlias(slug)?.resolve;
}
/**
* resolve a model slug to the OpenRouter-ready model string, following the
* fallback chain when a model is deprecated. returns undefined if the chain
* is exhausted/broken or the terminal alias has no openrouter equivalent
* (e.g. free opencode models).
*/
export function resolveOpenRouterModel(slug: string): string | undefined {
return resolveDisplayAlias(slug)?.openRouterResolve;
}
@@ -7,32 +7,32 @@ exports[`latest model per provider snapshot > matches snapshot 1`] = `
"releaseDate": "2026-04-16", "releaseDate": "2026-04-16",
}, },
"deepseek": { "deepseek": {
"modelId": "deepseek-reasoner", "modelId": "deepseek-v4-pro",
"releaseDate": "2025-12-01", "releaseDate": "2026-04-24",
}, },
"google": { "google": {
"modelId": "gemma-4-31b-it", "modelId": "gemma-4-31b-it",
"releaseDate": "2026-04-02", "releaseDate": "2026-04-02",
}, },
"moonshotai": { "moonshotai": {
"modelId": "kimi-k2.5", "modelId": "kimi-k2.6",
"releaseDate": "2026-01", "releaseDate": "2026-04-21",
}, },
"openai": { "openai": {
"modelId": "gpt-5.4-nano", "modelId": "gpt-5.5-pro",
"releaseDate": "2026-03-17", "releaseDate": "2026-04-23",
}, },
"opencode": { "opencode": {
"modelId": "claude-opus-4-7", "modelId": "gpt-5.5-pro",
"releaseDate": "2026-04-16", "releaseDate": "2026-04-24",
}, },
"openrouter": { "openrouter": {
"modelId": "anthropic/claude-opus-4.7", "modelId": "poolside/laguna-xs.2:free",
"releaseDate": "2026-04-16", "releaseDate": "2026-04-28",
}, },
"xai": { "xai": {
"modelId": "grok-4.20-multi-agent-0309", "modelId": "grok-4.3",
"releaseDate": "2026-03-09", "releaseDate": "2026-05-01",
}, },
} }
`; `;
+4 -2
View File
@@ -1,4 +1,4 @@
import { modelAliases } from "../models.ts"; import { resolveDisplayAlias } from "../models.ts";
export const PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->"; export const PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
@@ -26,7 +26,9 @@ export interface BuildPullfrogFooterParams {
} }
function formatModelLabel(slug: string): string { function formatModelLabel(slug: string): string {
const alias = modelAliases.find((a) => a.slug === slug); // walk the fallback chain so a deprecated stored slug shows the model the
// run actually executed against (e.g. "GPT", not "GPT Codex").
const alias = resolveDisplayAlias(slug);
if (!alias) return `\`${slug}\``; if (!alias) return `\`${slug}\``;
return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``; return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
} }