diff --git a/entry b/entry
index d05f8ff..c2151d6 100755
--- a/entry
+++ b/entry
@@ -145015,6 +145015,11 @@ function GetCheckSuiteLogsTool(ctx) {
// utils/buildPullfrogFooter.ts
var PULLFROG_DIVIDER = "";
var FROG_LOGO = `
`;
+function formatModelLabel(slug) {
+ const alias = modelAliases.find((a) => a.slug === slug);
+ if (!alias) return `\`${slug}\``;
+ return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
+}
function buildPullfrogFooter(params) {
const parts = [];
if (params.customParts) {
@@ -145030,11 +145035,10 @@ function buildPullfrogFooter(params) {
if (params.triggeredBy) {
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
}
- const allParts = [
- ...parts,
- "[pullfrog.com](https://pullfrog.com)",
- "[\u{1D54F}](https://x.com/pullfrogai)"
- ];
+ if (params.model) {
+ parts.push(`Using ${formatModelLabel(params.model)}`);
+ }
+ const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
return `
${PULLFROG_DIVIDER}
${FROG_LOGO} \uFF5C ${allParts.join(" \uFF5C ")}`;
@@ -145098,22 +145102,25 @@ async function buildCommentFooter(params) {
} catch {
}
}
- const footerParams = {
+ return buildPullfrogFooter({
triggeredBy: true,
- workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId, jobId } : void 0
- };
- if (params.customParts && params.customParts.length > 0) {
- return buildPullfrogFooter({ ...footerParams, customParts: params.customParts });
- }
- return buildPullfrogFooter(footerParams);
+ workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId, jobId } : void 0,
+ customParts: params.customParts,
+ model: params.model
+ });
}
function buildImplementPlanLink(owner, repo, issueNumber, commentId) {
const apiUrl = getApiUrl();
return `[Implement plan \u2794](${apiUrl}/trigger/${owner}/${repo}/${issueNumber}?action=implement&comment_id=${commentId})`;
}
async function addFooter(ctx, body) {
+ if (/
[ \t]*\n(?!\s*\n)/i.test(body)) {
+ throw new Error(
+ "body contains
followed by a non-blank line, which breaks GitHub markdown rendering. always add a blank line after
tags."
+ );
+ }
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
- const footer = await buildCommentFooter({ octokit: ctx.octokit });
+ const footer = await buildCommentFooter({ octokit: ctx.octokit, model: ctx.toolState?.model });
return `${bodyWithoutFooter}${footer}`;
}
var Comment = type({
@@ -145221,7 +145228,8 @@ async function reportProgress(ctx, params) {
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({
octokit: ctx.octokit,
- customParts
+ customParts,
+ model: ctx.toolState.model
});
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
const result2 = await ctx.octokit.rest.issues.updateComment({
@@ -145247,7 +145255,8 @@ async function reportProgress(ctx, params) {
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({
octokit: ctx.octokit,
- customParts
+ customParts,
+ model: ctx.toolState.model
});
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
const result2 = await ctx.octokit.rest.issues.updateComment({
@@ -145289,7 +145298,8 @@ async function reportProgress(ctx, params) {
const bodyWithoutFooter = stripExistingFooter(body);
const footer = await buildCommentFooter({
octokit: ctx.octokit,
- customParts
+ customParts,
+ model: ctx.toolState.model
});
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
const updateResult = await ctx.octokit.rest.issues.updateComment({
@@ -146722,7 +146732,8 @@ var PullRequest = type({
function buildPrBodyWithFooter(ctx, body) {
const footer = buildPullfrogFooter({
triggeredBy: true,
- workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0
+ workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
+ model: ctx.toolState.model
});
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
return `${bodyWithoutFooter}${footer}`;
@@ -147031,7 +147042,8 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
}
const footer = buildPullfrogFooter({
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
- customParts
+ customParts,
+ model: ctx.toolState.model
});
return ctx.octokit.rest.pulls.submitReview({
owner: params.owner,
@@ -149367,7 +149379,8 @@ ${ctx.error}` : ctx.error;
const footer = buildPullfrogFooter({
triggeredBy: true,
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
- customParts
+ customParts,
+ model: ctx.toolState.model
});
await octokit.rest.issues.updateComment({
owner: repoContext.owner,
@@ -150369,6 +150382,7 @@ async function main() {
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
timer.checkpoint("runContextData");
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
+ toolState.model = payload.model;
const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push }), true);
if (payload.shell !== "enabled") {
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
diff --git a/main.ts b/main.ts
index d530628..914535a 100644
--- a/main.ts
+++ b/main.ts
@@ -103,6 +103,7 @@ export async function main(): Promise {
// resolve payload to determine shell permission
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
+ toolState.model = payload.model;
// resolve tokens:
// - gitToken: contents permission based on push setting (assumed exfiltratable)
diff --git a/mcp/comment.ts b/mcp/comment.ts
index e296528..4cf11bb 100644
--- a/mcp/comment.ts
+++ b/mcp/comment.ts
@@ -55,6 +55,7 @@ export const LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
interface BuildCommentFooterParams {
octokit?: OctokitWithPlugins | undefined;
customParts?: string[] | undefined;
+ model?: string | undefined;
}
async function buildCommentFooter(params: BuildCommentFooterParams): Promise {
@@ -77,17 +78,14 @@ async function buildCommentFooter(params: BuildCommentFooterParams): Promise 0) {
- return buildPullfrogFooter({ ...footerParams, customParts: params.customParts });
- }
- return buildPullfrogFooter(footerParams);
+ customParts: params.customParts,
+ model: params.model,
+ });
}
function buildImplementPlanLink(
@@ -102,11 +100,17 @@ function buildImplementPlanLink(
export interface AddFooterCtx {
octokit?: OctokitWithPlugins | undefined;
+ toolState?: { model?: string | undefined } | undefined;
}
export async function addFooter(ctx: AddFooterCtx, body: string): Promise {
+ if (/
[ \t]*\n(?!\s*\n)/i.test(body)) {
+ throw new Error(
+ "body contains
followed by a non-blank line, which breaks GitHub markdown rendering. always add a blank line after
tags."
+ );
+ }
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
- const footer = await buildCommentFooter({ octokit: ctx.octokit });
+ const footer = await buildCommentFooter({ octokit: ctx.octokit, model: ctx.toolState?.model });
return `${bodyWithoutFooter}${footer}`;
}
@@ -262,6 +266,7 @@ export async function reportProgress(
const footer = await buildCommentFooter({
octokit: ctx.octokit,
customParts,
+ model: ctx.toolState.model,
});
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
@@ -299,6 +304,7 @@ export async function reportProgress(
const footer = await buildCommentFooter({
octokit: ctx.octokit,
customParts,
+ model: ctx.toolState.model,
});
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
@@ -359,6 +365,7 @@ export async function reportProgress(
const footer = await buildCommentFooter({
octokit: ctx.octokit,
customParts,
+ model: ctx.toolState.model,
});
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
diff --git a/mcp/pr.ts b/mcp/pr.ts
index 9e9c999..f695832 100644
--- a/mcp/pr.ts
+++ b/mcp/pr.ts
@@ -21,6 +21,7 @@ function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
workflowRun: ctx.runId
? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId }
: undefined,
+ model: ctx.toolState.model,
});
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
diff --git a/mcp/review.ts b/mcp/review.ts
index 2471499..b4d10d9 100644
--- a/mcp/review.ts
+++ b/mcp/review.ts
@@ -264,6 +264,7 @@ async function createAndSubmitWithFooter(
? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId }
: undefined,
customParts,
+ model: ctx.toolState.model,
});
return ctx.octokit.rest.pulls.submitReview({
diff --git a/mcp/server.ts b/mcp/server.ts
index 37d5c4a..56cfd14 100644
--- a/mcp/server.ts
+++ b/mcp/server.ts
@@ -89,6 +89,7 @@ export interface ToolState {
existingSummaryCommentId?: number;
output?: string;
usageEntries: AgentUsage[];
+ model?: string | undefined;
}
interface InitToolStateParams {
diff --git a/post b/post
index a828513..dcb0dbc 100755
--- a/post
+++ b/post
@@ -37509,9 +37509,177 @@ function getApiUrl() {
return raw;
}
+// models.ts
+function provider(config) {
+ return config;
+}
+var providers = {
+ anthropic: provider({
+ displayName: "Anthropic",
+ envVars: ["ANTHROPIC_API_KEY"],
+ models: {
+ "claude-opus": {
+ displayName: "Claude Opus",
+ resolve: "anthropic/claude-opus-4-6",
+ recommended: true
+ },
+ "claude-sonnet": { displayName: "Claude Sonnet", resolve: "anthropic/claude-sonnet-4-6" },
+ "claude-haiku": { displayName: "Claude Haiku", resolve: "anthropic/claude-haiku-4-5" }
+ }
+ }),
+ openai: provider({
+ displayName: "OpenAI",
+ envVars: ["OPENAI_API_KEY"],
+ models: {
+ "gpt-codex": { displayName: "GPT Codex", resolve: "openai/gpt-5.3-codex", recommended: true },
+ "gpt-codex-mini": { displayName: "GPT Codex Mini", resolve: "openai/codex-mini-latest" },
+ o3: { displayName: "O3", resolve: "openai/o3" }
+ }
+ }),
+ google: provider({
+ displayName: "Google",
+ envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"],
+ models: {
+ "gemini-pro": {
+ displayName: "Gemini Pro",
+ resolve: "google/gemini-3.1-pro-preview",
+ recommended: true
+ },
+ "gemini-flash": { displayName: "Gemini Flash", resolve: "google/gemini-3-flash-preview" }
+ }
+ }),
+ xai: provider({
+ displayName: "xAI",
+ envVars: ["XAI_API_KEY"],
+ models: {
+ grok: { displayName: "Grok", resolve: "xai/grok-4", recommended: true },
+ "grok-fast": { displayName: "Grok Fast", resolve: "xai/grok-4-fast" },
+ "grok-code-fast": { displayName: "Grok Code Fast", resolve: "xai/grok-code-fast-1" }
+ }
+ }),
+ deepseek: provider({
+ displayName: "DeepSeek",
+ envVars: ["DEEPSEEK_API_KEY"],
+ models: {
+ "deepseek-reasoner": {
+ displayName: "DeepSeek Reasoner",
+ resolve: "deepseek/deepseek-reasoner",
+ recommended: true
+ },
+ "deepseek-chat": { displayName: "DeepSeek Chat", resolve: "deepseek/deepseek-chat" }
+ }
+ }),
+ moonshotai: provider({
+ displayName: "Moonshot AI",
+ envVars: ["MOONSHOT_API_KEY"],
+ models: {
+ "kimi-k2": { displayName: "Kimi K2", resolve: "moonshotai/kimi-k2.5", recommended: true }
+ }
+ }),
+ opencode: provider({
+ displayName: "OpenCode",
+ envVars: ["OPENCODE_API_KEY"],
+ models: {
+ "big-pickle": {
+ displayName: "Big Pickle",
+ resolve: "opencode/big-pickle",
+ recommended: true,
+ envVars: [],
+ isFree: true
+ },
+ "claude-opus": { displayName: "Claude Opus", resolve: "opencode/claude-opus-4-6" },
+ "claude-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" },
+ "claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" },
+ "gpt-codex": { displayName: "GPT Codex", resolve: "opencode/gpt-5.3-codex" },
+ "gpt-codex-mini": { displayName: "GPT Codex Mini", resolve: "opencode/gpt-5.1-codex-mini" },
+ "gemini-pro": { displayName: "Gemini Pro", resolve: "opencode/gemini-3.1-pro" },
+ "gemini-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" },
+ "kimi-k2": { displayName: "Kimi K2", resolve: "opencode/kimi-k2.5" },
+ "gpt-5-nano": {
+ displayName: "GPT Nano",
+ resolve: "opencode/gpt-5-nano",
+ envVars: [],
+ isFree: true
+ },
+ "mimo-v2-flash-free": {
+ displayName: "MiMo V2 Flash",
+ resolve: "opencode/mimo-v2-flash-free",
+ envVars: [],
+ isFree: true
+ },
+ "minimax-m2.5-free": {
+ displayName: "MiniMax M2.5",
+ resolve: "opencode/minimax-m2.5-free",
+ envVars: [],
+ isFree: true
+ },
+ "nemotron-3-super-free": {
+ displayName: "Nemotron 3 Super",
+ resolve: "opencode/nemotron-3-super-free",
+ envVars: [],
+ isFree: true
+ }
+ }
+ }),
+ openrouter: provider({
+ displayName: "OpenRouter",
+ envVars: ["OPENROUTER_API_KEY"],
+ models: {
+ "claude-opus": {
+ displayName: "Claude Opus",
+ resolve: "openrouter/anthropic/claude-opus-4.6",
+ recommended: true
+ },
+ "claude-sonnet": {
+ displayName: "Claude Sonnet",
+ resolve: "openrouter/anthropic/claude-sonnet-4.6"
+ },
+ "claude-haiku": {
+ displayName: "Claude Haiku",
+ resolve: "openrouter/anthropic/claude-haiku-4.5"
+ },
+ "gpt-codex": { displayName: "GPT Codex", resolve: "openrouter/openai/gpt-5.3-codex" },
+ "gpt-codex-mini": {
+ displayName: "GPT Codex Mini",
+ resolve: "openrouter/openai/gpt-5.1-codex-mini"
+ },
+ "o4-mini": { displayName: "O4 Mini", resolve: "openrouter/openai/o4-mini" },
+ "gemini-pro": {
+ displayName: "Gemini Pro",
+ resolve: "openrouter/google/gemini-3.1-pro-preview"
+ },
+ "gemini-flash": {
+ displayName: "Gemini Flash",
+ resolve: "openrouter/google/gemini-3-flash-preview"
+ },
+ grok: { displayName: "Grok", resolve: "openrouter/x-ai/grok-4" },
+ "deepseek-chat": {
+ displayName: "DeepSeek Chat",
+ resolve: "openrouter/deepseek/deepseek-chat-v3.1"
+ },
+ "kimi-k2": { displayName: "Kimi K2", resolve: "openrouter/moonshotai/kimi-k2.5" }
+ }
+ })
+};
+var modelAliases = Object.entries(providers).flatMap(
+ ([providerKey, config]) => Object.entries(config.models).map(([modelId, def]) => ({
+ slug: `${providerKey}/${modelId}`,
+ provider: providerKey,
+ displayName: def.displayName,
+ resolve: def.resolve,
+ recommended: def.recommended ?? false,
+ isFree: def.isFree ?? false
+ }))
+);
+
// utils/buildPullfrogFooter.ts
var PULLFROG_DIVIDER = "";
var FROG_LOGO = `
`;
+function formatModelLabel(slug) {
+ const alias = modelAliases.find((a) => a.slug === slug);
+ if (!alias) return `\`${slug}\``;
+ return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
+}
function buildPullfrogFooter(params) {
const parts = [];
if (params.customParts) {
@@ -37527,11 +37695,10 @@ function buildPullfrogFooter(params) {
if (params.triggeredBy) {
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
}
- const allParts = [
- ...parts,
- "[pullfrog.com](https://pullfrog.com)",
- "[\u{1D54F}](https://x.com/pullfrogai)"
- ];
+ if (params.model) {
+ parts.push(`Using ${formatModelLabel(params.model)}`);
+ }
+ const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
return `
${PULLFROG_DIVIDER}
${FROG_LOGO} \uFF5C ${allParts.join(" \uFF5C ")}`;
diff --git a/test/__snapshots__/models.test.ts.snap b/test/__snapshots__/models.test.ts.snap
index 0316857..d4c0b79 100644
--- a/test/__snapshots__/models.test.ts.snap
+++ b/test/__snapshots__/models.test.ts.snap
@@ -19,8 +19,8 @@ exports[`latest model per provider snapshot > matches snapshot 1`] = `
"releaseDate": "2026-01",
},
"openai": {
- "modelId": "gpt-5.4-pro",
- "releaseDate": "2026-03-05",
+ "modelId": "gpt-5.4-nano",
+ "releaseDate": "2026-03-17",
},
"opencode": {
"modelId": "nemotron-3-super-free",
diff --git a/test/models.test.ts b/test/models.test.ts
index 3857ddf..5c6b5bb 100644
--- a/test/models.test.ts
+++ b/test/models.test.ts
@@ -76,6 +76,8 @@ describe("latest model per provider snapshot", async () => {
}
}
+ // when this fails, a provider shipped a new model. check whether we need
+ // to add or update an alias in models.ts before updating the snapshot.
it("matches snapshot", () => {
expect(latestByProvider).toMatchSnapshot();
});
diff --git a/utils/buildPullfrogFooter.ts b/utils/buildPullfrogFooter.ts
index 9715630..5eaf26f 100644
--- a/utils/buildPullfrogFooter.ts
+++ b/utils/buildPullfrogFooter.ts
@@ -1,3 +1,5 @@
+import { modelAliases } from "../models.ts";
+
export const PULLFROG_DIVIDER = "";
const FROG_LOGO = `
`;
@@ -18,13 +20,21 @@ export interface BuildPullfrogFooterParams {
/** alternative: just pass a pre-built URL directly (for shortlinks etc.) */
workflowRunUrl?: string | undefined;
/** arbitrary custom parts (e.g., action links) */
- customParts?: string[];
+ customParts?: string[] | undefined;
+ /** model slug from payload (e.g., "anthropic/claude-opus"). shown in footer as "Using `Model Name`" */
+ model?: string | undefined;
+}
+
+function formatModelLabel(slug: string): string {
+ const alias = modelAliases.find((a) => a.slug === slug);
+ if (!alias) return `\`${slug}\``;
+ return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
}
/**
* build a pullfrog footer with configurable parts
- * always includes: frog logo at start, pullfrog.com link and X link at end
- * order: action links (customParts) > workflow run > attribution > reference links
+ * always includes: frog logo at start and X link at end
+ * order: action links (customParts) > workflow run > model > attribution > reference links
*/
export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
const parts: string[] = [];
@@ -45,11 +55,11 @@ export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
}
- const allParts = [
- ...parts,
- "[pullfrog.com](https://pullfrog.com)",
- "[𝕏](https://x.com/pullfrogai)",
- ];
+ if (params.model) {
+ parts.push(`Using ${formatModelLabel(params.model)}`);
+ }
+
+ const allParts = [...parts, "[𝕏](https://x.com/pullfrogai)"];
return `
${PULLFROG_DIVIDER}
diff --git a/utils/errorReport.ts b/utils/errorReport.ts
index a16636d..bb3cee2 100644
--- a/utils/errorReport.ts
+++ b/utils/errorReport.ts
@@ -36,6 +36,7 @@ export async function reportErrorToComment(ctx: ReportErrorParams): Promise