delete the getApiKey hook — pi-ai resolves keys natively per provider (owner, 2026-09-26)

baseAgentOptions (teacher-agent.ts) and runSubagent (subagents/pi-run.ts)
passed getApiKey: () => openRouterApiKey(). pi-agent-core resolves
(config.getApiKey ? await config.getApiKey(provider) : undefined) || config.apiKey
(agent-loop.js:191) — a provided hook OVERRIDES pi-ai's native per-provider
env resolution, and this one ignored its provider arg: every non-OpenRouter
provider was sent the OpenRouter key and 401'd (owner repro: openai and
anthropic direct). Deletion restores the architecture's stated intent —
the keys seam reflects file-saved keys into env and pi-ai reads them per
request. openRouterApiKey() export kept (now consumer-less; visual-maker/
pdf-reader read process.env.OPENROUTER_API_KEY directly for their own
OpenRouter-specific fetch paths — correct, untouched).

Pinned by tests/api-key-hook.test.ts: built teacher + prober expose NO
getApiKey hook. Gates: typecheck 0, 987/987, lint clean, build green.
This commit is contained in:
amosblomqvist
2026-09-25 21:24:43 +03:00
parent e89d4c2e23
commit b07cb313f0
3 changed files with 57 additions and 4 deletions
+1 -2
View File
@@ -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<any, any>[], opts: SubagentRu
messages: opts.messages ?? [],
},
streamFn: engine.streamFn,
getApiKey: () => openRouterApiKey(),
shouldStopAfterTurn: (context) => {
turns++;
return turns >= opts.maxTurns || opts.shouldStop?.(context.message, context.toolResults) === true;
+1 -2
View File
@@ -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) {
+55
View File
@@ -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",
);
}
});
});