extract resolveModel() to run before agent selection

model resolution was duplicated inside each agent (opentoad, claude) and
PULLFROG_MODEL override was not considered when choosing the agent. now
resolveModel() runs first in main.ts, its result feeds into resolveAgent()
for agent selection, and the resolved model is passed to the agent via
ctx.resolvedModel. agents only handle their own fallback (opentoad: auto-select
via opencode models, claude: strip provider prefix).

also removes the hardcoded anthropic/claude-sonnet test runner default since
ANTHROPIC_API_KEY is no longer in CI.

Made-with: Cursor
This commit is contained in:
Colin McDonnell
2026-04-01 06:51:18 +00:00
committed by pullfrog[bot]
parent 1f1e3995f9
commit b1f9878877
8 changed files with 83 additions and 127 deletions
+8 -27
View File
@@ -14,7 +14,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { performance } from "node:perf_hooks";
import { ghPullfrogMcpName } from "../external.ts";
import { resolveModelSlug } from "../models.ts";
import { getIdleMs, markActivity } from "../utils/activity.ts";
import { log } from "../utils/cli.ts";
import { installFromNpmTarball } from "../utils/install.ts";
@@ -52,32 +52,12 @@ function writeMcpConfig(ctx: AgentRunContext): string {
return configPath;
}
// ── model resolution ─────────────────────────────────────────────────────────
// ── model helpers ─────────────────────────────────────────────────────────────
function resolveClaudeModel(modelSlug: string | undefined): string | undefined {
// 1. explicit env var override
const envModel = process.env.PULLFROG_MODEL?.trim();
if (envModel) {
const slashIndex = envModel.indexOf("/");
const cliModel = slashIndex > 0 ? envModel.slice(slashIndex + 1) : envModel;
log.info(`» model: ${cliModel} (override via PULLFROG_MODEL)`);
return cliModel;
}
if (!modelSlug) return undefined;
// 2. resolve slug to concrete specifier (e.g. "anthropic/claude-opus" → "anthropic/claude-opus-4-6")
// then strip the "anthropic/" prefix to get the Claude CLI model name
const resolved = resolveModelSlug(modelSlug);
if (resolved) {
const slashIndex = resolved.indexOf("/");
const cliModel = slashIndex > 0 ? resolved.slice(slashIndex + 1) : resolved;
log.info(`» model: ${cliModel} (resolved from ${modelSlug})`);
return cliModel;
}
log.warning(`» unknown model slug "${modelSlug}" — letting Claude Code auto-select`);
return undefined;
// claude CLI expects bare model names (e.g. "claude-sonnet-4-6"), not provider-prefixed specifiers
function stripProviderPrefix(specifier: string): string {
const slashIndex = specifier.indexOf("/");
return slashIndex > 0 ? specifier.slice(slashIndex + 1) : specifier;
}
// ── NDJSON event types ─────────────────────────────────────────────────────────
@@ -488,7 +468,8 @@ export const claude = agent({
run: async (ctx) => {
const cliPath = await installClaudeCli();
const model = ctx.payload.proxyModel ?? resolveClaudeModel(ctx.payload.model);
const specifier = ctx.payload.proxyModel ?? ctx.resolvedModel;
const model = specifier ? stripProviderPrefix(specifier) : undefined;
const homeEnv = {
HOME: ctx.tmpdir,
+8 -42
View File
@@ -15,7 +15,7 @@ import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { performance } from "node:perf_hooks";
import { ghPullfrogMcpName } from "../external.ts";
import { modelAliases, resolveCliModel } from "../models.ts";
import { modelAliases } from "../models.ts";
import { getIdleMs, markActivity } from "../utils/activity.ts";
import { log } from "../utils/cli.ts";
import { installFromNpmTarball } from "../utils/install.ts";
@@ -74,13 +74,11 @@ function buildSecurityConfig(ctx: AgentRunContext, model: string | undefined): s
return JSON.stringify(config);
}
// ── model resolution (see wiki/model-resolution.md) ─────────────────────────────
// ── model auto-select fallback ──────────────────────────────────────────────────
//
// priority:
// 1. PULLFROG_MODEL env var (explicit override)
// 2. explicit slug from repo config / payload
// 3. auto-select: `opencode models` → preferred aliases first, then secondary
// 4. undefined → let OpenCode decide
// steps 12 of model resolution (PULLFROG_MODEL env, slug resolution) are handled
// by resolveModel() in utils/agent.ts before the agent runs. this fallback only
// handles step 3: auto-select via `opencode models`.
function getOpenCodeModels(cliPath: string): string[] {
try {
@@ -104,35 +102,8 @@ function getOpenCodeModels(cliPath: string): string[] {
const AUTO_SELECT_WARNING =
"select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this.";
function resolveOpenCodeModel(ctx: {
cliPath: string;
modelSlug?: string | undefined;
}): string | undefined {
// 1. explicit env var override
const envModel = process.env.PULLFROG_MODEL?.trim();
if (envModel) {
log.info(`» model: ${envModel} (override via PULLFROG_MODEL)`);
return envModel;
}
// 2. explicit slug from repo config / payload
if (ctx.modelSlug) {
const resolved = resolveCliModel(ctx.modelSlug);
if (resolved) {
if (resolved !== ctx.modelSlug) {
log.info(`» model: ${ctx.modelSlug} (resolved to ${resolved})`);
} else {
log.info(`» model: ${resolved}`);
}
return resolved;
}
log.warning(`» unknown model slug "${ctx.modelSlug}" — falling through to auto-select`);
}
// 3. auto-select: ask OpenCode what's available, pick our best curated match.
// `opencode models` returns `provider/model-id` specifiers matching our resolve values exactly.
// two-pass: preferred (top-tier per provider) first, then secondary models.
const availableModels = getOpenCodeModels(ctx.cliPath);
function autoSelectModel(cliPath: string): string | undefined {
const availableModels = getOpenCodeModels(cliPath);
const availableSet = new Set(availableModels);
if (availableSet.size > 0) {
log.debug(`» opencode models (${availableSet.size}): ${availableModels.join(", ")}`);
@@ -625,12 +596,7 @@ export const opentoad = agent({
run: async (ctx) => {
const cliPath = await installOpencodeCli();
const model =
ctx.payload.proxyModel ??
resolveOpenCodeModel({
cliPath,
modelSlug: ctx.payload.model,
});
const model = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
const homeEnv = {
HOME: ctx.tmpdir,
+1
View File
@@ -31,6 +31,7 @@ export interface AgentResult {
*/
export interface AgentRunContext {
payload: ResolvedPayload;
resolvedModel?: string | undefined;
mcpServerUrl: string;
tmpdir: string;
instructions: ResolvedInstructions;