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 = `Pullfrog`; +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 = `Pullfrog`; +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 = `Pullfrog`; @@ -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