mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 18:04:41 +00:00
Add API keys in settings.json, fixes #295
This commit is contained in:
parent
e234e8d18f
commit
bb1da1ec51
4 changed files with 72 additions and 20 deletions
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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 ?? {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue