From bb1da1ec51ef07dcbc3d86a2cad62af08da517e5 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 24 Dec 2025 02:11:17 +0100 Subject: [PATCH] Add API keys in settings.json, fixes #295 --- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/docs/sdk.md | 10 +++- packages/coding-agent/src/core/sdk.ts | 54 ++++++++++++------- .../coding-agent/src/core/settings-manager.ts | 24 +++++++++ 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1c7c7f55..4ebf6a97 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- **API keys in settings.json**: Store API keys in `~/.pi/agent/settings.json` under the `apiKeys` field (e.g., `{ "apiKeys": { "anthropic": "sk-..." } }`). Settings keys take priority over environment variables. ([#295](https://github.com/badlogic/pi-mono/issues/295)) + ### Fixed - **Allow startup without API keys**: Interactive mode no longer throws when no API keys are configured. Users can now start the agent and use `/login` to authenticate. ([#288](https://github.com/badlogic/pi-mono/issues/288)) diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index 4ba3ceec..66d60fdb 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -258,10 +258,16 @@ If no model is provided: ### API Keys +API key resolution priority: +1. `settings.json` apiKeys (e.g., `{ "apiKeys": { "anthropic": "sk-..." } }`) +2. Custom providers from `models.json` +3. OAuth credentials from `oauth.json` +4. Environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) + ```typescript import { defaultGetApiKey, configureOAuthStorage } from "@mariozechner/pi-coding-agent"; -// Default: checks models.json, OAuth, environment variables +// Default: checks settings.json, models.json, OAuth, environment variables const { session } = await createAgentSession(); // Custom resolver @@ -271,7 +277,7 @@ const { session } = await createAgentSession({ if (model.provider === "anthropic") { return process.env.MY_ANTHROPIC_KEY; } - // Fall back to default + // Fall back to default (pass settingsManager for settings.json lookup) return defaultGetApiKey()(model); }, }); diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 2bbb6e81..e2b3a812 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -328,10 +328,22 @@ export function discoverSlashCommands(cwd?: string, agentDir?: string): FileSlas /** * Create the default API key resolver. - * Checks custom providers (models.json), OAuth, and environment variables. + * Priority: settings.json apiKeys > custom providers (models.json) > OAuth > environment variables. */ -export function defaultGetApiKey(): (model: Model) => Promise { - return getApiKeyForModel; +export function defaultGetApiKey( + settingsManager?: SettingsManager, +): (model: Model) => Promise { + return async (model: Model) => { + // Check settings.json apiKeys first + if (settingsManager) { + const settingsKey = settingsManager.getApiKey(model.provider); + if (settingsKey) { + return settingsKey; + } + } + // Fall back to existing resolution (custom providers, OAuth, env vars) + return getApiKeyForModel(model); + }; } // System Prompt @@ -476,6 +488,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const sessionManager = options.sessionManager ?? SessionManager.create(cwd, agentDir); time("sessionManager"); + // Helper to check API key availability (settings first, then OAuth/env vars) + const hasApiKey = async (m: Model): Promise => { + const settingsKey = settingsManager.getApiKey(m.provider); + if (settingsKey) return true; + return !!(await getApiKeyForModel(m)); + }; + // Check if session has existing data to restore const existingSession = sessionManager.loadSession(); time("loadSession"); @@ -487,11 +506,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // If session has data, try to restore model from it if (!model && hasExistingSession && existingSession.model) { const restoredModel = findModel(existingSession.model.provider, existingSession.model.modelId); - if (restoredModel) { - const key = await getApiKeyForModel(restoredModel); - if (key) { - model = restoredModel; - } + if (restoredModel && (await hasApiKey(restoredModel))) { + model = restoredModel; } if (!model) { modelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`; @@ -504,21 +520,23 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const defaultModelId = settingsManager.getDefaultModel(); if (defaultProvider && defaultModelId) { const settingsModel = findModel(defaultProvider, defaultModelId); - if (settingsModel) { - const key = await getApiKeyForModel(settingsModel); - if (key) { - model = settingsModel; - } + if (settingsModel && (await hasApiKey(settingsModel))) { + model = settingsModel; } } } - // Fall back to first available + // Fall back to first available model with a valid API key if (!model) { - const available = await discoverAvailableModels(); + const allModels = discoverModels(agentDir); + for (const m of allModels) { + if (await hasApiKey(m)) { + model = m; + break; + } + } time("discoverAvailableModels"); - if (available.length > 0) { - model = available[0]; + if (model) { if (modelFallbackMessage) { modelFallbackMessage += `. Using ${model.provider}/${model.id}`; } @@ -545,7 +563,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} thinkingLevel = "off"; } - const getApiKey = options.getApiKey ?? defaultGetApiKey(); + const getApiKey = options.getApiKey ?? defaultGetApiKey(settingsManager); const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings()); time("discoverSkills"); diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index e56b2779..ff0e8019 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -47,6 +47,7 @@ export interface Settings { customTools?: string[]; // Array of custom tool file paths skills?: SkillsSettings; terminal?: TerminalSettings; + apiKeys?: Record; // provider -> API key (e.g., { "anthropic": "sk-..." }) } /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */ @@ -365,4 +366,27 @@ export class SettingsManager { this.globalSettings.terminal.showImages = show; this.save(); } + + getApiKey(provider: string): string | undefined { + return this.settings.apiKeys?.[provider]; + } + + setApiKey(provider: string, key: string): void { + if (!this.globalSettings.apiKeys) { + this.globalSettings.apiKeys = {}; + } + this.globalSettings.apiKeys[provider] = key; + this.save(); + } + + removeApiKey(provider: string): void { + if (this.globalSettings.apiKeys) { + delete this.globalSettings.apiKeys[provider]; + this.save(); + } + } + + getApiKeys(): Record { + return this.settings.apiKeys ?? {}; + } }