Add API keys in settings.json, fixes #295

This commit is contained in:
Mario Zechner 2025-12-24 02:11:17 +01:00
parent e234e8d18f
commit bb1da1ec51
4 changed files with 72 additions and 20 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased] ## [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 ### 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)) - **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))

View file

@ -258,10 +258,16 @@ If no model is provided:
### API Keys ### 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 ```typescript
import { defaultGetApiKey, configureOAuthStorage } from "@mariozechner/pi-coding-agent"; 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(); const { session } = await createAgentSession();
// Custom resolver // Custom resolver
@ -271,7 +277,7 @@ const { session } = await createAgentSession({
if (model.provider === "anthropic") { if (model.provider === "anthropic") {
return process.env.MY_ANTHROPIC_KEY; return process.env.MY_ANTHROPIC_KEY;
} }
// Fall back to default // Fall back to default (pass settingsManager for settings.json lookup)
return defaultGetApiKey()(model); return defaultGetApiKey()(model);
}, },
}); });

View file

@ -328,10 +328,22 @@ export function discoverSlashCommands(cwd?: string, agentDir?: string): FileSlas
/** /**
* Create the default API key resolver. * 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<any>) => Promise<string | undefined> { export function defaultGetApiKey(
return getApiKeyForModel; settingsManager?: SettingsManager,
): (model: Model<any>) => Promise<string | undefined> {
return async (model: Model<any>) => {
// 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 // System Prompt
@ -476,6 +488,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const sessionManager = options.sessionManager ?? SessionManager.create(cwd, agentDir); const sessionManager = options.sessionManager ?? SessionManager.create(cwd, agentDir);
time("sessionManager"); time("sessionManager");
// Helper to check API key availability (settings first, then OAuth/env vars)
const hasApiKey = async (m: Model<any>): Promise<boolean> => {
const settingsKey = settingsManager.getApiKey(m.provider);
if (settingsKey) return true;
return !!(await getApiKeyForModel(m));
};
// Check if session has existing data to restore // Check if session has existing data to restore
const existingSession = sessionManager.loadSession(); const existingSession = sessionManager.loadSession();
time("loadSession"); time("loadSession");
@ -487,11 +506,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// If session has data, try to restore model from it // If session has data, try to restore model from it
if (!model && hasExistingSession && existingSession.model) { if (!model && hasExistingSession && existingSession.model) {
const restoredModel = findModel(existingSession.model.provider, existingSession.model.modelId); const restoredModel = findModel(existingSession.model.provider, existingSession.model.modelId);
if (restoredModel) { if (restoredModel && (await hasApiKey(restoredModel))) {
const key = await getApiKeyForModel(restoredModel); model = restoredModel;
if (key) {
model = restoredModel;
}
} }
if (!model) { if (!model) {
modelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`; 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(); const defaultModelId = settingsManager.getDefaultModel();
if (defaultProvider && defaultModelId) { if (defaultProvider && defaultModelId) {
const settingsModel = findModel(defaultProvider, defaultModelId); const settingsModel = findModel(defaultProvider, defaultModelId);
if (settingsModel) { if (settingsModel && (await hasApiKey(settingsModel))) {
const key = await getApiKeyForModel(settingsModel); model = settingsModel;
if (key) {
model = settingsModel;
}
} }
} }
} }
// Fall back to first available // Fall back to first available model with a valid API key
if (!model) { if (!model) {
const available = await discoverAvailableModels(); const allModels = discoverModels(agentDir);
for (const m of allModels) {
if (await hasApiKey(m)) {
model = m;
break;
}
}
time("discoverAvailableModels"); time("discoverAvailableModels");
if (available.length > 0) { if (model) {
model = available[0];
if (modelFallbackMessage) { if (modelFallbackMessage) {
modelFallbackMessage += `. Using ${model.provider}/${model.id}`; modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
} }
@ -545,7 +563,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
thinkingLevel = "off"; thinkingLevel = "off";
} }
const getApiKey = options.getApiKey ?? defaultGetApiKey(); const getApiKey = options.getApiKey ?? defaultGetApiKey(settingsManager);
const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings()); const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
time("discoverSkills"); time("discoverSkills");

View file

@ -47,6 +47,7 @@ export interface Settings {
customTools?: string[]; // Array of custom tool file paths customTools?: string[]; // Array of custom tool file paths
skills?: SkillsSettings; skills?: SkillsSettings;
terminal?: TerminalSettings; terminal?: TerminalSettings;
apiKeys?: Record<string, string>; // provider -> API key (e.g., { "anthropic": "sk-..." })
} }
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */ /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
@ -365,4 +366,27 @@ export class SettingsManager {
this.globalSettings.terminal.showImages = show; this.globalSettings.terminal.showImages = show;
this.save(); 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<string, string> {
return this.settings.apiKeys ?? {};
}
} }