add OpenRouter proxy for managed model routing and OSS program (#488)

* add OpenRouter proxy for managed model routing and OSS program

proxy layer that mints ephemeral OpenRouter keys for users without BYOK
API keys. two paths: pro plan users get their selected model proxied via
OpenRouter; OSS program repos (hard-coded allowlist in config.ts) get
free Claude Opus. BYOK env vars (PULLFROG_MODEL/OPENCODE_MODEL) always
take precedence.

frontend: OSS repos see a static "Opus (Free)" badge with the model
dropdown disabled and no API key requirement. all models now carry
openRouterResolve metadata for proxy target resolution.

Made-with: Cursor

* implement OSS program: proxy infrastructure for free model credits

server-side OSS allowlist determines eligible repos. action mints
ephemeral OpenRouter keys via OIDC-authenticated /api/proxy-token
endpoint (idempotent on runId, $10 per-key safety limit). keys are
disabled on workflow completion when no running refs remain. HWM-based
usage sync tracks cumulative spend per account.

schema: ProxyKey model, Account.usageUsd/activeKeyId, WorkflowRun.proxyKeyId
action: OIDC credential stashing, resolveProxyModel uses server oss flag
frontend: isOss flows from server page to components (no client allowlist)
Made-with: Cursor

* address PR review: repo cross-check, key retirement lifecycle, dead code removal

- proxy-token: verify runId belongs to OIDC-authenticated repo via repo relation
- add retireKey() shared primitive: disable in OpenRouter first, then mark disabled in DB
- rotateKey: retire old active key after swap to prevent orphans
- webhook: replace inline cleanupProxyKey with retireKey calls
- syncAccountUsage: skip disabled keys
- remove vestigial AccountPlan/plan field from action types
- add disabled field to ProxyKey schema + migration

Made-with: Cursor

* replace deprecated opencode/mimo-v2-flash-free with mimo-v2-pro-free

Made-with: Cursor

* fix migration ordering: rename disabled migration to sort after table creation

Made-with: Cursor

* squash proxy key migrations into single migration

Made-with: Cursor

* add preview repo to OSS allowlist for testing

Made-with: Cursor

* populate OSS allowlist from oss-program-invitees.json

Made-with: Cursor

* format oss-program-invitees.json

Made-with: Cursor

* add installed public repos to OSS allowlist

split ossRepos into three provenance-tracked lists:
- internalRepos (pullfrog, colinhacks, RobinTail)
- installedPublicRepos (external public non-fork repos with active installs)
- invitees (from oss-program-invitees.json)

also adds scripts/list-oss-candidates.ts to regenerate the installed list

Made-with: Cursor

* fix: resolve tokens before clearing OIDC env vars

resolveTokens → acquireNewToken → isOIDCAvailable() checks
ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN env vars. The new OIDC
stashing code was deleting them in restricted shell mode before
resolveTokens ran, causing it to fall through to the GitHub App
path which requires GITHUB_APP_ID/GITHUB_PRIVATE_KEY.

Made-with: Cursor

* derive proxy-token auth from OIDC claims, add ensureWorkflowRun upsert

- proxy-token no longer requires body.runId; uses claims.run_id + claims.repository
- shared ensureWorkflowRun upsert called from both webhook and proxy-token
- workflow_run_requested handler now eagerly creates WorkflowRun records
- eliminates race condition between webhook and action proxy-token call

Made-with: Cursor

* hardcode PULLFROG_ACCOUNT_ID, document preview debugging lessons

GitHub node IDs are constant — no reason for this to be an env var.
Removes the trailing-newline bug that caused P2025 errors.
Adds wiki docs on workflow testing, Vercel env gotchas, and Neon
preview branch discovery.

Made-with: Cursor

* fix: parse OpenRouter create-key response correctly

the API returns `key` at the top level, not inside `data`

Made-with: Cursor

* onboarding cards, unlock OSS model selection, simplify console

- add OnboardingCard component with two states: workflow install
  and model+test (dispatches "Tell me a joke" for test run)
- replace PromptBox overlay gates with dedicated onboarding cards;
  PromptBox is now just the form, always enabled
- use hasWorkflowRuns DB check to decide onboarding vs promptbox
- unlock ModelSelector for OSS repos (was locked to Opus badge);
  resolve proxyModel from repo's selected model alias in run-context
- rename "API key" row to "Model costs" with pure client-side states:
  OSS covered, auto-resolve, free model, BYOK with env var names
- add "(Recommended)" badge to model aliases with recommended: true
- remove OSS_MODEL_DISPLAY_NAME constant and secrets-fetching logic

Made-with: Cursor

* update stale xai model snapshot

Made-with: Cursor

* rename Permissions to Security, hide git push toggle, add shell isolation toggle with disabled state for public repos, remove opentoad agent name from logs

Made-with: Cursor

* chevron hover states, sidebar hooks/security entries

Made-with: Cursor

* address PR review feedback: rename recommended→preferred, fix dispatch orphan, update wiki docs

- rename `recommended` to `preferred` in model alias registry to distinguish
  from the UI "Recommended" badge (which is hardcoded for opus + codex only)
- cancel precreated WorkflowRun when workflow lookup fails in dispatch-workflow
- replace run_sql/vercel env pull in wiki docs with neonctl + Prisma pattern
- extend scripts/neon-branch.ts to output DATABASE_URL via neonctl
- add Recommended badge to GPT Codex alongside Claude Opus in ModelSelector

Made-with: Cursor
This commit is contained in:
Colin McDonnell
2026-03-25 17:19:55 +00:00
committed by pullfrog[bot]
parent 39525547b5
commit e6d34ee01b
20 changed files with 706 additions and 146 deletions
-1
View File
@@ -42,7 +42,6 @@ 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
+14 -13
View File
@@ -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 });
-1
View File
@@ -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}`);
+1
View File
@@ -0,0 +1 @@
// action-level constants shared across the action runtime
+214 -52
View File
@@ -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,18 +132036,50 @@ 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",
@@ -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
})) }))
); );
@@ -148805,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}`);
@@ -148865,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) {
@@ -148883,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;
@@ -149199,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
}); });
@@ -149857,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",
@@ -150059,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
}; };
} }
@@ -150174,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;
@@ -150205,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);
@@ -150228,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
}; };
} }
@@ -150367,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);
@@ -150396,10 +150548,20 @@ async function main() {
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings); const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
toolState.model = payload.model; 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;
@@ -150427,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
}); });
+1
View File
@@ -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,
+1
View File
@@ -18,6 +18,7 @@ export type {
export { export {
getModelEnvVars, getModelEnvVars,
getModelProvider, getModelProvider,
getProviderDisplayName,
ghPullfrogMcpName, ghPullfrogMcpName,
modelAliases, modelAliases,
parseModel, parseModel,
+86 -4
View File
@@ -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);
@@ -105,17 +171,33 @@ export async function main(): Promise<MainResult> {
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings); const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
toolState.model = payload.model; 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);
@@ -152,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,
}); });
+3 -3
View File
@@ -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);
} }
}); });
+146 -32
View File
@@ -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,18 +166,50 @@ 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",
@@ -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,
})) }))
); );
+1 -1
View File
@@ -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",
+141 -36
View File
@@ -37521,19 +37521,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({
@@ -37543,18 +37564,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({
@@ -37564,16 +37603,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({
@@ -37583,18 +37632,50 @@ 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",
@@ -37628,36 +37709,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"
}
} }
}) })
}; };
@@ -37667,7 +37771,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
})) }))
); );
@@ -41451,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",
-1
View File
@@ -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();
+76
View File
@@ -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[];
+1 -1
View File
@@ -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
+7
View File
@@ -30,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
View File
@@ -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",
+3
View File
@@ -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,
}; };
} }
+7
View File
@@ -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);
+4
View File
@@ -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,
}; };
} }