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
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
@@ -42,11 +42,10 @@ jobs:
|
|||||||
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
PULLFROG_MODEL: ${{ vars.PULLFROG_MODEL }}
|
PULLFROG_MODEL: ${{ vars.PULLFROG_MODEL }}
|
||||||
OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
@@ -80,7 +79,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|||||||
+14
-13
@@ -75,9 +75,9 @@ function buildSecurityConfig(ctx: AgentRunContext, model: string | undefined): s
|
|||||||
// ── model resolution (see wiki/model-resolution.md) ─────────────────────────────
|
// ── model resolution (see wiki/model-resolution.md) ─────────────────────────────
|
||||||
//
|
//
|
||||||
// priority:
|
// 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
|
// 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
|
// 4. undefined → let OpenCode decide
|
||||||
|
|
||||||
function getOpenCodeModels(cliPath: string): string[] {
|
function getOpenCodeModels(cliPath: string): string[] {
|
||||||
@@ -106,11 +106,10 @@ function resolveOpenCodeModel(ctx: {
|
|||||||
cliPath: string;
|
cliPath: string;
|
||||||
modelSlug?: string | undefined;
|
modelSlug?: string | undefined;
|
||||||
}): string | undefined {
|
}): string | undefined {
|
||||||
// 1. explicit env var override (PULLFROG_MODEL takes precedence over OPENCODE_MODEL)
|
// 1. explicit env var override
|
||||||
const envModel = process.env.PULLFROG_MODEL?.trim() || process.env.OPENCODE_MODEL?.trim();
|
const envModel = process.env.PULLFROG_MODEL?.trim();
|
||||||
if (envModel) {
|
if (envModel) {
|
||||||
const source = process.env.PULLFROG_MODEL?.trim() ? "PULLFROG_MODEL" : "OPENCODE_MODEL";
|
log.info(`» model: ${envModel} (override via PULLFROG_MODEL)`);
|
||||||
log.info(`» model: ${envModel} (override via ${source})`);
|
|
||||||
return envModel;
|
return envModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,17 +125,17 @@ function resolveOpenCodeModel(ctx: {
|
|||||||
|
|
||||||
// 3. auto-select: ask OpenCode what's available, pick our best curated match.
|
// 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.
|
// `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 availableModels = getOpenCodeModels(ctx.cliPath);
|
||||||
const availableSet = new Set(availableModels);
|
const availableSet = new Set(availableModels);
|
||||||
if (availableSet.size > 0) {
|
if (availableSet.size > 0) {
|
||||||
log.debug(`» opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
log.debug(`» opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
||||||
const match =
|
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));
|
modelAliases.find((a) => availableSet.has(a.resolve));
|
||||||
if (match) {
|
if (match) {
|
||||||
log.info(
|
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}`);
|
log.warning(`» model auto-selected. ${AUTO_SELECT_WARNING}`);
|
||||||
return match.resolve;
|
return match.resolve;
|
||||||
@@ -622,10 +621,12 @@ export const opentoad = agent({
|
|||||||
run: async (ctx) => {
|
run: async (ctx) => {
|
||||||
const cliPath = await installOpencodeCli();
|
const cliPath = await installOpencodeCli();
|
||||||
|
|
||||||
const model = resolveOpenCodeModel({
|
const model =
|
||||||
cliPath,
|
ctx.payload.proxyModel ??
|
||||||
modelSlug: ctx.payload.model,
|
resolveOpenCodeModel({
|
||||||
});
|
cliPath,
|
||||||
|
modelSlug: ctx.payload.model,
|
||||||
|
});
|
||||||
|
|
||||||
const tempHome = ctx.tmpdir;
|
const tempHome = ctx.tmpdir;
|
||||||
mkdirSync(join(tempHome, ".config", "opencode"), { recursive: true });
|
mkdirSync(join(tempHome, ".config", "opencode"), { recursive: true });
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export const agent = (input: Agent): Agent => {
|
|||||||
return {
|
return {
|
||||||
...input,
|
...input,
|
||||||
run: async (ctx: AgentRunContext): Promise<AgentResult> => {
|
run: async (ctx: AgentRunContext): Promise<AgentResult> => {
|
||||||
log.info(`» agent: ${input.name}`);
|
|
||||||
if (ctx.payload.model) log.info(`» model: ${ctx.payload.model}`);
|
if (ctx.payload.model) log.info(`» model: ${ctx.payload.model}`);
|
||||||
if (ctx.payload.timeout) log.info(`» timeout: ${ctx.payload.timeout}`);
|
if (ctx.payload.timeout) log.info(`» timeout: ${ctx.payload.timeout}`);
|
||||||
log.info(`» push: ${ctx.payload.push}`);
|
log.info(`» push: ${ctx.payload.push}`);
|
||||||
|
|||||||
@@ -19718,10 +19718,10 @@ var require_core = __commonJS({
|
|||||||
(0, command_1.issueCommand)("set-env", { name }, convertedVal);
|
(0, command_1.issueCommand)("set-env", { name }, convertedVal);
|
||||||
}
|
}
|
||||||
exports.exportVariable = exportVariable;
|
exports.exportVariable = exportVariable;
|
||||||
function setSecret2(secret) {
|
function setSecret3(secret) {
|
||||||
(0, command_1.issueCommand)("add-mask", {}, secret);
|
(0, command_1.issueCommand)("add-mask", {}, secret);
|
||||||
}
|
}
|
||||||
exports.setSecret = setSecret2;
|
exports.setSecret = setSecret3;
|
||||||
function addPath(inputPath) {
|
function addPath(inputPath) {
|
||||||
const filePath = process.env["GITHUB_PATH"] || "";
|
const filePath = process.env["GITHUB_PATH"] || "";
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
@@ -19838,12 +19838,12 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
|||||||
return process.env[`STATE_${name}`] || "";
|
return process.env[`STATE_${name}`] || "";
|
||||||
}
|
}
|
||||||
exports.getState = getState;
|
exports.getState = getState;
|
||||||
function getIDToken2(aud) {
|
function getIDToken3(aud) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
return yield oidc_utils_1.OidcClient.getIDToken(aud);
|
return yield oidc_utils_1.OidcClient.getIDToken(aud);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
exports.getIDToken = getIDToken2;
|
exports.getIDToken = getIDToken3;
|
||||||
var summary_1 = require_summary();
|
var summary_1 = require_summary();
|
||||||
Object.defineProperty(exports, "summary", { enumerable: true, get: function() {
|
Object.defineProperty(exports, "summary", { enumerable: true, get: function() {
|
||||||
return summary_1.summary;
|
return summary_1.summary;
|
||||||
@@ -131925,19 +131925,40 @@ var providers = {
|
|||||||
"claude-opus": {
|
"claude-opus": {
|
||||||
displayName: "Claude Opus",
|
displayName: "Claude Opus",
|
||||||
resolve: "anthropic/claude-opus-4-6",
|
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-sonnet": {
|
||||||
"claude-haiku": { displayName: "Claude Haiku", resolve: "anthropic/claude-haiku-4-5" }
|
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({
|
openai: provider({
|
||||||
displayName: "OpenAI",
|
displayName: "OpenAI",
|
||||||
envVars: ["OPENAI_API_KEY"],
|
envVars: ["OPENAI_API_KEY"],
|
||||||
models: {
|
models: {
|
||||||
"gpt-codex": { displayName: "GPT Codex", resolve: "openai/gpt-5.3-codex", recommended: true },
|
"gpt-codex": {
|
||||||
"gpt-codex-mini": { displayName: "GPT Codex Mini", resolve: "openai/codex-mini-latest" },
|
displayName: "GPT Codex",
|
||||||
o3: { displayName: "O3", resolve: "openai/o3" }
|
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({
|
google: provider({
|
||||||
@@ -131947,18 +131968,36 @@ var providers = {
|
|||||||
"gemini-pro": {
|
"gemini-pro": {
|
||||||
displayName: "Gemini Pro",
|
displayName: "Gemini Pro",
|
||||||
resolve: "google/gemini-3.1-pro-preview",
|
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({
|
xai: provider({
|
||||||
displayName: "xAI",
|
displayName: "xAI",
|
||||||
envVars: ["XAI_API_KEY"],
|
envVars: ["XAI_API_KEY"],
|
||||||
models: {
|
models: {
|
||||||
grok: { displayName: "Grok", resolve: "xai/grok-4", recommended: true },
|
grok: {
|
||||||
"grok-fast": { displayName: "Grok Fast", resolve: "xai/grok-4-fast" },
|
displayName: "Grok",
|
||||||
"grok-code-fast": { displayName: "Grok Code Fast", resolve: "xai/grok-code-fast-1" }
|
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({
|
deepseek: provider({
|
||||||
@@ -131968,16 +132007,26 @@ var providers = {
|
|||||||
"deepseek-reasoner": {
|
"deepseek-reasoner": {
|
||||||
displayName: "DeepSeek Reasoner",
|
displayName: "DeepSeek Reasoner",
|
||||||
resolve: "deepseek/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({
|
moonshotai: provider({
|
||||||
displayName: "Moonshot AI",
|
displayName: "Moonshot AI",
|
||||||
envVars: ["MOONSHOT_API_KEY"],
|
envVars: ["MOONSHOT_API_KEY"],
|
||||||
models: {
|
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({
|
opencode: provider({
|
||||||
@@ -131987,27 +132036,59 @@ var providers = {
|
|||||||
"big-pickle": {
|
"big-pickle": {
|
||||||
displayName: "Big Pickle",
|
displayName: "Big Pickle",
|
||||||
resolve: "opencode/big-pickle",
|
resolve: "opencode/big-pickle",
|
||||||
recommended: true,
|
preferred: true,
|
||||||
envVars: [],
|
envVars: [],
|
||||||
isFree: true
|
isFree: true
|
||||||
},
|
},
|
||||||
"claude-opus": { displayName: "Claude Opus", resolve: "opencode/claude-opus-4-6" },
|
"claude-opus": {
|
||||||
"claude-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" },
|
displayName: "Claude Opus",
|
||||||
"claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" },
|
resolve: "opencode/claude-opus-4-6",
|
||||||
"gpt-codex": { displayName: "GPT Codex", resolve: "opencode/gpt-5.3-codex" },
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.6"
|
||||||
"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" },
|
"claude-sonnet": {
|
||||||
"gemini-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" },
|
displayName: "Claude Sonnet",
|
||||||
"kimi-k2": { displayName: "Kimi K2", resolve: "opencode/kimi-k2.5" },
|
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": {
|
"gpt-5-nano": {
|
||||||
displayName: "GPT Nano",
|
displayName: "GPT Nano",
|
||||||
resolve: "opencode/gpt-5-nano",
|
resolve: "opencode/gpt-5-nano",
|
||||||
envVars: [],
|
envVars: [],
|
||||||
isFree: true
|
isFree: true
|
||||||
},
|
},
|
||||||
"mimo-v2-flash-free": {
|
"mimo-v2-pro-free": {
|
||||||
displayName: "MiMo V2 Flash",
|
displayName: "MiMo V2 Pro",
|
||||||
resolve: "opencode/mimo-v2-flash-free",
|
resolve: "opencode/mimo-v2-pro-free",
|
||||||
envVars: [],
|
envVars: [],
|
||||||
isFree: true
|
isFree: true
|
||||||
},
|
},
|
||||||
@@ -132032,36 +132113,59 @@ var providers = {
|
|||||||
"claude-opus": {
|
"claude-opus": {
|
||||||
displayName: "Claude Opus",
|
displayName: "Claude Opus",
|
||||||
resolve: "openrouter/anthropic/claude-opus-4.6",
|
resolve: "openrouter/anthropic/claude-opus-4.6",
|
||||||
recommended: true
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||||
|
preferred: true
|
||||||
},
|
},
|
||||||
"claude-sonnet": {
|
"claude-sonnet": {
|
||||||
displayName: "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": {
|
"claude-haiku": {
|
||||||
displayName: "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": {
|
"gpt-codex-mini": {
|
||||||
displayName: "GPT Codex Mini",
|
displayName: "GPT Codex Mini",
|
||||||
resolve: "openrouter/openai/gpt-5.1-codex-mini"
|
resolve: "openrouter/openai/gpt-5.1-codex-mini",
|
||||||
|
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
|
||||||
|
},
|
||||||
|
"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": {
|
"gemini-pro": {
|
||||||
displayName: "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": {
|
"gemini-flash": {
|
||||||
displayName: "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": {
|
"deepseek-chat": {
|
||||||
displayName: "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,
|
provider: providerKey,
|
||||||
displayName: def.displayName,
|
displayName: def.displayName,
|
||||||
resolve: def.resolve,
|
resolve: def.resolve,
|
||||||
recommended: def.recommended ?? false,
|
openRouterResolve: def.openRouterResolve,
|
||||||
|
preferred: def.preferred ?? false,
|
||||||
isFree: def.isFree ?? false
|
isFree: def.isFree ?? false
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -145015,6 +145120,11 @@ function GetCheckSuiteLogsTool(ctx) {
|
|||||||
// utils/buildPullfrogFooter.ts
|
// utils/buildPullfrogFooter.ts
|
||||||
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
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>`;
|
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) {
|
function buildPullfrogFooter(params) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (params.customParts) {
|
if (params.customParts) {
|
||||||
@@ -145030,11 +145140,10 @@ function buildPullfrogFooter(params) {
|
|||||||
if (params.triggeredBy) {
|
if (params.triggeredBy) {
|
||||||
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
|
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
|
||||||
}
|
}
|
||||||
const allParts = [
|
if (params.model) {
|
||||||
...parts,
|
parts.push(`Using ${formatModelLabel(params.model)}`);
|
||||||
"[pullfrog.com](https://pullfrog.com)",
|
}
|
||||||
"[\u{1D54F}](https://x.com/pullfrogai)"
|
const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
|
||||||
];
|
|
||||||
return `
|
return `
|
||||||
${PULLFROG_DIVIDER}
|
${PULLFROG_DIVIDER}
|
||||||
<sup>${FROG_LOGO} \uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
|
<sup>${FROG_LOGO} \uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
|
||||||
@@ -145098,22 +145207,25 @@ async function buildCommentFooter(params) {
|
|||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const footerParams = {
|
return buildPullfrogFooter({
|
||||||
triggeredBy: true,
|
triggeredBy: true,
|
||||||
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId, jobId } : void 0
|
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId, jobId } : void 0,
|
||||||
};
|
customParts: params.customParts,
|
||||||
if (params.customParts && params.customParts.length > 0) {
|
model: params.model
|
||||||
return buildPullfrogFooter({ ...footerParams, customParts: params.customParts });
|
});
|
||||||
}
|
|
||||||
return buildPullfrogFooter(footerParams);
|
|
||||||
}
|
}
|
||||||
function buildImplementPlanLink(owner, repo, issueNumber, commentId) {
|
function buildImplementPlanLink(owner, repo, issueNumber, commentId) {
|
||||||
const apiUrl = getApiUrl();
|
const apiUrl = getApiUrl();
|
||||||
return `[Implement plan \u2794](${apiUrl}/trigger/${owner}/${repo}/${issueNumber}?action=implement&comment_id=${commentId})`;
|
return `[Implement plan \u2794](${apiUrl}/trigger/${owner}/${repo}/${issueNumber}?action=implement&comment_id=${commentId})`;
|
||||||
}
|
}
|
||||||
async function addFooter(ctx, body) {
|
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 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}`;
|
return `${bodyWithoutFooter}${footer}`;
|
||||||
}
|
}
|
||||||
var Comment = type({
|
var Comment = type({
|
||||||
@@ -145221,7 +145333,8 @@ async function reportProgress(ctx, params) {
|
|||||||
const bodyWithoutFooter = stripExistingFooter(body);
|
const bodyWithoutFooter = stripExistingFooter(body);
|
||||||
const footer = await buildCommentFooter({
|
const footer = await buildCommentFooter({
|
||||||
octokit: ctx.octokit,
|
octokit: ctx.octokit,
|
||||||
customParts
|
customParts,
|
||||||
|
model: ctx.toolState.model
|
||||||
});
|
});
|
||||||
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
||||||
const result2 = await ctx.octokit.rest.issues.updateComment({
|
const result2 = await ctx.octokit.rest.issues.updateComment({
|
||||||
@@ -145247,7 +145360,8 @@ async function reportProgress(ctx, params) {
|
|||||||
const bodyWithoutFooter = stripExistingFooter(body);
|
const bodyWithoutFooter = stripExistingFooter(body);
|
||||||
const footer = await buildCommentFooter({
|
const footer = await buildCommentFooter({
|
||||||
octokit: ctx.octokit,
|
octokit: ctx.octokit,
|
||||||
customParts
|
customParts,
|
||||||
|
model: ctx.toolState.model
|
||||||
});
|
});
|
||||||
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
||||||
const result2 = await ctx.octokit.rest.issues.updateComment({
|
const result2 = await ctx.octokit.rest.issues.updateComment({
|
||||||
@@ -145289,7 +145403,8 @@ async function reportProgress(ctx, params) {
|
|||||||
const bodyWithoutFooter = stripExistingFooter(body);
|
const bodyWithoutFooter = stripExistingFooter(body);
|
||||||
const footer = await buildCommentFooter({
|
const footer = await buildCommentFooter({
|
||||||
octokit: ctx.octokit,
|
octokit: ctx.octokit,
|
||||||
customParts
|
customParts,
|
||||||
|
model: ctx.toolState.model
|
||||||
});
|
});
|
||||||
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
|
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
|
||||||
const updateResult = await ctx.octokit.rest.issues.updateComment({
|
const updateResult = await ctx.octokit.rest.issues.updateComment({
|
||||||
@@ -146722,7 +146837,8 @@ var PullRequest = type({
|
|||||||
function buildPrBodyWithFooter(ctx, body) {
|
function buildPrBodyWithFooter(ctx, body) {
|
||||||
const footer = buildPullfrogFooter({
|
const footer = buildPullfrogFooter({
|
||||||
triggeredBy: true,
|
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));
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
||||||
return `${bodyWithoutFooter}${footer}`;
|
return `${bodyWithoutFooter}${footer}`;
|
||||||
@@ -146898,6 +147014,16 @@ function CreatePullRequestReviewTool(ctx) {
|
|||||||
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
|
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
|
||||||
if (body) body = fixDoubleEscapedString(body);
|
if (body) body = fixDoubleEscapedString(body);
|
||||||
ctx.toolState.issueNumber = pull_number;
|
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";
|
let event = approved ? "APPROVE" : "COMMENT";
|
||||||
if (event === "APPROVE" && !ctx.prApproveEnabled) {
|
if (event === "APPROVE" && !ctx.prApproveEnabled) {
|
||||||
log.info("prApproveEnabled is disabled \u2014 downgrading APPROVE to COMMENT");
|
log.info("prApproveEnabled is disabled \u2014 downgrading APPROVE to COMMENT");
|
||||||
@@ -147031,7 +147157,8 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
|
|||||||
}
|
}
|
||||||
const footer = buildPullfrogFooter({
|
const footer = buildPullfrogFooter({
|
||||||
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,
|
||||||
customParts
|
customParts,
|
||||||
|
model: ctx.toolState.model
|
||||||
});
|
});
|
||||||
return ctx.octokit.rest.pulls.submitReview({
|
return ctx.octokit.rest.pulls.submitReview({
|
||||||
owner: params.owner,
|
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:
|
2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt:
|
||||||
- the diff file path
|
- the diff file path
|
||||||
- PR metadata (title, file count, commit count, base/head branches)
|
- 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 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\`
|
- 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.
|
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.
|
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.
|
4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body.
|
||||||
|
|
||||||
@@ -148783,7 +148910,6 @@ var agent = (input) => {
|
|||||||
return {
|
return {
|
||||||
...input,
|
...input,
|
||||||
run: async (ctx) => {
|
run: async (ctx) => {
|
||||||
log.info(`\xBB agent: ${input.name}`);
|
|
||||||
if (ctx.payload.model) log.info(`\xBB model: ${ctx.payload.model}`);
|
if (ctx.payload.model) log.info(`\xBB model: ${ctx.payload.model}`);
|
||||||
if (ctx.payload.timeout) log.info(`\xBB timeout: ${ctx.payload.timeout}`);
|
if (ctx.payload.timeout) log.info(`\xBB timeout: ${ctx.payload.timeout}`);
|
||||||
log.info(`\xBB push: ${ctx.payload.push}`);
|
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.";
|
var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this.";
|
||||||
function resolveOpenCodeModel(ctx) {
|
function resolveOpenCodeModel(ctx) {
|
||||||
const envModel = process.env.PULLFROG_MODEL?.trim() || process.env.OPENCODE_MODEL?.trim();
|
const envModel = process.env.PULLFROG_MODEL?.trim();
|
||||||
if (envModel) {
|
if (envModel) {
|
||||||
const source = process.env.PULLFROG_MODEL?.trim() ? "PULLFROG_MODEL" : "OPENCODE_MODEL";
|
log.info(`\xBB model: ${envModel} (override via PULLFROG_MODEL)`);
|
||||||
log.info(`\xBB model: ${envModel} (override via ${source})`);
|
|
||||||
return envModel;
|
return envModel;
|
||||||
}
|
}
|
||||||
if (ctx.modelSlug) {
|
if (ctx.modelSlug) {
|
||||||
@@ -148861,10 +148986,10 @@ function resolveOpenCodeModel(ctx) {
|
|||||||
const availableSet = new Set(availableModels);
|
const availableSet = new Set(availableModels);
|
||||||
if (availableSet.size > 0) {
|
if (availableSet.size > 0) {
|
||||||
log.debug(`\xBB opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
|
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) {
|
if (match3) {
|
||||||
log.info(
|
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}`);
|
log.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
|
||||||
return match3.resolve;
|
return match3.resolve;
|
||||||
@@ -149177,7 +149302,7 @@ var opentoad = agent({
|
|||||||
install: installOpencodeCli,
|
install: installOpencodeCli,
|
||||||
run: async (ctx) => {
|
run: async (ctx) => {
|
||||||
const cliPath = await installOpencodeCli();
|
const cliPath = await installOpencodeCli();
|
||||||
const model = resolveOpenCodeModel({
|
const model = ctx.payload.proxyModel ?? resolveOpenCodeModel({
|
||||||
cliPath,
|
cliPath,
|
||||||
modelSlug: ctx.payload.model
|
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
|
4. set the value to your API key
|
||||||
5. click "Add secret"
|
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) {
|
function hasEnvVar(name) {
|
||||||
const value2 = process.env[name];
|
const value2 = process.env[name];
|
||||||
@@ -149367,7 +149494,8 @@ ${ctx.error}` : ctx.error;
|
|||||||
const footer = buildPullfrogFooter({
|
const footer = buildPullfrogFooter({
|
||||||
triggeredBy: true,
|
triggeredBy: true,
|
||||||
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
|
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
|
||||||
customParts
|
customParts,
|
||||||
|
model: ctx.toolState.model
|
||||||
});
|
});
|
||||||
await octokit.rest.issues.updateComment({
|
await octokit.rest.issues.updateComment({
|
||||||
owner: repoContext.owner,
|
owner: repoContext.owner,
|
||||||
@@ -149832,7 +149960,7 @@ import { isAbsolute, resolve } from "node:path";
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.181",
|
version: "0.0.182",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -150034,7 +150162,9 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
|
|||||||
progressCommentId: jsonPayload?.progressCommentId,
|
progressCommentId: jsonPayload?.progressCommentId,
|
||||||
// permissions: inputs > repoSettings > fallbacks
|
// permissions: inputs > repoSettings > fallbacks
|
||||||
push: inputs.push ?? repoSettings.push ?? "restricted",
|
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 = {
|
var defaultRunContext = {
|
||||||
settings: defaultSettings,
|
settings: defaultSettings,
|
||||||
apiToken: ""
|
apiToken: "",
|
||||||
|
oss: false
|
||||||
};
|
};
|
||||||
async function fetchRunContext(params) {
|
async function fetchRunContext(params) {
|
||||||
const timeoutMs = 3e4;
|
const timeoutMs = 3e4;
|
||||||
@@ -150180,7 +150311,9 @@ async function fetchRunContext(params) {
|
|||||||
setupScript: data.settings?.setupScript ?? null,
|
setupScript: data.settings?.setupScript ?? null,
|
||||||
postCheckoutScript: data.settings?.postCheckoutScript ?? null
|
postCheckoutScript: data.settings?.postCheckoutScript ?? null
|
||||||
},
|
},
|
||||||
apiToken: data.apiToken
|
apiToken: data.apiToken,
|
||||||
|
oss: data.oss ?? false,
|
||||||
|
proxyModel: data.proxyModel
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
@@ -150203,7 +150336,9 @@ async function resolveRunContextData(params) {
|
|||||||
data: repoResponse.data
|
data: repoResponse.data
|
||||||
},
|
},
|
||||||
repoSettings: runContext.settings,
|
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");
|
log.info("\xBB structured output schema provided \u2014 output will be required");
|
||||||
return parsed2;
|
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) {
|
async function writeJobSummary(toolState) {
|
||||||
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
||||||
const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
|
const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
|
||||||
@@ -150369,11 +150546,22 @@ async function main() {
|
|||||||
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
||||||
timer.checkpoint("runContextData");
|
timer.checkpoint("runContextData");
|
||||||
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
||||||
|
toolState.model = payload.model;
|
||||||
const tokenRef = __using(_stack2, await resolveTokens({ push: payload.push }), true);
|
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") {
|
if (payload.shell !== "enabled") {
|
||||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||||
}
|
}
|
||||||
|
await resolveProxyModel({
|
||||||
|
payload,
|
||||||
|
oss: runContext.oss,
|
||||||
|
proxyModel: runContext.proxyModel,
|
||||||
|
oidcCredentials
|
||||||
|
});
|
||||||
const octokit = createOctokit(tokenRef.mcpToken);
|
const octokit = createOctokit(tokenRef.mcpToken);
|
||||||
const runInfo = await resolveRun({ octokit });
|
const runInfo = await resolveRun({ octokit });
|
||||||
let toolContext;
|
let toolContext;
|
||||||
@@ -150401,7 +150589,7 @@ async function main() {
|
|||||||
const agent2 = resolveAgent();
|
const agent2 = resolveAgent();
|
||||||
validateAgentApiKey({
|
validateAgentApiKey({
|
||||||
agent: agent2,
|
agent: agent2,
|
||||||
model: payload.model,
|
model: payload.proxyModel ?? payload.model,
|
||||||
owner: runContext.repo.owner,
|
owner: runContext.repo.owner,
|
||||||
name: runContext.repo.name
|
name: runContext.repo.name
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type { ModelAlias, ModelProvider, ProviderConfig } from "./models.ts";
|
|||||||
export {
|
export {
|
||||||
getModelEnvVars,
|
getModelEnvVars,
|
||||||
getModelProvider,
|
getModelProvider,
|
||||||
|
getProviderDisplayName,
|
||||||
modelAliases,
|
modelAliases,
|
||||||
parseModel,
|
parseModel,
|
||||||
providers,
|
providers,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type {
|
|||||||
export {
|
export {
|
||||||
getModelEnvVars,
|
getModelEnvVars,
|
||||||
getModelProvider,
|
getModelProvider,
|
||||||
|
getProviderDisplayName,
|
||||||
ghPullfrogMcpName,
|
ghPullfrogMcpName,
|
||||||
modelAliases,
|
modelAliases,
|
||||||
parseModel,
|
parseModel,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
DEFAULT_ACTIVITY_TIMEOUT_MS,
|
DEFAULT_ACTIVITY_TIMEOUT_MS,
|
||||||
} from "./utils/activity.ts";
|
} from "./utils/activity.ts";
|
||||||
import { resolveAgent } from "./utils/agent.ts";
|
import { resolveAgent } from "./utils/agent.ts";
|
||||||
|
import { apiFetch } from "./utils/apiFetch.ts";
|
||||||
import { validateAgentApiKey } from "./utils/apiKeys.ts";
|
import { validateAgentApiKey } from "./utils/apiKeys.ts";
|
||||||
import { resolveBody } from "./utils/body.ts";
|
import { resolveBody } from "./utils/body.ts";
|
||||||
import { formatUsageSummary, log, writeSummary } from "./utils/cli.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>;
|
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> {
|
async function writeJobSummary(toolState: ToolState): Promise<void> {
|
||||||
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
const usageSummary = formatUsageSummary(toolState.usageEntries);
|
||||||
const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
|
const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
|
||||||
@@ -103,18 +169,35 @@ export async function main(): Promise<MainResult> {
|
|||||||
|
|
||||||
// resolve payload to determine shell permission
|
// resolve payload to determine shell permission
|
||||||
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
||||||
|
toolState.model = payload.model;
|
||||||
|
|
||||||
// resolve tokens:
|
// resolve tokens first — acquireNewToken needs OIDC env vars for token exchange
|
||||||
// - gitToken: contents permission based on push setting (assumed exfiltratable)
|
|
||||||
// - mcpToken: full installation token (not exfiltratable via MCP tools)
|
|
||||||
await using tokenRef = await resolveTokens({ push: payload.push });
|
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
|
// clear OIDC env vars in restricted mode to prevent agent from minting tokens
|
||||||
if (payload.shell !== "enabled") {
|
if (payload.shell !== "enabled") {
|
||||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||||
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
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
|
// create octokit with MCP token for GitHub API calls
|
||||||
const octokit = createOctokit(tokenRef.mcpToken);
|
const octokit = createOctokit(tokenRef.mcpToken);
|
||||||
|
|
||||||
@@ -151,7 +234,7 @@ export async function main(): Promise<MainResult> {
|
|||||||
|
|
||||||
validateAgentApiKey({
|
validateAgentApiKey({
|
||||||
agent,
|
agent,
|
||||||
model: payload.model,
|
model: payload.proxyModel ?? payload.model,
|
||||||
owner: runContext.repo.owner,
|
owner: runContext.repo.owner,
|
||||||
name: runContext.repo.name,
|
name: runContext.repo.name,
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-8
@@ -55,6 +55,7 @@ export const LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
|
|||||||
interface BuildCommentFooterParams {
|
interface BuildCommentFooterParams {
|
||||||
octokit?: OctokitWithPlugins | undefined;
|
octokit?: OctokitWithPlugins | undefined;
|
||||||
customParts?: string[] | undefined;
|
customParts?: string[] | undefined;
|
||||||
|
model?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildCommentFooter(params: BuildCommentFooterParams): Promise<string> {
|
async function buildCommentFooter(params: BuildCommentFooterParams): Promise<string> {
|
||||||
@@ -77,17 +78,14 @@ async function buildCommentFooter(params: BuildCommentFooterParams): Promise<str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const footerParams = {
|
return buildPullfrogFooter({
|
||||||
triggeredBy: true,
|
triggeredBy: true,
|
||||||
workflowRun: runId
|
workflowRun: runId
|
||||||
? { owner: repoContext.owner, repo: repoContext.name, runId, jobId }
|
? { owner: repoContext.owner, repo: repoContext.name, runId, jobId }
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
customParts: params.customParts,
|
||||||
|
model: params.model,
|
||||||
if (params.customParts && params.customParts.length > 0) {
|
});
|
||||||
return buildPullfrogFooter({ ...footerParams, customParts: params.customParts });
|
|
||||||
}
|
|
||||||
return buildPullfrogFooter(footerParams);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildImplementPlanLink(
|
function buildImplementPlanLink(
|
||||||
@@ -102,11 +100,17 @@ function buildImplementPlanLink(
|
|||||||
|
|
||||||
export interface AddFooterCtx {
|
export interface AddFooterCtx {
|
||||||
octokit?: OctokitWithPlugins | undefined;
|
octokit?: OctokitWithPlugins | undefined;
|
||||||
|
toolState?: { model?: string | undefined } | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addFooter(ctx: AddFooterCtx, body: string): Promise<string> {
|
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 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}`;
|
return `${bodyWithoutFooter}${footer}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +266,7 @@ export async function reportProgress(
|
|||||||
const footer = await buildCommentFooter({
|
const footer = await buildCommentFooter({
|
||||||
octokit: ctx.octokit,
|
octokit: ctx.octokit,
|
||||||
customParts,
|
customParts,
|
||||||
|
model: ctx.toolState.model,
|
||||||
});
|
});
|
||||||
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
||||||
|
|
||||||
@@ -299,6 +304,7 @@ export async function reportProgress(
|
|||||||
const footer = await buildCommentFooter({
|
const footer = await buildCommentFooter({
|
||||||
octokit: ctx.octokit,
|
octokit: ctx.octokit,
|
||||||
customParts,
|
customParts,
|
||||||
|
model: ctx.toolState.model,
|
||||||
});
|
});
|
||||||
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
||||||
|
|
||||||
@@ -359,6 +365,7 @@ export async function reportProgress(
|
|||||||
const footer = await buildCommentFooter({
|
const footer = await buildCommentFooter({
|
||||||
octokit: ctx.octokit,
|
octokit: ctx.octokit,
|
||||||
customParts,
|
customParts,
|
||||||
|
model: ctx.toolState.model,
|
||||||
});
|
});
|
||||||
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
|
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function buildPrBodyWithFooter(ctx: ToolContext, body: string): string {
|
|||||||
workflowRun: ctx.runId
|
workflowRun: ctx.runId
|
||||||
? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId }
|
? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId }
|
||||||
: undefined,
|
: undefined,
|
||||||
|
model: ctx.toolState.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
||||||
|
|||||||
@@ -84,6 +84,18 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
// set issue context (PRs are issues)
|
// set issue context (PRs are issues)
|
||||||
ctx.toolState.issueNumber = pull_number;
|
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
|
// enforce prApproveEnabled: downgrade APPROVE to COMMENT if disabled
|
||||||
let event: "APPROVE" | "COMMENT" = approved ? "APPROVE" : "COMMENT";
|
let event: "APPROVE" | "COMMENT" = approved ? "APPROVE" : "COMMENT";
|
||||||
if (event === "APPROVE" && !ctx.prApproveEnabled) {
|
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 }
|
? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId }
|
||||||
: undefined,
|
: undefined,
|
||||||
customParts,
|
customParts,
|
||||||
|
model: ctx.toolState.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
return ctx.octokit.rest.pulls.submitReview({
|
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:
|
2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt:
|
||||||
- the diff file path
|
- the diff file path
|
||||||
- PR metadata (title, file count, commit count, base/head branches)
|
- 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 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\`
|
- 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.
|
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;
|
existingSummaryCommentId?: number;
|
||||||
output?: string;
|
output?: string;
|
||||||
usageEntries: AgentUsage[];
|
usageEntries: AgentUsage[];
|
||||||
|
model?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitToolStateParams {
|
interface InitToolStateParams {
|
||||||
|
|||||||
+4
-4
@@ -51,7 +51,7 @@ describe("getModelEnvVars", () => {
|
|||||||
it("returns empty env vars for free opencode models", () => {
|
it("returns empty env vars for free opencode models", () => {
|
||||||
expect(getModelEnvVars("opencode/big-pickle")).toEqual([]);
|
expect(getModelEnvVars("opencode/big-pickle")).toEqual([]);
|
||||||
expect(getModelEnvVars("opencode/gpt-5-nano")).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/minimax-m2.5-free")).toEqual([]);
|
||||||
expect(getModelEnvVars("opencode/nemotron-3-super-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)) {
|
for (const providerKey of Object.keys(providers)) {
|
||||||
const recommended = modelAliases.filter((a) => a.provider === providerKey && a.recommended);
|
const preferred = modelAliases.filter((a) => a.provider === providerKey && a.preferred);
|
||||||
expect(recommended.length, `${providerKey} should have exactly 1 recommended model`).toBe(1);
|
expect(preferred.length, `${providerKey} should have exactly 1 preferred model`).toBe(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ export interface ModelAlias {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
|
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
|
||||||
resolve: string;
|
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 */
|
/** top-tier pick for this provider — preferred during auto-select */
|
||||||
recommended: boolean;
|
preferred: boolean;
|
||||||
/** whether this alias is free and requires no API key */
|
/** whether this alias is free and requires no API key */
|
||||||
isFree: boolean;
|
isFree: boolean;
|
||||||
}
|
}
|
||||||
@@ -26,7 +28,9 @@ interface ModelDef {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
|
/** concrete models.dev specifier, e.g. "anthropic/claude-opus-4-6" */
|
||||||
resolve: string;
|
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[];
|
envVars?: readonly string[];
|
||||||
isFree?: boolean;
|
isFree?: boolean;
|
||||||
}
|
}
|
||||||
@@ -51,19 +55,40 @@ export const providers = {
|
|||||||
"claude-opus": {
|
"claude-opus": {
|
||||||
displayName: "Claude Opus",
|
displayName: "Claude Opus",
|
||||||
resolve: "anthropic/claude-opus-4-6",
|
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({
|
openai: provider({
|
||||||
displayName: "OpenAI",
|
displayName: "OpenAI",
|
||||||
envVars: ["OPENAI_API_KEY"],
|
envVars: ["OPENAI_API_KEY"],
|
||||||
models: {
|
models: {
|
||||||
"gpt-codex": { displayName: "GPT Codex", resolve: "openai/gpt-5.3-codex", recommended: true },
|
"gpt-codex": {
|
||||||
"gpt-codex-mini": { displayName: "GPT Codex Mini", resolve: "openai/codex-mini-latest" },
|
displayName: "GPT Codex",
|
||||||
o3: { displayName: "O3", resolve: "openai/o3" },
|
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({
|
google: provider({
|
||||||
@@ -73,18 +98,36 @@ export const providers = {
|
|||||||
"gemini-pro": {
|
"gemini-pro": {
|
||||||
displayName: "Gemini Pro",
|
displayName: "Gemini Pro",
|
||||||
resolve: "google/gemini-3.1-pro-preview",
|
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({
|
xai: provider({
|
||||||
displayName: "xAI",
|
displayName: "xAI",
|
||||||
envVars: ["XAI_API_KEY"],
|
envVars: ["XAI_API_KEY"],
|
||||||
models: {
|
models: {
|
||||||
grok: { displayName: "Grok", resolve: "xai/grok-4", recommended: true },
|
grok: {
|
||||||
"grok-fast": { displayName: "Grok Fast", resolve: "xai/grok-4-fast" },
|
displayName: "Grok",
|
||||||
"grok-code-fast": { displayName: "Grok Code Fast", resolve: "xai/grok-code-fast-1" },
|
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({
|
deepseek: provider({
|
||||||
@@ -94,16 +137,26 @@ export const providers = {
|
|||||||
"deepseek-reasoner": {
|
"deepseek-reasoner": {
|
||||||
displayName: "DeepSeek Reasoner",
|
displayName: "DeepSeek Reasoner",
|
||||||
resolve: "deepseek/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({
|
moonshotai: provider({
|
||||||
displayName: "Moonshot AI",
|
displayName: "Moonshot AI",
|
||||||
envVars: ["MOONSHOT_API_KEY"],
|
envVars: ["MOONSHOT_API_KEY"],
|
||||||
models: {
|
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({
|
opencode: provider({
|
||||||
@@ -113,27 +166,59 @@ export const providers = {
|
|||||||
"big-pickle": {
|
"big-pickle": {
|
||||||
displayName: "Big Pickle",
|
displayName: "Big Pickle",
|
||||||
resolve: "opencode/big-pickle",
|
resolve: "opencode/big-pickle",
|
||||||
recommended: true,
|
preferred: true,
|
||||||
envVars: [],
|
envVars: [],
|
||||||
isFree: true,
|
isFree: true,
|
||||||
},
|
},
|
||||||
"claude-opus": { displayName: "Claude Opus", resolve: "opencode/claude-opus-4-6" },
|
"claude-opus": {
|
||||||
"claude-sonnet": { displayName: "Claude Sonnet", resolve: "opencode/claude-sonnet-4-6" },
|
displayName: "Claude Opus",
|
||||||
"claude-haiku": { displayName: "Claude Haiku", resolve: "opencode/claude-haiku-4-5" },
|
resolve: "opencode/claude-opus-4-6",
|
||||||
"gpt-codex": { displayName: "GPT Codex", resolve: "opencode/gpt-5.3-codex" },
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||||
"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" },
|
"claude-sonnet": {
|
||||||
"gemini-flash": { displayName: "Gemini Flash", resolve: "opencode/gemini-3-flash" },
|
displayName: "Claude Sonnet",
|
||||||
"kimi-k2": { displayName: "Kimi K2", resolve: "opencode/kimi-k2.5" },
|
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": {
|
"gpt-5-nano": {
|
||||||
displayName: "GPT Nano",
|
displayName: "GPT Nano",
|
||||||
resolve: "opencode/gpt-5-nano",
|
resolve: "opencode/gpt-5-nano",
|
||||||
envVars: [],
|
envVars: [],
|
||||||
isFree: true,
|
isFree: true,
|
||||||
},
|
},
|
||||||
"mimo-v2-flash-free": {
|
"mimo-v2-pro-free": {
|
||||||
displayName: "MiMo V2 Flash",
|
displayName: "MiMo V2 Pro",
|
||||||
resolve: "opencode/mimo-v2-flash-free",
|
resolve: "opencode/mimo-v2-pro-free",
|
||||||
envVars: [],
|
envVars: [],
|
||||||
isFree: true,
|
isFree: true,
|
||||||
},
|
},
|
||||||
@@ -158,36 +243,59 @@ export const providers = {
|
|||||||
"claude-opus": {
|
"claude-opus": {
|
||||||
displayName: "Claude Opus",
|
displayName: "Claude Opus",
|
||||||
resolve: "openrouter/anthropic/claude-opus-4.6",
|
resolve: "openrouter/anthropic/claude-opus-4.6",
|
||||||
recommended: true,
|
openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
|
||||||
|
preferred: true,
|
||||||
},
|
},
|
||||||
"claude-sonnet": {
|
"claude-sonnet": {
|
||||||
displayName: "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": {
|
"claude-haiku": {
|
||||||
displayName: "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": {
|
"gpt-codex-mini": {
|
||||||
displayName: "GPT Codex Mini",
|
displayName: "GPT Codex Mini",
|
||||||
resolve: "openrouter/openai/gpt-5.1-codex-mini",
|
resolve: "openrouter/openai/gpt-5.1-codex-mini",
|
||||||
|
openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
|
||||||
|
},
|
||||||
|
"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": {
|
"gemini-pro": {
|
||||||
displayName: "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": {
|
"gemini-flash": {
|
||||||
displayName: "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": {
|
"deepseek-chat": {
|
||||||
displayName: "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>;
|
} satisfies Record<string, ProviderConfig>;
|
||||||
@@ -208,6 +316,11 @@ export function getModelProvider(slug: string): string {
|
|||||||
return parseModel(slug).provider;
|
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[] {
|
export function getModelEnvVars(slug: string): string[] {
|
||||||
const parsed = parseModel(slug);
|
const parsed = parseModel(slug);
|
||||||
const providerConfig = (providers as Record<string, ProviderConfig>)[parsed.provider];
|
const providerConfig = (providers as Record<string, ProviderConfig>)[parsed.provider];
|
||||||
@@ -232,7 +345,8 @@ export const modelAliases: ModelAlias[] = Object.entries(providers).flatMap(
|
|||||||
provider: providerKey,
|
provider: providerKey,
|
||||||
displayName: def.displayName,
|
displayName: def.displayName,
|
||||||
resolve: def.resolve,
|
resolve: def.resolve,
|
||||||
recommended: def.recommended ?? false,
|
openRouterResolve: def.openRouterResolve,
|
||||||
|
preferred: def.preferred ?? false,
|
||||||
isFree: def.isFree ?? 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.
|
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.
|
4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/pullfrog",
|
"name": "@pullfrog/pullfrog",
|
||||||
"version": "0.0.181",
|
"version": "0.0.182",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -37509,9 +37509,282 @@ function getApiUrl() {
|
|||||||
return raw;
|
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
|
// utils/buildPullfrogFooter.ts
|
||||||
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
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>`;
|
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) {
|
function buildPullfrogFooter(params) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (params.customParts) {
|
if (params.customParts) {
|
||||||
@@ -37527,11 +37800,10 @@ function buildPullfrogFooter(params) {
|
|||||||
if (params.triggeredBy) {
|
if (params.triggeredBy) {
|
||||||
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
|
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
|
||||||
}
|
}
|
||||||
const allParts = [
|
if (params.model) {
|
||||||
...parts,
|
parts.push(`Using ${formatModelLabel(params.model)}`);
|
||||||
"[pullfrog.com](https://pullfrog.com)",
|
}
|
||||||
"[\u{1D54F}](https://x.com/pullfrogai)"
|
const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
|
||||||
];
|
|
||||||
return `
|
return `
|
||||||
${PULLFROG_DIVIDER}
|
${PULLFROG_DIVIDER}
|
||||||
<sup>${FROG_LOGO} \uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
|
<sup>${FROG_LOGO} \uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
|
||||||
@@ -41284,7 +41556,7 @@ var core3 = __toESM(require_core(), 1);
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.181",
|
version: "0.0.182",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -19,20 +19,20 @@ exports[`latest model per provider snapshot > matches snapshot 1`] = `
|
|||||||
"releaseDate": "2026-01",
|
"releaseDate": "2026-01",
|
||||||
},
|
},
|
||||||
"openai": {
|
"openai": {
|
||||||
"modelId": "gpt-5.4-pro",
|
"modelId": "gpt-5.4-nano",
|
||||||
"releaseDate": "2026-03-05",
|
"releaseDate": "2026-03-17",
|
||||||
},
|
},
|
||||||
"opencode": {
|
"opencode": {
|
||||||
"modelId": "nemotron-3-super-free",
|
"modelId": "mimo-v2-pro-free",
|
||||||
"releaseDate": "2026-03-11",
|
"releaseDate": "2026-03-18",
|
||||||
},
|
},
|
||||||
"openrouter": {
|
"openrouter": {
|
||||||
"modelId": "openrouter/hunter-alpha",
|
"modelId": "xiaomi/mimo-v2-pro",
|
||||||
"releaseDate": "2026-03-11",
|
"releaseDate": "2026-03-18",
|
||||||
},
|
},
|
||||||
"xai": {
|
"xai": {
|
||||||
"modelId": "grok-4-1-fast-non-reasoning",
|
"modelId": "grok-4.20-multi-agent-0309",
|
||||||
"releaseDate": "2025-11-19",
|
"releaseDate": "2026-03-09",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ const expectedAgentEnvVars = [
|
|||||||
"GITHUB_TOKEN",
|
"GITHUB_TOKEN",
|
||||||
...new Set(Object.values(providers).flatMap((p) => [...p.envVars])),
|
...new Set(Object.values(providers).flatMap((p) => [...p.envVars])),
|
||||||
"PULLFROG_MODEL",
|
"PULLFROG_MODEL",
|
||||||
"OPENCODE_MODEL",
|
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const expectedAgnosticEnvVars = ["ANTHROPIC_API_KEY", "GITHUB_TOKEN"].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 () => {
|
describe("latest model per provider snapshot", async () => {
|
||||||
const data = await api;
|
const data = await api;
|
||||||
const providerKeys = Object.keys(providers) as ModelProvider[];
|
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", () => {
|
it("matches snapshot", () => {
|
||||||
expect(latestByProvider).toMatchSnapshot();
|
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
|
// use anthropic sonnet to avoid google quota issues and gemini doom-looping
|
||||||
if (ctx.agent === "opentoad") {
|
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
|
// 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", () => {
|
it("passes for other free opencode models", () => {
|
||||||
for (const slug of [
|
for (const slug of [
|
||||||
"opencode/gpt-5-nano",
|
"opencode/gpt-5-nano",
|
||||||
"opencode/mimo-v2-flash-free",
|
"opencode/mimo-v2-pro-free",
|
||||||
"opencode/minimax-m2.5-free",
|
"opencode/minimax-m2.5-free",
|
||||||
"opencode/nemotron-3-super-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
|
4. set the value to your API key
|
||||||
5. click "Add secret"
|
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 {
|
function hasEnvVar(name: string): boolean {
|
||||||
@@ -28,6 +30,13 @@ function hasEnvVar(name: string): boolean {
|
|||||||
return typeof value === "string" && value.length > 0;
|
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: {
|
export function validateAgentApiKey(params: {
|
||||||
agent: { name: string };
|
agent: { name: string };
|
||||||
model: string | undefined;
|
model: string | undefined;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { modelAliases } from "../models.ts";
|
||||||
|
|
||||||
export const PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
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>`;
|
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.) */
|
/** alternative: just pass a pre-built URL directly (for shortlinks etc.) */
|
||||||
workflowRunUrl?: string | undefined;
|
workflowRunUrl?: string | undefined;
|
||||||
/** arbitrary custom parts (e.g., action links) */
|
/** 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
|
* build a pullfrog footer with configurable parts
|
||||||
* always includes: frog logo at start, pullfrog.com link and X link at end
|
* always includes: frog logo at start and X link at end
|
||||||
* order: action links (customParts) > workflow run > attribution > reference links
|
* order: action links (customParts) > workflow run > model > attribution > reference links
|
||||||
*/
|
*/
|
||||||
export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
|
export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
@@ -45,11 +55,11 @@ export function buildPullfrogFooter(params: BuildPullfrogFooterParams): string {
|
|||||||
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
|
parts.push("Triggered by [Pullfrog](https://pullfrog.com)");
|
||||||
}
|
}
|
||||||
|
|
||||||
const allParts = [
|
if (params.model) {
|
||||||
...parts,
|
parts.push(`Using ${formatModelLabel(params.model)}`);
|
||||||
"[pullfrog.com](https://pullfrog.com)",
|
}
|
||||||
"[𝕏](https://x.com/pullfrogai)",
|
|
||||||
];
|
const allParts = [...parts, "[𝕏](https://x.com/pullfrogai)"];
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${PULLFROG_DIVIDER}
|
${PULLFROG_DIVIDER}
|
||||||
|
|||||||
@@ -118,7 +118,6 @@ const testEnvAllowList = new Set([
|
|||||||
"GEMINI_API_KEY",
|
"GEMINI_API_KEY",
|
||||||
"GOOGLE_GENERATIVE_AI_API_KEY",
|
"GOOGLE_GENERATIVE_AI_API_KEY",
|
||||||
"PULLFROG_MODEL",
|
"PULLFROG_MODEL",
|
||||||
"OPENCODE_MODEL",
|
|
||||||
"LOG_LEVEL",
|
"LOG_LEVEL",
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export async function reportErrorToComment(ctx: ReportErrorParams): Promise<void
|
|||||||
triggeredBy: true,
|
triggeredBy: true,
|
||||||
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : undefined,
|
workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : undefined,
|
||||||
customParts,
|
customParts,
|
||||||
|
model: ctx.toolState.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
await octokit.rest.issues.updateComment({
|
await octokit.rest.issues.updateComment({
|
||||||
|
|||||||
@@ -161,6 +161,9 @@ export function resolvePayload(
|
|||||||
// permissions: inputs > repoSettings > fallbacks
|
// permissions: inputs > repoSettings > fallbacks
|
||||||
push: inputs.push ?? repoSettings.push ?? "restricted",
|
push: inputs.push ?? repoSettings.push ?? "restricted",
|
||||||
shell: resolvedShell,
|
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 {
|
export interface RunContext {
|
||||||
settings: RepoSettings;
|
settings: RepoSettings;
|
||||||
apiToken: string;
|
apiToken: string;
|
||||||
|
oss: boolean;
|
||||||
|
proxyModel?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSettings: RepoSettings = {
|
const defaultSettings: RepoSettings = {
|
||||||
@@ -39,6 +41,7 @@ const defaultSettings: RepoSettings = {
|
|||||||
const defaultRunContext: RunContext = {
|
const defaultRunContext: RunContext = {
|
||||||
settings: defaultSettings,
|
settings: defaultSettings,
|
||||||
apiToken: "",
|
apiToken: "",
|
||||||
|
oss: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,6 +76,8 @@ export async function fetchRunContext(params: {
|
|||||||
const data = (await response.json()) as {
|
const data = (await response.json()) as {
|
||||||
settings: RepoSettings | null;
|
settings: RepoSettings | null;
|
||||||
apiToken: string;
|
apiToken: string;
|
||||||
|
oss?: boolean;
|
||||||
|
proxyModel?: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
@@ -88,6 +93,8 @@ export async function fetchRunContext(params: {
|
|||||||
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
|
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
|
||||||
},
|
},
|
||||||
apiToken: data.apiToken,
|
apiToken: data.apiToken,
|
||||||
|
oss: data.oss ?? false,
|
||||||
|
proxyModel: data.proxyModel,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export interface RunContextData {
|
|||||||
};
|
};
|
||||||
repoSettings: RepoSettings;
|
repoSettings: RepoSettings;
|
||||||
apiToken: string;
|
apiToken: string;
|
||||||
|
oss: boolean;
|
||||||
|
proxyModel?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolveRunContextDataParams {
|
interface ResolveRunContextDataParams {
|
||||||
@@ -42,5 +44,7 @@ export async function resolveRunContextData(
|
|||||||
},
|
},
|
||||||
repoSettings: runContext.settings,
|
repoSettings: runContext.settings,
|
||||||
apiToken: runContext.apiToken,
|
apiToken: runContext.apiToken,
|
||||||
|
oss: runContext.oss,
|
||||||
|
proxyModel: runContext.proxyModel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user