diff --git a/src/agent/subagents/pi-run.ts b/src/agent/subagents/pi-run.ts index 890fe04..458bc29 100644 --- a/src/agent/subagents/pi-run.ts +++ b/src/agent/subagents/pi-run.ts @@ -1,6 +1,6 @@ import { Agent, type AgentEvent, type AgentMessage, type AgentTool } from "@earendil-works/pi-agent-core"; import type { AssistantMessage, ToolResultMessage, Usage } from "@earendil-works/pi-ai"; -import { openRouterApiKey, piEngineFor, type PiEngine } from "../models.ts"; +import { piEngineFor, type PiEngine } from "../models.ts"; import type { ModelRole } from "../../../config.ts"; /** @@ -65,7 +65,6 @@ export async function runSubagent(tools: AgentTool[], opts: SubagentRu messages: opts.messages ?? [], }, streamFn: engine.streamFn, - getApiKey: () => openRouterApiKey(), shouldStopAfterTurn: (context) => { turns++; return turns >= opts.maxTurns || opts.shouldStop?.(context.message, context.toolResults) === true; diff --git a/src/agent/teacher-agent.ts b/src/agent/teacher-agent.ts index 335e2d3..853e400 100644 --- a/src/agent/teacher-agent.ts +++ b/src/agent/teacher-agent.ts @@ -2,7 +2,7 @@ import { Agent } from "@earendil-works/pi-agent-core"; import type { AgentEvent, AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; import type { Usage } from "@earendil-works/pi-ai"; import { PATHS } from "../../config.ts"; -import { piEngineFor, openRouterApiKey, type PiEngine } from "./models.ts"; +import { piEngineFor, type PiEngine } from "./models.ts"; import { effectivePromptSegments } from "./prompt-segments.ts"; import { askUserQuestionTool, @@ -158,7 +158,6 @@ function baseAgentOptions(kind: AgentKind, opts: AgentBuildOptions, tools: Agent messages: opts.messages ?? [], }, streamFn: engine.streamFn, - getApiKey: () => openRouterApiKey(), shouldStopAfterTurn: stopper, }); if (opts.onUsage) { diff --git a/tests/api-key-hook.test.ts b/tests/api-key-hook.test.ts new file mode 100644 index 0000000..181961c --- /dev/null +++ b/tests/api-key-hook.test.ts @@ -0,0 +1,55 @@ +/** + * The getApiKey-hook fix (owner, 2026-09-26): both Agent constructions + * (baseAgentOptions in teacher-agent.ts, runSubagent's loop in + * subagents/pi-run.ts) used to pass `getApiKey: () => openRouterApiKey()` — + * and pi-agent-core resolves + * `(config.getApiKey ? await config.getApiKey(provider) : undefined) || config.apiKey` + * (agent-loop.js:191), so a provided hook OVERRIDES pi-ai's native + * per-provider env resolution. The hook ignored its provider arg, so every + * non-OpenRouter provider was sent the OpenRouter key and 401'd (owner repro: + * openai:gpt-5.6-luna, anthropic:claude-haiku-4-5 direct). + * + * The fix is DELETION: with no hook, pi-ai resolves keys natively from the + * environment per request — the architecture's stated intent (models.ts: + * "the keys seam reflects file-saved keys into env"). This file pins that + * the deletion holds: the built agents must expose NO getApiKey hook, so + * native resolution runs for every provider. + * + * runSubagent's Agent is internal to its promise (no exposed seam); its + * hook was deleted in the same commit and its behavior is covered by the + * sub-agent suite's faux engines. + */ + +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { createProberAgent, createTeacherAgent, type TeachingAgent } from "../src/agent/teacher-agent.ts"; +import type { ProberToolContext } from "../src/agent/tools.ts"; +import { jobStore } from "../src/store/jobs.ts"; +import { chatStore } from "../src/store/chats.ts"; +import { createFauxCore } from "@earendil-works/pi-ai"; +import type { PiEngine } from "../src/agent/models.ts"; + +/** A scripted pi engine — the agent builds need an injected engine since + * BYOK removed the factory pins (owner, 2026-09-22). */ +function fauxEngine(): PiEngine { + const faux = createFauxCore({}); + return { model: faux.getModel(), streamFn: faux.streamSimple }; +} + +describe("the getApiKey-hook fix (native per-provider key resolution)", () => { + it("teacher and prober builds expose NO getApiKey hook — pi-ai resolves keys natively", () => { + const chatId = chatStore.createChat("getApiKey pin").id; + const ctx: ProberToolContext = { chatId, chatStore, deliver: () => {}, jobStore, spawn: () => {} }; + const builds: TeachingAgent[] = [ + createTeacherAgent(undefined, { engine: fauxEngine() }), + createProberAgent(ctx, { engine: fauxEngine() }), + ]; + for (const agent of builds) { + assert.equal( + (agent as unknown as { getApiKey?: unknown }).getApiKey, + undefined, + "the OpenRouter-pinning hook must stay deleted — a set hook overrides pi-ai's native per-provider env resolution for EVERY provider", + ); + } + }); +}); \ No newline at end of file