From bbda005ee940a322cdb153108d58e036dd999363 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Tue, 9 Dec 2025 11:53:19 -0500 Subject: [PATCH] remove any default mapping for models --- agents/opencode.ts | 65 ++++++++++++++++++++++++++++++++++------------ agents/shared.ts | 1 + external.ts | 2 +- fixtures/basic.txt | 2 +- main.ts | 51 +++++++++++++++++++++++++++++++----- 5 files changed, 96 insertions(+), 25 deletions(-) diff --git a/agents/opencode.ts b/agents/opencode.ts index 22026ac..58ecc84 100644 --- a/agents/opencode.ts +++ b/agents/opencode.ts @@ -6,7 +6,6 @@ import { addInstructions } from "./instructions.ts"; import { agent, type ConfigureMcpServersParams, - createAgentEnv, installFromNpmTarball, setupProcessAgentEnv, } from "./shared.ts"; @@ -211,34 +210,54 @@ export const opencode = agent({ installDependencies: true, }); }, - run: async ({ payload, apiKey, mcpServers, cliPath }) => { + run: async ({ payload, apiKey, apiKeys, mcpServers, cliPath }) => { // 1. configure home/config directory const tempHome = process.env.PULLFROG_TEMP_DIR!; const configDir = join(tempHome, ".config", "opencode"); mkdirSync(configDir, { recursive: true }); - // 2. initialize MCP servers and sandbox + if (!apiKey) { + throw new Error("at least one API key is required for opencode agent"); + } + + // 2. build env vars from all available API keys (map to OpenCode's expected env var names) + const apiKeyEnv: Record = {}; + const envVarMap: Record = { + anthropic_api_key: "ANTHROPIC_API_KEY", + openai_api_key: "OPENAI_API_KEY", + google_api_key: "GOOGLE_GENERATIVE_AI_API_KEY", + gemini_api_key: "GOOGLE_GENERATIVE_AI_API_KEY", + }; + for (const [inputKey, value] of Object.entries(apiKeys || {})) { + const envVarName = envVarMap[inputKey] || inputKey.toUpperCase(); + apiKeyEnv[envVarName] = value; + } + + // 3. initialize MCP servers and sandbox configureOpenCodeMcpServers({ mcpServers }); configureOpenCodeSandbox({ sandbox: payload.sandbox ?? false }); - if (!apiKey) { - throw new Error("anthropic_api_key is required for opencode agent"); - } - - // 3. prepare prompt and args + // 4. prepare prompt and args const prompt = addInstructions(payload); - const args = ["run", "--format", "json", "-m", "anthropic/claude-sonnet-4-20250514", prompt]; + const args = ["run", "--format", "json", prompt]; if (payload.sandbox) { log.info("🔒 sandbox mode enabled: restricting to read-only operations"); } - // 4. set up environment + // 6. set up environment const packageDir = join(cliPath, "..", ".."); - setupProcessAgentEnv({ ANTHROPIC_API_KEY: apiKey, HOME: tempHome }); - const env = createAgentEnv({ ANTHROPIC_API_KEY: apiKey, HOME: tempHome }); + setupProcessAgentEnv({ HOME: tempHome, ...apiKeyEnv }); + // don't pass GITHUB_TOKEN to opencode - it may try to use it incorrectly + const env: Record = { + PATH: process.env.PATH || "", + HOME: tempHome, + LOG_LEVEL: process.env.LOG_LEVEL || "", + NODE_ENV: process.env.NODE_ENV || "", + ...apiKeyEnv, + }; - // 5. spawn and stream JSON output + // 7. spawn and stream JSON output let output = ""; const result = await spawn({ cmd: cliPath, @@ -278,7 +297,7 @@ export const opencode = agent({ }, }); - // 6. log tokens if they weren't logged yet (fallback if result event wasn't emitted) + // 8. log tokens if they weren't logged yet (fallback if result event wasn't emitted) if (!tokensLogged && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0)) { const totalTokens = accumulatedTokens.input + accumulatedTokens.output; await log.summaryTable([ @@ -291,11 +310,23 @@ export const opencode = agent({ ]); } - // 7. return result + // 9. return result + if (result.exitCode !== 0) { + const errorMessage = + result.stderr || result.stdout || "Unknown error - no output from OpenCode CLI"; + log.error(`OpenCode CLI exited with code ${result.exitCode}: ${errorMessage}`); + log.debug(`OpenCode stdout: ${result.stdout?.substring(0, 500)}`); + log.debug(`OpenCode stderr: ${result.stderr?.substring(0, 500)}`); + return { + success: false, + output: finalOutput || output, + error: errorMessage, + }; + } + return { - success: result.exitCode === 0, + success: true, output: finalOutput || output, - error: result.exitCode !== 0 ? result.stderr : undefined, }; }, }); diff --git a/agents/shared.ts b/agents/shared.ts index 26e180a..03de7c0 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -25,6 +25,7 @@ export interface AgentResult { */ export interface AgentConfig { apiKey: string; + apiKeys?: Record; // all available keys for this agent payload: Payload; mcpServers: Record; cliPath: string; diff --git a/external.ts b/external.ts index f089a5f..a2b33c2 100644 --- a/external.ts +++ b/external.ts @@ -40,7 +40,7 @@ export const agentsManifest = { }, opencode: { displayName: "OpenCode", - apiKeyNames: ["anthropic_api_key"], + apiKeyNames: ["anthropic_api_key", "openai_api_key", "google_api_key", "gemini_api_key"], url: "https://opencode.ai", }, } as const satisfies Record; diff --git a/fixtures/basic.txt b/fixtures/basic.txt index b1f5d4a..a13899a 100644 --- a/fixtures/basic.txt +++ b/fixtures/basic.txt @@ -1 +1 @@ -say hello \ No newline at end of file +Use the debug_shell_command MCP tool to run `git status` and show me the output \ No newline at end of file diff --git a/main.ts b/main.ts index b07f147..e5e4672 100644 --- a/main.ts +++ b/main.ts @@ -185,13 +185,17 @@ interface MainContext { mcpServers: ReturnType; cliPath: string; apiKey: string; + apiKeys: Record; } async function initializeContext( inputs: Inputs, payload: Payload ): Promise< - Omit + Omit< + MainContext, + "mcpServerUrl" | "mcpServerClose" | "mcpServers" | "cliPath" | "apiKey" | "apiKeys" + > > { log.info(`🐸 Running pullfrog/action@${packageJson.version}...`); Inputs.assert(inputs); @@ -240,8 +244,31 @@ async function resolveAgent( if (!agent) { throw new Error(`invalid agent name: ${agentName}`); } - log.info(`Selected configured agent: ${agentName}`); - return { agentName, agent }; + + // if explicitly configured (via override or payload), respect it even without matching keys + // this allows users to force an agent selection (will fail later with clear error if no keys) + const isExplicitOverride = agentOverride !== undefined || payload.agent !== null; + + if (isExplicitOverride) { + log.info(`Selected configured agent: ${agentName}`); + return { agentName, agent }; + } + + // for repo-level defaults, check if agent has matching keys before selecting + const hasMatchingKey = agent.apiKeyNames.some((inputKey) => inputs[inputKey]); + if (!hasMatchingKey) { + log.warning( + `Repo default agent ${agentName} has no matching API keys. Available agents: ${ + getAvailableAgents(inputs) + .map((a) => a.name) + .join(", ") || "none" + }` + ); + // fall through to auto-selection for repo defaults + } else { + log.info(`Selected configured agent: ${agentName}`); + return { agentName, agent }; + } } const availableAgents = getAvailableAgents(inputs); @@ -330,8 +357,16 @@ async function installAgentCli(ctx: MainContext): Promise { } async function validateApiKey(ctx: MainContext): Promise { - const matchingInputKey = ctx.agent.apiKeyNames.find((inputKey) => ctx.inputs[inputKey]); - if (!matchingInputKey) { + // collect all matching API keys for this agent + const apiKeys: Record = {}; + for (const inputKey of ctx.agent.apiKeyNames) { + const value = ctx.inputs[inputKey]; + if (value) { + apiKeys[inputKey] = value; + } + } + + if (Object.keys(apiKeys).length === 0) { await throwMissingApiKeyError({ agent: ctx.agent, repoContext: ctx.repoContext, @@ -339,7 +374,10 @@ async function validateApiKey(ctx: MainContext): Promise { // unreachable - throwMissingApiKeyError always throws return; } - ctx.apiKey = ctx.inputs[matchingInputKey]!; + + // keep apiKey for backward compat (first available key) + ctx.apiKey = Object.values(apiKeys)[0]; + ctx.apiKeys = apiKeys; } async function runAgent(ctx: MainContext): Promise { @@ -354,6 +392,7 @@ async function runAgent(ctx: MainContext): Promise { payload: ctx.payload, mcpServers: ctx.mcpServers, apiKey: ctx.apiKey, + apiKeys: ctx.apiKeys, cliPath: ctx.cliPath, }); }