fix(pi): inherit default model and api key for embedded agent

This commit is contained in:
William Valentin
2026-02-23 22:06:42 -08:00
parent e8204f5d42
commit 1dfa6ce2b4
3 changed files with 78 additions and 3 deletions
+21
View File
@@ -100,6 +100,27 @@ describe('PiEmbeddedBackend', () => {
}
});
it('surfaces agent state error when no assistant text is produced', async () => {
const mod = createModule(`
export class Agent {
constructor() {
this.state = { messages: [], error: undefined };
}
async prompt() {
this.state.error = "Missing API key for default provider";
}
}
`);
try {
const backend = new PiEmbeddedBackend({ module: mod.moduleUrl, timeoutMs: 2000 });
await expect(backend.process({ prompt: 'hello', history: [] }))
.rejects.toThrow('Missing API key for default provider');
} finally {
mod.cleanup();
}
});
it('throws when module cannot be loaded', async () => {
const backend = new PiEmbeddedBackend({ module: '/definitely/missing/pi-module.mjs', timeoutMs: 2000 });
await expect(backend.process({ prompt: 'hello', history: [] }))
+27 -3
View File
@@ -33,6 +33,8 @@ interface PiModelFactoryModuleLike extends PiModuleLike {
export interface PiEmbeddedBackendOptions {
timeoutMs?: number;
model?: string;
defaultModelSpec?: string;
getApiKey?: (provider: string) => string | undefined | Promise<string | undefined>;
systemPromptMode?: PiSystemPromptMode;
module?: string;
}
@@ -197,12 +199,16 @@ export class PiEmbeddedBackend implements ExternalBackend {
readonly name = 'pi_embedded' as const;
private readonly timeoutMs: number;
private readonly model?: string;
private readonly defaultModelSpec?: string;
private readonly getApiKey?: (provider: string) => string | undefined | Promise<string | undefined>;
private readonly systemPromptMode: PiSystemPromptMode;
private readonly moduleOverride?: string;
constructor(options: PiEmbeddedBackendOptions = {}) {
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
this.model = options.model;
this.defaultModelSpec = options.defaultModelSpec;
this.getApiKey = options.getApiKey;
this.systemPromptMode = options.systemPromptMode ?? 'hybrid';
this.moduleOverride = options.module;
}
@@ -294,14 +300,28 @@ export class PiEmbeddedBackend implements ExternalBackend {
input: ExternalBackendRequest,
moduleName: string,
): Promise<string> {
const agent = new AgentCtor();
const modelSpec = this.model ?? this.defaultModelSpec;
const model = modelSpec
? await this.resolvePiModel(modelSpec, moduleName)
: undefined;
const agentOptions: Record<string, unknown> = {};
if (model) {
agentOptions.initialState = { model };
}
if (this.getApiKey) {
agentOptions.getApiKey = this.getApiKey;
}
const agent = Object.keys(agentOptions).length > 0
? new AgentCtor(agentOptions)
: new AgentCtor();
if (!agent || typeof agent !== 'object') {
throw new Error('Pi Agent constructor returned an invalid object');
}
const agentObj = agent as PiSessionLike;
if (this.model) {
const model = await this.resolvePiModel(this.model, moduleName);
if (model) {
const setModel = agentObj.setModel;
if (typeof setModel === 'function') {
await Promise.resolve(setModel.call(agent, model));
@@ -348,6 +368,10 @@ export class PiEmbeddedBackend implements ExternalBackend {
}
}
const stateError = (state as PiSessionLike).error;
if (typeof stateError === 'string' && stateError.trim().length > 0) {
throw new Error(`Pi Agent runtime produced no assistant text: ${stateError}`);
}
throw new Error('Pi Agent runtime produced no assistant text');
}
+30
View File
@@ -44,11 +44,39 @@ import {
type ExternalBackendName,
} from '../backends/index.js';
function collectModelApiKeys(config: Config): Record<string, string> {
const keys: Record<string, string> = {};
const candidates = [
config.models.default,
config.models.fast,
config.models.complex,
config.models.local,
...Object.values(config.models.local_providers ?? {}),
];
for (const candidate of candidates) {
if (!candidate?.provider || !candidate.api_key) {
continue;
}
if (candidate.api_key.trim().length === 0) {
continue;
}
keys[candidate.provider] = candidate.api_key;
}
return keys;
}
function defaultPiModelSpec(config: Config): string {
return `${config.models.default.provider}:${config.models.default.model}`;
}
export function createConfiguredExternalBackends(config: Config): {
externalBackends: Partial<Record<ExternalBackendName, ExternalBackend>>;
defaultName?: ExternalBackendName;
} {
const backends: Partial<Record<ExternalBackendName, ExternalBackend>> = {};
const modelApiKeys = collectModelApiKeys(config);
if (config.backends.claude_code.enabled) {
backends.claude_code = new ClaudeCodeBackend(
@@ -82,6 +110,8 @@ export function createConfiguredExternalBackends(config: Config): {
backends.pi_embedded = new PiEmbeddedBackend({
timeoutMs: config.backends.pi_embedded.timeout_ms,
model: config.backends.pi_embedded.model,
defaultModelSpec: defaultPiModelSpec(config),
getApiKey: (provider) => modelApiKeys[provider],
systemPromptMode: config.backends.pi_embedded.system_prompt_mode,
module: config.backends.pi_embedded.module,
});