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]
### 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))

View file

@ -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);
},
});

View file

@ -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<any>) => Promise<string | undefined> {
return getApiKeyForModel;
export function defaultGetApiKey(
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
@ -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<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
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");

View file

@ -47,6 +47,7 @@ export interface Settings {
customTools?: string[]; // Array of custom tool file paths
skills?: SkillsSettings;
terminal?: TerminalSettings;
apiKeys?: Record<string, string>; // 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<string, string> {
return this.settings.apiKeys ?? {};
}
}