Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6d34ee01b | |||
| 39525547b5 | |||
| 3ff11f97eb | |||
| b31800c213 | |||
| 3a1ffde545 | |||
| cccf1775d6 | |||
| 026cc7a276 | |||
| c6a3ee0e9a |
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
@@ -42,11 +42,10 @@ jobs:
|
||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
PULLFROG_MODEL: ${{ vars.PULLFROG_MODEL }}
|
||||
OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
@@ -80,7 +79,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
|
||||
+14
-13
@@ -75,9 +75,9 @@ function buildSecurityConfig(ctx: AgentRunContext, model: string | undefined): s
|
||||
// ── model resolution (see wiki/model-resolution.md) ─────────────────────────────
|
||||
//
|
||||
// priority:
|
||||
// 1. PULLFROG_MODEL or OPENCODE_MODEL env var (explicit override)
|
||||
// 1. PULLFROG_MODEL env var (explicit override)
|
||||
// 2. explicit slug from repo config / payload
|
||||
// 3. auto-select: `opencode models` → recommended aliases first, then secondary
|
||||
// 3. auto-select: `opencode models` → preferred aliases first, then secondary
|
||||
// 4. undefined → let OpenCode decide
|
||||
|
||||
function getOpenCodeModels(cliPath: string): string[] {
|
||||
@@ -106,11 +106,10 @@ function resolveOpenCodeModel(ctx: {
|
||||
cliPath: string;
|
||||
modelSlug?: string | undefined;
|
||||
}): string | undefined {
|
||||
// 1. explicit env var override (PULLFROG_MODEL takes precedence over OPENCODE_MODEL)
|
||||
const envModel = process.env.PULLFROG_MODEL?.trim() || process.env.OPENCODE_MODEL?.trim();
|
||||
// 1. explicit env var override
|
||||
const envModel = process.env.PULLFROG_MODEL?.trim();
|
||||
if (envModel) {
|
||||
const source = process.env.PULLFROG_MODEL?.trim() ? "PULLFROG_MODEL" : "OPENCODE_MODEL";
|
||||
log.info(`» model: ${envModel} (override via ${source})`);
|
||||
log.info(`» model: ${envModel} (override via PULLFROG_MODEL)`);
|
||||
return envModel;
|
||||
}
|
||||
|
||||
@@ -126,17 +125,17 @@ function resolveOpenCodeModel(ctx: {
|
||||
|
||||
// 3. auto-select: ask OpenCode what's available, pick our best curated match.
|
||||
// `opencode models` returns `provider/model-id` specifiers matching our resolve values exactly.
|
||||
// two-pass: recommended (top-tier per provider) first, then secondary models.
|
||||
// two-pass: preferred (top-tier per provider) first, then secondary models.
|
||||
const availableModels = getOpenCodeModels(ctx.cliPath);
|
||||
const availableSet = new Set(availableModels);
|
||||
if (availableSet.size > 0) {
|
||||
log.debug(`» opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
||||
const match =
|
||||
modelAliases.find((a) => a.recommended && availableSet.has(a.resolve)) ??
|
||||
modelAliases.find((a) => a.preferred && availableSet.has(a.resolve)) ??
|
||||
modelAliases.find((a) => availableSet.has(a.resolve));
|
||||
if (match) {
|
||||
log.info(
|
||||
`» model: ${match.resolve} (auto-selected${match.recommended ? " — recommended" : ""} curated match)`
|
||||
`» model: ${match.resolve} (auto-selected${match.preferred ? " — preferred" : ""} curated match)`
|
||||
);
|
||||
log.warning(`» model auto-selected. ${AUTO_SELECT_WARNING}`);
|
||||
return match.resolve;
|
||||
@@ -622,10 +621,12 @@ export const opentoad = agent({
|
||||
run: async (ctx) => {
|
||||
const cliPath = await installOpencodeCli();
|
||||
|
||||
const model = resolveOpenCodeModel({
|
||||
cliPath,
|
||||
modelSlug: ctx.payload.model,
|
||||
});
|
||||
const model =
|
||||
ctx.payload.proxyModel ??
|
||||
resolveOpenCodeModel({
|
||||
cliPath,
|
||||
modelSlug: ctx.payload.model,
|
||||
});
|
||||
|
||||
const tempHome = ctx.tmpdir;
|
||||
mkdirSync(join(tempHome, ".config", "opencode"), { recursive: true });
|
||||
|
||||
@@ -45,7 +45,6 @@ export const agent = (input: Agent): Agent => {
|
||||
return {
|
||||
...input,
|
||||
run: async (ctx: AgentRunContext): Promise<AgentResult> => {
|
||||
log.info(`» agent: ${input.name}`);
|
||||
if (ctx.payload.model) log.info(`» model: ${ctx.payload.model}`);
|
||||
if (ctx.payload.timeout) log.info(`» timeout: ${ctx.payload.timeout}`);
|
||||
log.info(`» push: ${ctx.payload.push}`);
|
||||
|
||||
@@ -19718,10 +19718,10 @@ var require_core = __commonJS({
|
||||
(0, command_1.issueCommand)("set-env", { name }, convertedVal);
|
||||
}
|
||||
exports.exportVariable = exportVariable;
|
||||
function setSecret2(secret) {
|
||||
function setSecret3(secret) {
|
||||
(0, command_1.issueCommand)("add-mask", {}, secret);
|
||||
}
|
||||
exports.setSecret = setSecret2;
|
||||
exports.setSecret = setSecret3;
|
||||
function addPath(inputPath) {
|
||||
const filePath = process.env["GITHUB_PATH"] || "";
|
||||
if (filePath) {
|
||||
@@ -19838,12 +19838,12 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
||||
return process.env[`STATE_${name}`] || "";
|
||||
}
|
||||
exports.getState = getState;
|
||||
function getIDToken2(aud) {
|
||||
function getIDToken3(aud) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
return yield oidc_utils_1.OidcClient.getIDToken(aud);
|
||||
});
|
||||
}
|
||||
exports.getIDToken = getIDToken2;
|
||||
exports.getIDToken = getIDToken3;
|
||||
var summary_1 = require_summary();
|
||||
Object.defineProperty(exports, "summary", { enumerable: true, get: function() {
|
||||
return summary_1.summary;
|
||||
@@ -131925,19 +131925,40 @@ var providers = {
|
||||
"claude-opus": {
|
||||
displayName: "Claude Opus",
|
||||
resolve: "anthropic/claude-opus-4-6",
|
||||
recommended: true
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
preferred: true
|
||||
},
|
||||
"claude-sonnet": { displayName: "Claude Sonnet", resolve: "anthropic/claude-sonnet-4-6" },
|
||||
"claude-haiku": { displayName: "Claude Haiku", resolve: "anthropic/claude-haiku-4-5" }
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "anthropic/claude-sonnet-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "anthropic/claude-haiku-4-5",
|
||||
openRouterResolve: "openrouter/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" }
|
||||
"gpt-codex": {
|
||||
displayName: "GPT Codex",
|
||||
resolve: "openai/gpt-5.3-codex",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex",
|
||||
preferred: true
|
||||
},
|
||||
"gpt-codex-mini": {
|
||||
displayName: "GPT Codex Mini",
|
||||
resolve: "openai/codex-mini-latest",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
|
||||
},
|
||||
o3: {
|
||||
displayName: "O3",
|
||||
resolve: "openai/o3"
|
||||
}
|
||||
}
|
||||
}),
|
||||
google: provider({
|
||||
@@ -131947,18 +131968,36 @@ var providers = {
|
||||
"gemini-pro": {
|
||||
displayName: "Gemini Pro",
|
||||
resolve: "google/gemini-3.1-pro-preview",
|
||||
recommended: true
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
||||
preferred: true
|
||||
},
|
||||
"gemini-flash": { displayName: "Gemini Flash", resolve: "google/gemini-3-flash-preview" }
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "google/gemini-3-flash-preview",
|
||||
openRouterResolve: "openrouter/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" }
|
||||
grok: {
|
||||
displayName: "Grok",
|
||||
resolve: "xai/grok-4",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4",
|
||||
preferred: true
|
||||
},
|
||||
"grok-fast": {
|
||||
displayName: "Grok Fast",
|
||||
resolve: "xai/grok-4-fast",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4-fast"
|
||||
},
|
||||
"grok-code-fast": {
|
||||
displayName: "Grok Code Fast",
|
||||
resolve: "xai/grok-code-fast-1",
|
||||
openRouterResolve: "openrouter/x-ai/grok-code-fast-1"
|
||||
}
|
||||
}
|
||||
}),
|
||||
deepseek: provider({
|
||||
@@ -131968,16 +132007,26 @@ var providers = {
|
||||
"deepseek-reasoner": {
|
||||
displayName: "DeepSeek Reasoner",
|
||||
resolve: "deepseek/deepseek-reasoner",
|
||||
recommended: true
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
|
||||
preferred: true
|
||||
},
|
||||
"deepseek-chat": { displayName: "DeepSeek Chat", resolve: "deepseek/deepseek-chat" }
|
||||
"deepseek-chat": {
|
||||
displayName: "DeepSeek Chat",
|
||||
resolve: "deepseek/deepseek-chat",
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2"
|
||||
}
|
||||
}
|
||||
}),
|
||||
moonshotai: provider({
|
||||
displayName: "Moonshot AI",
|
||||
envVars: ["MOONSHOT_API_KEY"],
|
||||
models: {
|
||||
"kimi-k2": { displayName: "Kimi K2", resolve: "moonshotai/kimi-k2.5", recommended: true }
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "moonshotai/kimi-k2.5",
|
||||
openRouterResolve: "openrouter/moonshotai/kimi-k2.5",
|
||||
preferred: true
|
||||
}
|
||||
}
|
||||
}),
|
||||
opencode: provider({
|
||||
@@ -131987,27 +132036,59 @@ var providers = {
|
||||
"big-pickle": {
|
||||
displayName: "Big Pickle",
|
||||
resolve: "opencode/big-pickle",
|
||||
recommended: true,
|
||||
preferred: 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" },
|
||||
"claude-opus": {
|
||||
displayName: "Claude Opus",
|
||||
resolve: "opencode/claude-opus-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6"
|
||||
},
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "opencode/claude-sonnet-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "opencode/claude-haiku-4-5",
|
||||
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
|
||||
},
|
||||
"gpt-codex": {
|
||||
displayName: "GPT Codex",
|
||||
resolve: "opencode/gpt-5.3-codex",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex"
|
||||
},
|
||||
"gpt-codex-mini": {
|
||||
displayName: "GPT Codex Mini",
|
||||
resolve: "opencode/gpt-5.1-codex-mini",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
|
||||
},
|
||||
"gemini-pro": {
|
||||
displayName: "Gemini Pro",
|
||||
resolve: "opencode/gemini-3.1-pro",
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
||||
},
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "opencode/gemini-3-flash",
|
||||
openRouterResolve: "openrouter/google/gemini-3-flash-preview"
|
||||
},
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "opencode/kimi-k2.5",
|
||||
openRouterResolve: "openrouter/moonshotai/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",
|
||||
"mimo-v2-pro-free": {
|
||||
displayName: "MiMo V2 Pro",
|
||||
resolve: "opencode/mimo-v2-pro-free",
|
||||
envVars: [],
|
||||
isFree: true
|
||||
},
|
||||
@@ -132032,36 +132113,59 @@ var providers = {
|
||||
"claude-opus": {
|
||||
displayName: "Claude Opus",
|
||||
resolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
recommended: true
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
preferred: true
|
||||
},
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "openrouter/anthropic/claude-sonnet-4.6"
|
||||
resolve: "openrouter/anthropic/claude-sonnet-4.6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "openrouter/anthropic/claude-haiku-4.5"
|
||||
resolve: "openrouter/anthropic/claude-haiku-4.5",
|
||||
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
|
||||
},
|
||||
"gpt-codex": {
|
||||
displayName: "GPT Codex",
|
||||
resolve: "openrouter/openai/gpt-5.3-codex",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex"
|
||||
},
|
||||
"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"
|
||||
resolve: "openrouter/openai/gpt-5.1-codex-mini",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
|
||||
},
|
||||
"o4-mini": {
|
||||
displayName: "O4 Mini",
|
||||
resolve: "openrouter/openai/o4-mini",
|
||||
openRouterResolve: "openrouter/openai/o4-mini"
|
||||
},
|
||||
"o4-mini": { displayName: "O4 Mini", resolve: "openrouter/openai/o4-mini" },
|
||||
"gemini-pro": {
|
||||
displayName: "Gemini Pro",
|
||||
resolve: "openrouter/google/gemini-3.1-pro-preview"
|
||||
resolve: "openrouter/google/gemini-3.1-pro-preview",
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
||||
},
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "openrouter/google/gemini-3-flash-preview"
|
||||
resolve: "openrouter/google/gemini-3-flash-preview",
|
||||
openRouterResolve: "openrouter/google/gemini-3-flash-preview"
|
||||
},
|
||||
grok: {
|
||||
displayName: "Grok",
|
||||
resolve: "openrouter/x-ai/grok-4",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4"
|
||||
},
|
||||
grok: { displayName: "Grok", resolve: "openrouter/x-ai/grok-4" },
|
||||
"deepseek-chat": {
|
||||
displayName: "DeepSeek Chat",
|
||||
resolve: "openrouter/deepseek/deepseek-chat-v3.1"
|
||||
resolve: "openrouter/deepseek/deepseek-v3.2",
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2"
|
||||
},
|
||||
"kimi-k2": { displayName: "Kimi K2", resolve: "openrouter/moonshotai/kimi-k2.5" }
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "openrouter/moonshotai/kimi-k2.5",
|
||||
openRouterResolve: "openrouter/moonshotai/kimi-k2.5"
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -132090,7 +132194,8 @@ var modelAliases = Object.entries(providers).flatMap(
|
||||
provider: providerKey,
|
||||
displayName: def.displayName,
|
||||
resolve: def.resolve,
|
||||
recommended: def.recommended ?? false,
|
||||
openRouterResolve: def.openRouterResolve,
|
||||
preferred: def.preferred ?? false,
|
||||
isFree: def.isFree ?? false
|
||||
}))
|
||||
);
|
||||
@@ -145015,6 +145120,11 @@ function GetCheckSuiteLogsTool(ctx) {
|
||||
// utils/buildPullfrogFooter.ts
|
||||
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
||||
var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
|
||||
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 +145140,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}
|
||||
<sup>${FROG_LOGO} \uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
|
||||
@@ -145098,22 +145207,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 (/<br\s*\/?>[ \t]*\n(?!\s*\n)/i.test(body)) {
|
||||
throw new Error(
|
||||
"body contains <br/> followed by a non-blank line, which breaks GitHub markdown rendering. always add a blank line after <br/> 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 +145333,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 +145360,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 +145403,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 +146837,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}`;
|
||||
@@ -146898,6 +147014,16 @@ function CreatePullRequestReviewTool(ctx) {
|
||||
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
|
||||
if (body) body = fixDoubleEscapedString(body);
|
||||
ctx.toolState.issueNumber = pull_number;
|
||||
if (!body && comments.length === 0) {
|
||||
log.info(
|
||||
"review has no body and no inline comments \u2014 skipping submission (no issues found)"
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
skipped: true,
|
||||
reason: "no issues found \u2014 nothing to post"
|
||||
};
|
||||
}
|
||||
let event = approved ? "APPROVE" : "COMMENT";
|
||||
if (event === "APPROVE" && !ctx.prApproveEnabled) {
|
||||
log.info("prApproveEnabled is disabled \u2014 downgrading APPROVE to COMMENT");
|
||||
@@ -147031,7 +147157,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,
|
||||
@@ -147711,7 +147838,7 @@ An existing plan comment was found for this issue. Update that comment with the
|
||||
2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt:
|
||||
- the diff file path
|
||||
- PR metadata (title, file count, commit count, base/head branches)
|
||||
- format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with before/after framing
|
||||
- format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with plain-language \`##\` titles and before/after framing
|
||||
- instruct it to use the TOC to selectively read relevant diff sections, not the entire file
|
||||
- instruct it to return the full summary markdown via \`${ghPullfrogMcpName}/set_output\`
|
||||
3. After the subagent completes, call \`${ghPullfrogMcpName}/create_issue_comment\` with \`type: "Summary"\` and the summary body.
|
||||
@@ -148635,7 +148762,7 @@ Do NOT overwrite a good comment with links/details with a generic message like "
|
||||
|
||||
2. **ANALYZE** - Read the diff file. Use the TOC to selectively read relevant sections \u2014 do not read the entire file unless the PR is small.
|
||||
|
||||
3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with before/after framing.
|
||||
3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with human-readable \`##\` titles and before/after framing.
|
||||
|
||||
4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body.
|
||||
|
||||
@@ -148783,7 +148910,6 @@ var agent = (input) => {
|
||||
return {
|
||||
...input,
|
||||
run: async (ctx) => {
|
||||
log.info(`\xBB agent: ${input.name}`);
|
||||
if (ctx.payload.model) log.info(`\xBB model: ${ctx.payload.model}`);
|
||||
if (ctx.payload.timeout) log.info(`\xBB timeout: ${ctx.payload.timeout}`);
|
||||
log.info(`\xBB push: ${ctx.payload.push}`);
|
||||
@@ -148843,10 +148969,9 @@ function getOpenCodeModels(cliPath) {
|
||||
}
|
||||
var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this.";
|
||||
function resolveOpenCodeModel(ctx) {
|
||||
const envModel = process.env.PULLFROG_MODEL?.trim() || process.env.OPENCODE_MODEL?.trim();
|
||||
const envModel = process.env.PULLFROG_MODEL?.trim();
|
||||
if (envModel) {
|
||||
const source = process.env.PULLFROG_MODEL?.trim() ? "PULLFROG_MODEL" : "OPENCODE_MODEL";
|
||||
log.info(`\xBB model: ${envModel} (override via ${source})`);
|
||||
log.info(`\xBB model: ${envModel} (override via PULLFROG_MODEL)`);
|
||||
return envModel;
|
||||
}
|
||||
if (ctx.modelSlug) {
|
||||
@@ -148861,10 +148986,10 @@ function resolveOpenCodeModel(ctx) {
|
||||
const availableSet = new Set(availableModels);
|
||||
if (availableSet.size > 0) {
|
||||
log.debug(`\xBB opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
||||
const match3 = modelAliases.find((a) => a.recommended && availableSet.has(a.resolve)) ?? modelAliases.find((a) => availableSet.has(a.resolve));
|
||||
const match3 = modelAliases.find((a) => a.preferred && availableSet.has(a.resolve)) ?? modelAliases.find((a) => availableSet.has(a.resolve));
|
||||
if (match3) {
|
||||
log.info(
|
||||
`\xBB model: ${match3.resolve} (auto-selected${match3.recommended ? " \u2014 recommended" : ""} curated match)`
|
||||
`\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
|
||||
);
|
||||
log.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
|
||||
return match3.resolve;
|
||||
@@ -149177,7 +149302,7 @@ var opentoad = agent({
|
||||
install: installOpencodeCli,
|
||||
run: async (ctx) => {
|
||||
const cliPath = await installOpencodeCli();
|
||||
const model = resolveOpenCodeModel({
|
||||
const model = ctx.payload.proxyModel ?? resolveOpenCodeModel({
|
||||
cliPath,
|
||||
modelSlug: ctx.payload.model
|
||||
});
|
||||
@@ -149229,7 +149354,9 @@ to fix this, add the required secret to your GitHub repository:
|
||||
4. set the value to your API key
|
||||
5. click "Add secret"
|
||||
|
||||
configure your model at ${settingsUrl}`;
|
||||
configure your model at ${settingsUrl}
|
||||
|
||||
for full setup instructions, see https://docs.pullfrog.com/keys`;
|
||||
}
|
||||
function hasEnvVar(name) {
|
||||
const value2 = process.env[name];
|
||||
@@ -149367,7 +149494,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,
|
||||
@@ -149832,7 +149960,7 @@ import { isAbsolute, resolve } from "node:path";
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.181",
|
||||
version: "0.0.182",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
@@ -150034,7 +150162,9 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
|
||||
progressCommentId: jsonPayload?.progressCommentId,
|
||||
// permissions: inputs > repoSettings > fallbacks
|
||||
push: inputs.push ?? repoSettings.push ?? "restricted",
|
||||
shell: resolvedShell
|
||||
shell: resolvedShell,
|
||||
// set by proxy logic in main.ts when routing through OpenRouter
|
||||
proxyModel: void 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150149,7 +150279,8 @@ var defaultSettings = {
|
||||
};
|
||||
var defaultRunContext = {
|
||||
settings: defaultSettings,
|
||||
apiToken: ""
|
||||
apiToken: "",
|
||||
oss: false
|
||||
};
|
||||
async function fetchRunContext(params) {
|
||||
const timeoutMs = 3e4;
|
||||
@@ -150180,7 +150311,9 @@ async function fetchRunContext(params) {
|
||||
setupScript: data.settings?.setupScript ?? null,
|
||||
postCheckoutScript: data.settings?.postCheckoutScript ?? null
|
||||
},
|
||||
apiToken: data.apiToken
|
||||
apiToken: data.apiToken,
|
||||
oss: data.oss ?? false,
|
||||
proxyModel: data.proxyModel
|
||||
};
|
||||
} catch {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -150203,7 +150336,9 @@ async function resolveRunContextData(params) {
|
||||
data: repoResponse.data
|
||||
},
|
||||
repoSettings: runContext.settings,
|
||||
apiToken: runContext.apiToken
|
||||
apiToken: runContext.apiToken,
|
||||
oss: runContext.oss,
|
||||
proxyModel: runContext.proxyModel
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150342,6 +150477,48 @@ function resolveOutputSchema() {
|
||||
log.info("\xBB structured output schema provided \u2014 output will be required");
|
||||
return parsed2;
|
||||
}
|
||||
async function mintProxyKey(ctx) {
|
||||
try {
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
||||
const oidcToken = await core5.getIDToken("pullfrog-api");
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
const response = await apiFetch({
|
||||
path: "/api/proxy-token",
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${oidcToken}` }
|
||||
});
|
||||
if (!response.ok) {
|
||||
log.warning(`proxy key mint failed (${response.status})`);
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.key;
|
||||
} catch (error49) {
|
||||
log.warning(`proxy key mint error: ${error49 instanceof Error ? error49.message : String(error49)}`);
|
||||
return null;
|
||||
} finally {
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
}
|
||||
}
|
||||
async function resolveProxyModel(ctx) {
|
||||
if (process.env.PULLFROG_MODEL?.trim()) return;
|
||||
if (ctx.oss && ctx.proxyModel) {
|
||||
if (!ctx.oidcCredentials) {
|
||||
log.warning("\xBB oss repo but no OIDC credentials available \u2014 skipping proxy");
|
||||
return;
|
||||
}
|
||||
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
|
||||
if (!key) return;
|
||||
process.env.OPENROUTER_API_KEY = key;
|
||||
core5.setSecret(key);
|
||||
ctx.payload.proxyModel = ctx.proxyModel;
|
||||
log.info(`\xBB proxy: oss \u2192 ${ctx.proxyModel}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
async function writeJobSummary(toolState) {
|
||||
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
||||
const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
|
||||
@@ -150369,11 +150546,22 @@ 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);
|
||||
const oidcCredentials = process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ? {
|
||||
requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
|
||||
requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
||||
} : null;
|
||||
if (payload.shell !== "enabled") {
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
}
|
||||
await resolveProxyModel({
|
||||
payload,
|
||||
oss: runContext.oss,
|
||||
proxyModel: runContext.proxyModel,
|
||||
oidcCredentials
|
||||
});
|
||||
const octokit = createOctokit(tokenRef.mcpToken);
|
||||
const runInfo = await resolveRun({ octokit });
|
||||
let toolContext;
|
||||
@@ -150401,7 +150589,7 @@ async function main() {
|
||||
const agent2 = resolveAgent();
|
||||
validateAgentApiKey({
|
||||
agent: agent2,
|
||||
model: payload.model,
|
||||
model: payload.proxyModel ?? payload.model,
|
||||
owner: runContext.repo.owner,
|
||||
name: runContext.repo.name
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ export type { ModelAlias, ModelProvider, ProviderConfig } from "./models.ts";
|
||||
export {
|
||||
getModelEnvVars,
|
||||
getModelProvider,
|
||||
getProviderDisplayName,
|
||||
modelAliases,
|
||||
parseModel,
|
||||
providers,
|
||||
|
||||
@@ -18,6 +18,7 @@ export type {
|
||||
export {
|
||||
getModelEnvVars,
|
||||
getModelProvider,
|
||||
getProviderDisplayName,
|
||||
ghPullfrogMcpName,
|
||||
modelAliases,
|
||||
parseModel,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
DEFAULT_ACTIVITY_TIMEOUT_MS,
|
||||
} from "./utils/activity.ts";
|
||||
import { resolveAgent } from "./utils/agent.ts";
|
||||
import { apiFetch } from "./utils/apiFetch.ts";
|
||||
import { validateAgentApiKey } from "./utils/apiKeys.ts";
|
||||
import { resolveBody } from "./utils/body.ts";
|
||||
import { formatUsageSummary, log, writeSummary } from "./utils/cli.ts";
|
||||
@@ -63,6 +64,71 @@ function resolveOutputSchema(): Record<string, unknown> | undefined {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
import type { ResolvedPayload } from "./utils/payload.ts";
|
||||
|
||||
interface OidcCredentials {
|
||||
requestUrl: string;
|
||||
requestToken: string;
|
||||
}
|
||||
|
||||
async function mintProxyKey(ctx: { oidcCredentials: OidcCredentials }): Promise<string | null> {
|
||||
try {
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ctx.oidcCredentials.requestUrl;
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = ctx.oidcCredentials.requestToken;
|
||||
const oidcToken = await core.getIDToken("pullfrog-api");
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
|
||||
const response = await apiFetch({
|
||||
path: "/api/proxy-token",
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${oidcToken}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
log.warning(`proxy key mint failed (${response.status})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { key: string };
|
||||
return data.key;
|
||||
} catch (error) {
|
||||
log.warning(`proxy key mint error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return null;
|
||||
} finally {
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveProxyModel(ctx: {
|
||||
payload: ResolvedPayload;
|
||||
oss: boolean;
|
||||
proxyModel?: string | undefined;
|
||||
oidcCredentials: OidcCredentials | null;
|
||||
}): Promise<void> {
|
||||
// env override = BYOK escape hatch, don't proxy
|
||||
if (process.env.PULLFROG_MODEL?.trim()) return;
|
||||
|
||||
// OSS: server decided the model
|
||||
if (ctx.oss && ctx.proxyModel) {
|
||||
if (!ctx.oidcCredentials) {
|
||||
log.warning("» oss repo but no OIDC credentials available — skipping proxy");
|
||||
return;
|
||||
}
|
||||
const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials });
|
||||
if (!key) return;
|
||||
|
||||
process.env.OPENROUTER_API_KEY = key;
|
||||
core.setSecret(key);
|
||||
ctx.payload.proxyModel = ctx.proxyModel;
|
||||
log.info(`» proxy: oss → ${ctx.proxyModel}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// managed billing will add its path here later
|
||||
}
|
||||
|
||||
async function writeJobSummary(toolState: ToolState): Promise<void> {
|
||||
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
||||
const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
|
||||
@@ -103,18 +169,35 @@ export async function main(): Promise<MainResult> {
|
||||
|
||||
// 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)
|
||||
// - mcpToken: full installation token (not exfiltratable via MCP tools)
|
||||
// resolve tokens first — acquireNewToken needs OIDC env vars for token exchange
|
||||
await using tokenRef = await resolveTokens({ push: payload.push });
|
||||
|
||||
// stash OIDC credentials in memory before wiping from process.env
|
||||
// the agent's shell commands can't access JS variables, so this is safe
|
||||
const oidcCredentials: OidcCredentials | null =
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
||||
? {
|
||||
requestUrl: process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
|
||||
requestToken: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN,
|
||||
}
|
||||
: null;
|
||||
|
||||
// clear OIDC env vars in restricted mode to prevent agent from minting tokens
|
||||
if (payload.shell !== "enabled") {
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
}
|
||||
|
||||
// proxy decision: mint an OpenRouter key for OSS repos (or later, managed billing)
|
||||
await resolveProxyModel({
|
||||
payload,
|
||||
oss: runContext.oss,
|
||||
proxyModel: runContext.proxyModel,
|
||||
oidcCredentials,
|
||||
});
|
||||
|
||||
// create octokit with MCP token for GitHub API calls
|
||||
const octokit = createOctokit(tokenRef.mcpToken);
|
||||
|
||||
@@ -151,7 +234,7 @@ export async function main(): Promise<MainResult> {
|
||||
|
||||
validateAgentApiKey({
|
||||
agent,
|
||||
model: payload.model,
|
||||
model: payload.proxyModel ?? payload.model,
|
||||
owner: runContext.repo.owner,
|
||||
name: runContext.repo.name,
|
||||
});
|
||||
|
||||
+15
-8
@@ -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<string> {
|
||||
@@ -77,17 +78,14 @@ async function buildCommentFooter(params: BuildCommentFooterParams): Promise<str
|
||||
}
|
||||
}
|
||||
|
||||
const footerParams = {
|
||||
return buildPullfrogFooter({
|
||||
triggeredBy: true,
|
||||
workflowRun: runId
|
||||
? { owner: repoContext.owner, repo: repoContext.name, runId, jobId }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (params.customParts && params.customParts.length > 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<string> {
|
||||
if (/<br\s*\/?>[ \t]*\n(?!\s*\n)/i.test(body)) {
|
||||
throw new Error(
|
||||
"body contains <br/> followed by a non-blank line, which breaks GitHub markdown rendering. always add a blank line after <br/> 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}`;
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -84,6 +84,18 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
||||
// set issue context (PRs are issues)
|
||||
ctx.toolState.issueNumber = pull_number;
|
||||
|
||||
// skip empty reviews (no body, no inline comments) — nothing to post
|
||||
if (!body && comments.length === 0) {
|
||||
log.info(
|
||||
"review has no body and no inline comments — skipping submission (no issues found)"
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
skipped: true,
|
||||
reason: "no issues found — nothing to post",
|
||||
};
|
||||
}
|
||||
|
||||
// enforce prApproveEnabled: downgrade APPROVE to COMMENT if disabled
|
||||
let event: "APPROVE" | "COMMENT" = approved ? "APPROVE" : "COMMENT";
|
||||
if (event === "APPROVE" && !ctx.prApproveEnabled) {
|
||||
@@ -264,6 +276,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({
|
||||
|
||||
+1
-1
@@ -193,7 +193,7 @@ An existing plan comment was found for this issue. Update that comment with the
|
||||
2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt:
|
||||
- the diff file path
|
||||
- PR metadata (title, file count, commit count, base/head branches)
|
||||
- format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with before/after framing
|
||||
- format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with plain-language \`##\` titles and before/after framing
|
||||
- instruct it to use the TOC to selectively read relevant diff sections, not the entire file
|
||||
- instruct it to return the full summary markdown via \`${ghPullfrogMcpName}/set_output\`
|
||||
3. After the subagent completes, call \`${ghPullfrogMcpName}/create_issue_comment\` with \`type: "Summary"\` and the summary body.
|
||||
|
||||
@@ -89,6 +89,7 @@ export interface ToolState {
|
||||
existingSummaryCommentId?: number;
|
||||
output?: string;
|
||||
usageEntries: AgentUsage[];
|
||||
model?: string | undefined;
|
||||
}
|
||||
|
||||
interface InitToolStateParams {
|
||||
|
||||
+4
-4
@@ -51,7 +51,7 @@ describe("getModelEnvVars", () => {
|
||||
it("returns empty env vars for free opencode models", () => {
|
||||
expect(getModelEnvVars("opencode/big-pickle")).toEqual([]);
|
||||
expect(getModelEnvVars("opencode/gpt-5-nano")).toEqual([]);
|
||||
expect(getModelEnvVars("opencode/mimo-v2-flash-free")).toEqual([]);
|
||||
expect(getModelEnvVars("opencode/mimo-v2-pro-free")).toEqual([]);
|
||||
expect(getModelEnvVars("opencode/minimax-m2.5-free")).toEqual([]);
|
||||
expect(getModelEnvVars("opencode/nemotron-3-super-free")).toEqual([]);
|
||||
});
|
||||
@@ -96,10 +96,10 @@ describe("modelAliases registry", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("has exactly one recommended model per provider", () => {
|
||||
it("has exactly one preferred model per provider", () => {
|
||||
for (const providerKey of Object.keys(providers)) {
|
||||
const recommended = modelAliases.filter((a) => a.provider === providerKey && a.recommended);
|
||||
expect(recommended.length, `${providerKey} should have exactly 1 recommended model`).toBe(1);
|
||||
const preferred = modelAliases.filter((a) => a.provider === providerKey && a.preferred);
|
||||
expect(preferred.length, `${providerKey} should have exactly 1 preferred model`).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@ export interface ModelAlias {
|
||||
displayName: string;
|
||||
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
|
||||
resolve: string;
|
||||
/** full models.dev specifier for the OpenRouter equivalent (undefined for free models) */
|
||||
openRouterResolve: string | undefined;
|
||||
/** top-tier pick for this provider — preferred during auto-select */
|
||||
recommended: boolean;
|
||||
preferred: boolean;
|
||||
/** whether this alias is free and requires no API key */
|
||||
isFree: boolean;
|
||||
}
|
||||
@@ -26,7 +28,9 @@ interface ModelDef {
|
||||
displayName: string;
|
||||
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
|
||||
resolve: string;
|
||||
recommended?: boolean;
|
||||
/** full models.dev specifier for the OpenRouter equivalent, e.g. "openrouter/anthropic/claude-opus-4.6" */
|
||||
openRouterResolve?: string;
|
||||
preferred?: boolean;
|
||||
envVars?: readonly string[];
|
||||
isFree?: boolean;
|
||||
}
|
||||
@@ -51,19 +55,40 @@ export const providers = {
|
||||
"claude-opus": {
|
||||
displayName: "Claude Opus",
|
||||
resolve: "anthropic/claude-opus-4-6",
|
||||
recommended: true,
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
preferred: true,
|
||||
},
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "anthropic/claude-sonnet-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6",
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "anthropic/claude-haiku-4-5",
|
||||
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5",
|
||||
},
|
||||
"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" },
|
||||
"gpt-codex": {
|
||||
displayName: "GPT Codex",
|
||||
resolve: "openai/gpt-5.3-codex",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex",
|
||||
preferred: true,
|
||||
},
|
||||
"gpt-codex-mini": {
|
||||
displayName: "GPT Codex Mini",
|
||||
resolve: "openai/codex-mini-latest",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
|
||||
},
|
||||
o3: {
|
||||
displayName: "O3",
|
||||
resolve: "openai/o3",
|
||||
},
|
||||
},
|
||||
}),
|
||||
google: provider({
|
||||
@@ -73,18 +98,36 @@ export const providers = {
|
||||
"gemini-pro": {
|
||||
displayName: "Gemini Pro",
|
||||
resolve: "google/gemini-3.1-pro-preview",
|
||||
recommended: true,
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
||||
preferred: true,
|
||||
},
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "google/gemini-3-flash-preview",
|
||||
openRouterResolve: "openrouter/google/gemini-3-flash-preview",
|
||||
},
|
||||
"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" },
|
||||
grok: {
|
||||
displayName: "Grok",
|
||||
resolve: "xai/grok-4",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4",
|
||||
preferred: true,
|
||||
},
|
||||
"grok-fast": {
|
||||
displayName: "Grok Fast",
|
||||
resolve: "xai/grok-4-fast",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4-fast",
|
||||
},
|
||||
"grok-code-fast": {
|
||||
displayName: "Grok Code Fast",
|
||||
resolve: "xai/grok-code-fast-1",
|
||||
openRouterResolve: "openrouter/x-ai/grok-code-fast-1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
deepseek: provider({
|
||||
@@ -94,16 +137,26 @@ export const providers = {
|
||||
"deepseek-reasoner": {
|
||||
displayName: "DeepSeek Reasoner",
|
||||
resolve: "deepseek/deepseek-reasoner",
|
||||
recommended: true,
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
|
||||
preferred: true,
|
||||
},
|
||||
"deepseek-chat": {
|
||||
displayName: "DeepSeek Chat",
|
||||
resolve: "deepseek/deepseek-chat",
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
|
||||
},
|
||||
"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 },
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "moonshotai/kimi-k2.5",
|
||||
openRouterResolve: "openrouter/moonshotai/kimi-k2.5",
|
||||
preferred: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
opencode: provider({
|
||||
@@ -113,27 +166,59 @@ export const providers = {
|
||||
"big-pickle": {
|
||||
displayName: "Big Pickle",
|
||||
resolve: "opencode/big-pickle",
|
||||
recommended: true,
|
||||
preferred: 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" },
|
||||
"claude-opus": {
|
||||
displayName: "Claude Opus",
|
||||
resolve: "opencode/claude-opus-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
},
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "opencode/claude-sonnet-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6",
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "opencode/claude-haiku-4-5",
|
||||
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5",
|
||||
},
|
||||
"gpt-codex": {
|
||||
displayName: "GPT Codex",
|
||||
resolve: "opencode/gpt-5.3-codex",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex",
|
||||
},
|
||||
"gpt-codex-mini": {
|
||||
displayName: "GPT Codex Mini",
|
||||
resolve: "opencode/gpt-5.1-codex-mini",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
|
||||
},
|
||||
"gemini-pro": {
|
||||
displayName: "Gemini Pro",
|
||||
resolve: "opencode/gemini-3.1-pro",
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
||||
},
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "opencode/gemini-3-flash",
|
||||
openRouterResolve: "openrouter/google/gemini-3-flash-preview",
|
||||
},
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "opencode/kimi-k2.5",
|
||||
openRouterResolve: "openrouter/moonshotai/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",
|
||||
"mimo-v2-pro-free": {
|
||||
displayName: "MiMo V2 Pro",
|
||||
resolve: "opencode/mimo-v2-pro-free",
|
||||
envVars: [],
|
||||
isFree: true,
|
||||
},
|
||||
@@ -158,36 +243,59 @@ export const providers = {
|
||||
"claude-opus": {
|
||||
displayName: "Claude Opus",
|
||||
resolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
recommended: true,
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
preferred: true,
|
||||
},
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "openrouter/anthropic/claude-sonnet-4.6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6",
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "openrouter/anthropic/claude-haiku-4.5",
|
||||
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5",
|
||||
},
|
||||
"gpt-codex": {
|
||||
displayName: "GPT Codex",
|
||||
resolve: "openrouter/openai/gpt-5.3-codex",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex",
|
||||
},
|
||||
"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",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
|
||||
},
|
||||
"o4-mini": {
|
||||
displayName: "O4 Mini",
|
||||
resolve: "openrouter/openai/o4-mini",
|
||||
openRouterResolve: "openrouter/openai/o4-mini",
|
||||
},
|
||||
"o4-mini": { displayName: "O4 Mini", resolve: "openrouter/openai/o4-mini" },
|
||||
"gemini-pro": {
|
||||
displayName: "Gemini Pro",
|
||||
resolve: "openrouter/google/gemini-3.1-pro-preview",
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
||||
},
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "openrouter/google/gemini-3-flash-preview",
|
||||
openRouterResolve: "openrouter/google/gemini-3-flash-preview",
|
||||
},
|
||||
grok: {
|
||||
displayName: "Grok",
|
||||
resolve: "openrouter/x-ai/grok-4",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4",
|
||||
},
|
||||
grok: { displayName: "Grok", resolve: "openrouter/x-ai/grok-4" },
|
||||
"deepseek-chat": {
|
||||
displayName: "DeepSeek Chat",
|
||||
resolve: "openrouter/deepseek/deepseek-chat-v3.1",
|
||||
resolve: "openrouter/deepseek/deepseek-v3.2",
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
|
||||
},
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "openrouter/moonshotai/kimi-k2.5",
|
||||
openRouterResolve: "openrouter/moonshotai/kimi-k2.5",
|
||||
},
|
||||
"kimi-k2": { displayName: "Kimi K2", resolve: "openrouter/moonshotai/kimi-k2.5" },
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, ProviderConfig>;
|
||||
@@ -208,6 +316,11 @@ export function getModelProvider(slug: string): string {
|
||||
return parseModel(slug).provider;
|
||||
}
|
||||
|
||||
export function getProviderDisplayName(slug: string): string | undefined {
|
||||
const parsed = parseModel(slug);
|
||||
return (providers as Record<string, ProviderConfig>)[parsed.provider]?.displayName;
|
||||
}
|
||||
|
||||
export function getModelEnvVars(slug: string): string[] {
|
||||
const parsed = parseModel(slug);
|
||||
const providerConfig = (providers as Record<string, ProviderConfig>)[parsed.provider];
|
||||
@@ -232,7 +345,8 @@ export const modelAliases: ModelAlias[] = Object.entries(providers).flatMap(
|
||||
provider: providerKey,
|
||||
displayName: def.displayName,
|
||||
resolve: def.resolve,
|
||||
recommended: def.recommended ?? false,
|
||||
openRouterResolve: def.openRouterResolve,
|
||||
preferred: def.preferred ?? false,
|
||||
isFree: def.isFree ?? false,
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -322,7 +322,7 @@ Do NOT overwrite a good comment with links/details with a generic message like "
|
||||
|
||||
2. **ANALYZE** - Read the diff file. Use the TOC to selectively read relevant sections — do not read the entire file unless the PR is small.
|
||||
|
||||
3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with before/after framing.
|
||||
3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with human-readable \`##\` titles and before/after framing.
|
||||
|
||||
4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body.
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/pullfrog",
|
||||
"version": "0.0.181",
|
||||
"version": "0.0.182",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
@@ -37509,9 +37509,282 @@ 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",
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
preferred: true
|
||||
},
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "anthropic/claude-sonnet-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "anthropic/claude-haiku-4-5",
|
||||
openRouterResolve: "openrouter/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",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex",
|
||||
preferred: true
|
||||
},
|
||||
"gpt-codex-mini": {
|
||||
displayName: "GPT Codex Mini",
|
||||
resolve: "openai/codex-mini-latest",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
|
||||
},
|
||||
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",
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview",
|
||||
preferred: true
|
||||
},
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "google/gemini-3-flash-preview",
|
||||
openRouterResolve: "openrouter/google/gemini-3-flash-preview"
|
||||
}
|
||||
}
|
||||
}),
|
||||
xai: provider({
|
||||
displayName: "xAI",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
models: {
|
||||
grok: {
|
||||
displayName: "Grok",
|
||||
resolve: "xai/grok-4",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4",
|
||||
preferred: true
|
||||
},
|
||||
"grok-fast": {
|
||||
displayName: "Grok Fast",
|
||||
resolve: "xai/grok-4-fast",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4-fast"
|
||||
},
|
||||
"grok-code-fast": {
|
||||
displayName: "Grok Code Fast",
|
||||
resolve: "xai/grok-code-fast-1",
|
||||
openRouterResolve: "openrouter/x-ai/grok-code-fast-1"
|
||||
}
|
||||
}
|
||||
}),
|
||||
deepseek: provider({
|
||||
displayName: "DeepSeek",
|
||||
envVars: ["DEEPSEEK_API_KEY"],
|
||||
models: {
|
||||
"deepseek-reasoner": {
|
||||
displayName: "DeepSeek Reasoner",
|
||||
resolve: "deepseek/deepseek-reasoner",
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
|
||||
preferred: true
|
||||
},
|
||||
"deepseek-chat": {
|
||||
displayName: "DeepSeek Chat",
|
||||
resolve: "deepseek/deepseek-chat",
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2"
|
||||
}
|
||||
}
|
||||
}),
|
||||
moonshotai: provider({
|
||||
displayName: "Moonshot AI",
|
||||
envVars: ["MOONSHOT_API_KEY"],
|
||||
models: {
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "moonshotai/kimi-k2.5",
|
||||
openRouterResolve: "openrouter/moonshotai/kimi-k2.5",
|
||||
preferred: true
|
||||
}
|
||||
}
|
||||
}),
|
||||
opencode: provider({
|
||||
displayName: "OpenCode",
|
||||
envVars: ["OPENCODE_API_KEY"],
|
||||
models: {
|
||||
"big-pickle": {
|
||||
displayName: "Big Pickle",
|
||||
resolve: "opencode/big-pickle",
|
||||
preferred: true,
|
||||
envVars: [],
|
||||
isFree: true
|
||||
},
|
||||
"claude-opus": {
|
||||
displayName: "Claude Opus",
|
||||
resolve: "opencode/claude-opus-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6"
|
||||
},
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "opencode/claude-sonnet-4-6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "opencode/claude-haiku-4-5",
|
||||
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
|
||||
},
|
||||
"gpt-codex": {
|
||||
displayName: "GPT Codex",
|
||||
resolve: "opencode/gpt-5.3-codex",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex"
|
||||
},
|
||||
"gpt-codex-mini": {
|
||||
displayName: "GPT Codex Mini",
|
||||
resolve: "opencode/gpt-5.1-codex-mini",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
|
||||
},
|
||||
"gemini-pro": {
|
||||
displayName: "Gemini Pro",
|
||||
resolve: "opencode/gemini-3.1-pro",
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
||||
},
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "opencode/gemini-3-flash",
|
||||
openRouterResolve: "openrouter/google/gemini-3-flash-preview"
|
||||
},
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "opencode/kimi-k2.5",
|
||||
openRouterResolve: "openrouter/moonshotai/kimi-k2.5"
|
||||
},
|
||||
"gpt-5-nano": {
|
||||
displayName: "GPT Nano",
|
||||
resolve: "opencode/gpt-5-nano",
|
||||
envVars: [],
|
||||
isFree: true
|
||||
},
|
||||
"mimo-v2-pro-free": {
|
||||
displayName: "MiMo V2 Pro",
|
||||
resolve: "opencode/mimo-v2-pro-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",
|
||||
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||
preferred: true
|
||||
},
|
||||
"claude-sonnet": {
|
||||
displayName: "Claude Sonnet",
|
||||
resolve: "openrouter/anthropic/claude-sonnet-4.6",
|
||||
openRouterResolve: "openrouter/anthropic/claude-sonnet-4.6"
|
||||
},
|
||||
"claude-haiku": {
|
||||
displayName: "Claude Haiku",
|
||||
resolve: "openrouter/anthropic/claude-haiku-4.5",
|
||||
openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
|
||||
},
|
||||
"gpt-codex": {
|
||||
displayName: "GPT Codex",
|
||||
resolve: "openrouter/openai/gpt-5.3-codex",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.3-codex"
|
||||
},
|
||||
"gpt-codex-mini": {
|
||||
displayName: "GPT Codex Mini",
|
||||
resolve: "openrouter/openai/gpt-5.1-codex-mini",
|
||||
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
|
||||
},
|
||||
"o4-mini": {
|
||||
displayName: "O4 Mini",
|
||||
resolve: "openrouter/openai/o4-mini",
|
||||
openRouterResolve: "openrouter/openai/o4-mini"
|
||||
},
|
||||
"gemini-pro": {
|
||||
displayName: "Gemini Pro",
|
||||
resolve: "openrouter/google/gemini-3.1-pro-preview",
|
||||
openRouterResolve: "openrouter/google/gemini-3.1-pro-preview"
|
||||
},
|
||||
"gemini-flash": {
|
||||
displayName: "Gemini Flash",
|
||||
resolve: "openrouter/google/gemini-3-flash-preview",
|
||||
openRouterResolve: "openrouter/google/gemini-3-flash-preview"
|
||||
},
|
||||
grok: {
|
||||
displayName: "Grok",
|
||||
resolve: "openrouter/x-ai/grok-4",
|
||||
openRouterResolve: "openrouter/x-ai/grok-4"
|
||||
},
|
||||
"deepseek-chat": {
|
||||
displayName: "DeepSeek Chat",
|
||||
resolve: "openrouter/deepseek/deepseek-v3.2",
|
||||
openRouterResolve: "openrouter/deepseek/deepseek-v3.2"
|
||||
},
|
||||
"kimi-k2": {
|
||||
displayName: "Kimi K2",
|
||||
resolve: "openrouter/moonshotai/kimi-k2.5",
|
||||
openRouterResolve: "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,
|
||||
openRouterResolve: def.openRouterResolve,
|
||||
preferred: def.preferred ?? false,
|
||||
isFree: def.isFree ?? false
|
||||
}))
|
||||
);
|
||||
|
||||
// utils/buildPullfrogFooter.ts
|
||||
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
||||
var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
|
||||
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 +37800,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}
|
||||
<sup>${FROG_LOGO} \uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
|
||||
@@ -41284,7 +41556,7 @@ var core3 = __toESM(require_core(), 1);
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.181",
|
||||
version: "0.0.182",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
|
||||
@@ -19,20 +19,20 @@ 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",
|
||||
"releaseDate": "2026-03-11",
|
||||
"modelId": "mimo-v2-pro-free",
|
||||
"releaseDate": "2026-03-18",
|
||||
},
|
||||
"openrouter": {
|
||||
"modelId": "openrouter/hunter-alpha",
|
||||
"releaseDate": "2026-03-11",
|
||||
"modelId": "xiaomi/mimo-v2-pro",
|
||||
"releaseDate": "2026-03-18",
|
||||
},
|
||||
"xai": {
|
||||
"modelId": "grok-4-1-fast-non-reasoning",
|
||||
"releaseDate": "2025-11-19",
|
||||
"modelId": "grok-4.20-multi-agent-0309",
|
||||
"releaseDate": "2026-03-09",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -64,7 +64,6 @@ const expectedAgentEnvVars = [
|
||||
"GITHUB_TOKEN",
|
||||
...new Set(Object.values(providers).flatMap((p) => [...p.envVars])),
|
||||
"PULLFROG_MODEL",
|
||||
"OPENCODE_MODEL",
|
||||
].sort();
|
||||
|
||||
const expectedAgnosticEnvVars = ["ANTHROPIC_API_KEY", "GITHUB_TOKEN"].sort();
|
||||
|
||||
@@ -46,6 +46,82 @@ describe("models.dev validity", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── openRouterResolve coverage ─────────────────────────────────────────────────
|
||||
|
||||
// models that have no OpenRouter equivalent and require BYOK.
|
||||
// add a model here ONLY when it genuinely doesn't exist on both models.dev and OpenRouter.
|
||||
const BYOK_ONLY_MODELS = new Set(["openai/o3"]);
|
||||
|
||||
describe("openRouterResolve completeness", () => {
|
||||
for (const alias of modelAliases) {
|
||||
if (alias.isFree) continue;
|
||||
if (BYOK_ONLY_MODELS.has(alias.slug)) continue;
|
||||
it(`${alias.slug} has openRouterResolve`, () => {
|
||||
expect(
|
||||
alias.openRouterResolve,
|
||||
`non-free model "${alias.slug}" is missing openRouterResolve — add it or add to BYOK_ONLY_MODELS`
|
||||
).toBeDefined();
|
||||
});
|
||||
}
|
||||
|
||||
for (const alias of modelAliases) {
|
||||
if (!alias.isFree) continue;
|
||||
it(`${alias.slug} (free) does not need openRouterResolve`, () => {
|
||||
expect(alias.openRouterResolve).toBeUndefined();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("openRouterResolve models.dev validity", async () => {
|
||||
const data = await api;
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const alias of modelAliases) {
|
||||
if (!alias.openRouterResolve) continue;
|
||||
if (seen.has(alias.openRouterResolve)) continue;
|
||||
seen.add(alias.openRouterResolve);
|
||||
|
||||
const parsed = parseResolve(alias.openRouterResolve);
|
||||
|
||||
it(`${alias.openRouterResolve} exists on models.dev`, () => {
|
||||
const providerData = data[parsed.provider];
|
||||
expect(providerData, `provider "${parsed.provider}" not found on models.dev`).toBeDefined();
|
||||
const model = providerData.models[parsed.modelId];
|
||||
expect(
|
||||
model,
|
||||
`model "${parsed.modelId}" not found under ${parsed.provider} on models.dev`
|
||||
).toBeDefined();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type OpenRouterModel = { id: string };
|
||||
type OpenRouterModelsResponse = { data: OpenRouterModel[] };
|
||||
|
||||
const openRouterApi = fetch("https://openrouter.ai/api/v1/models").then(
|
||||
(r) => r.json() as Promise<OpenRouterModelsResponse>
|
||||
);
|
||||
|
||||
describe("openRouterResolve OpenRouter API validity", async () => {
|
||||
const orData = await openRouterApi;
|
||||
const orModelIds = new Set(orData.data.map((m) => m.id));
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const alias of modelAliases) {
|
||||
if (!alias.openRouterResolve) continue;
|
||||
const orModelId = alias.openRouterResolve.slice("openrouter/".length);
|
||||
if (seen.has(orModelId)) continue;
|
||||
seen.add(orModelId);
|
||||
|
||||
it(`${orModelId} exists on OpenRouter`, () => {
|
||||
expect(
|
||||
orModelIds.has(orModelId),
|
||||
`model "${orModelId}" not found in OpenRouter API (/api/v1/models)`
|
||||
).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("latest model per provider snapshot", async () => {
|
||||
const data = await api;
|
||||
const providerKeys = Object.keys(providers) as ModelProvider[];
|
||||
@@ -76,6 +152,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();
|
||||
});
|
||||
|
||||
+1
-1
@@ -307,7 +307,7 @@ async function runTestForAgent(ctx: RunContext): Promise<ValidationResult> {
|
||||
|
||||
// use anthropic sonnet to avoid google quota issues and gemini doom-looping
|
||||
if (ctx.agent === "opentoad") {
|
||||
env.OPENCODE_MODEL ??= "anthropic/claude-sonnet-4-5";
|
||||
env.PULLFROG_MODEL ??= "anthropic/claude-sonnet-4-5";
|
||||
}
|
||||
|
||||
// build file-based env vars for MCP servers that don't inherit parent env
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("validateAgentApiKey", () => {
|
||||
it("passes for other free opencode models", () => {
|
||||
for (const slug of [
|
||||
"opencode/gpt-5-nano",
|
||||
"opencode/mimo-v2-flash-free",
|
||||
"opencode/mimo-v2-pro-free",
|
||||
"opencode/minimax-m2.5-free",
|
||||
"opencode/nemotron-3-super-free",
|
||||
]) {
|
||||
|
||||
+10
-1
@@ -20,7 +20,9 @@ to fix this, add the required secret to your GitHub repository:
|
||||
4. set the value to your API key
|
||||
5. click "Add secret"
|
||||
|
||||
configure your model at ${settingsUrl}`;
|
||||
configure your model at ${settingsUrl}
|
||||
|
||||
for full setup instructions, see https://docs.pullfrog.com/keys`;
|
||||
}
|
||||
|
||||
function hasEnvVar(name: string): boolean {
|
||||
@@ -28,6 +30,13 @@ function hasEnvVar(name: string): boolean {
|
||||
return typeof value === "string" && value.length > 0;
|
||||
}
|
||||
|
||||
/** check if the user has a BYOK key for the given model's provider (does not throw) */
|
||||
export function hasProviderKey(model: string): boolean {
|
||||
const requiredVars = getModelEnvVars(model);
|
||||
if (requiredVars.length === 0) return true;
|
||||
return requiredVars.some((v) => hasEnvVar(v));
|
||||
}
|
||||
|
||||
export function validateAgentApiKey(params: {
|
||||
agent: { name: string };
|
||||
model: string | undefined;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { modelAliases } from "../models.ts";
|
||||
|
||||
export const PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
||||
|
||||
const FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
|
||||
@@ -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}
|
||||
|
||||
@@ -118,7 +118,6 @@ const testEnvAllowList = new Set([
|
||||
"GEMINI_API_KEY",
|
||||
"GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
"PULLFROG_MODEL",
|
||||
"OPENCODE_MODEL",
|
||||
"LOG_LEVEL",
|
||||
"DEBUG",
|
||||
"NODE_ENV",
|
||||
|
||||
@@ -36,6 +36,7 @@ export async function reportErrorToComment(ctx: ReportErrorParams): Promise<void
|
||||
triggeredBy: true,
|
||||
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : undefined,
|
||||
customParts,
|
||||
model: ctx.toolState.model,
|
||||
});
|
||||
|
||||
await octokit.rest.issues.updateComment({
|
||||
|
||||
@@ -161,6 +161,9 @@ export function resolvePayload(
|
||||
// permissions: inputs > repoSettings > fallbacks
|
||||
push: inputs.push ?? repoSettings.push ?? "restricted",
|
||||
shell: resolvedShell,
|
||||
|
||||
// set by proxy logic in main.ts when routing through OpenRouter
|
||||
proxyModel: undefined as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface RepoSettings {
|
||||
export interface RunContext {
|
||||
settings: RepoSettings;
|
||||
apiToken: string;
|
||||
oss: boolean;
|
||||
proxyModel?: string | undefined;
|
||||
}
|
||||
|
||||
const defaultSettings: RepoSettings = {
|
||||
@@ -39,6 +41,7 @@ const defaultSettings: RepoSettings = {
|
||||
const defaultRunContext: RunContext = {
|
||||
settings: defaultSettings,
|
||||
apiToken: "",
|
||||
oss: false,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,6 +76,8 @@ export async function fetchRunContext(params: {
|
||||
const data = (await response.json()) as {
|
||||
settings: RepoSettings | null;
|
||||
apiToken: string;
|
||||
oss?: boolean;
|
||||
proxyModel?: string;
|
||||
} | null;
|
||||
|
||||
if (data === null) {
|
||||
@@ -88,6 +93,8 @@ export async function fetchRunContext(params: {
|
||||
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
|
||||
},
|
||||
apiToken: data.apiToken,
|
||||
oss: data.oss ?? false,
|
||||
proxyModel: data.proxyModel,
|
||||
};
|
||||
} catch {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface RunContextData {
|
||||
};
|
||||
repoSettings: RepoSettings;
|
||||
apiToken: string;
|
||||
oss: boolean;
|
||||
proxyModel?: string | undefined;
|
||||
}
|
||||
|
||||
interface ResolveRunContextDataParams {
|
||||
@@ -42,5 +44,7 @@ export async function resolveRunContextData(
|
||||
},
|
||||
repoSettings: runContext.settings,
|
||||
apiToken: runContext.apiToken,
|
||||
oss: runContext.oss,
|
||||
proxyModel: runContext.proxyModel,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user