single source of truth for pi config via ~/.pi/agent

auth.json stores api keys, models.json stores local providers directly.
no more copies, sync layers, or per-user config generation.
docker containers mount pi config read-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 18:42:30 +00:00
co-authored by Claude Opus 4.6
parent 500a70910e
commit 11845fbae5
12 changed files with 302 additions and 580 deletions
@@ -15,7 +15,7 @@ import {
import { useClient } from 'hooks/useClient';
type VersionInfo = { version: string | null; path: string | null; globalPath: string | null };
type StoredApiKeys = { keys: { env: string; value: string }[] };
type StoredApiKeys = { keys: { provider: string; value: string }[] };
type LocalProviderEntry = {
id: string;
name: string;
@@ -33,31 +33,21 @@ type ProbeResult = {
error?: string;
};
const PI_PROVIDERS: { key: string; env: string[] }[] = [
{ key: 'OpenAI', env: ['OPENAI_API_KEY'] },
{ key: 'Google', env: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'] },
{ key: 'OpenCode Zen', env: ['OPENCODE_API_KEY'] },
{ key: 'MiniMax', env: ['MINIMAX_API_KEY'] },
{ key: 'Groq', env: ['GROQ_API_KEY'] },
{ key: 'Mistral', env: ['MISTRAL_API_KEY'] },
{ key: 'xAI', env: ['XAI_API_KEY'] },
{ key: 'OpenRouter', env: ['OPENROUTER_API_KEY'] },
{ key: 'Hugging Face', env: ['HF_TOKEN'] },
{ key: 'GitHub Copilot', env: ['COPILOT_GITHUB_TOKEN'] },
{ key: 'Amazon Bedrock', env: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'] },
{ key: 'Google Vertex AI', env: ['GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION'] },
{ key: 'Azure OpenAI', env: ['AZURE_OPENAI_API_KEY', 'AZURE_OPENAI_BASE_URL'] },
{ key: 'Anthropic', env: ['ANTHROPIC_API_KEY'] },
const PI_PROVIDERS: { key: string; piId: string }[] = [
{ key: 'Anthropic', piId: 'anthropic' },
{ key: 'OpenAI', piId: 'openai' },
{ key: 'Google', piId: 'google' },
{ key: 'Groq', piId: 'groq' },
{ key: 'Mistral', piId: 'mistral' },
{ key: 'xAI', piId: 'xai' },
{ key: 'OpenRouter', piId: 'openrouter' },
{ key: 'MiniMax', piId: 'minimax' },
{ key: 'Hugging Face', piId: 'huggingface' },
{ key: 'Azure OpenAI', piId: 'azure-openai-responses' },
{ key: 'OpenCode Zen', piId: 'zai' },
{ key: 'Cerebras', piId: 'cerebras' },
];
const TEXT_FIELDS = new Set([
'AWS_REGION',
'GOOGLE_APPLICATION_CREDENTIALS',
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_CLOUD_LOCATION',
'AZURE_OPENAI_BASE_URL',
]);
type ProbeState =
| { step: 'url' }
| { step: 'probing' }
@@ -117,24 +107,25 @@ export const AIHarnessesSection = () => {
setAuthPassword('');
};
const storedEnvs = new Set(piMonoKeys?.keys.map((k: { env: string }) => k.env) ?? []);
const connectedProviders = PI_PROVIDERS.filter((p) => p.env.some((e) => storedEnvs.has(e)));
const unconnectedProviders = PI_PROVIDERS.filter((p) => !p.env.some((e) => storedEnvs.has(e)));
const storedKeys: StoredApiKeys['keys'] = piMonoKeys?.keys ?? [];
const storedPiIds = new Set(storedKeys.map((k) => k.provider));
const connectedProviders = PI_PROVIDERS.filter((p) => storedPiIds.has(p.piId));
const unconnectedProviders = PI_PROVIDERS.filter((p) => !storedPiIds.has(p.piId));
const getStoredMasked = (env: string) =>
piMonoKeys?.keys.find((k: { env: string; value: string }) => k.env === env)?.value ?? '';
const getStoredMasked = (piId: string) =>
storedKeys.find((k) => k.provider === piId)?.value ?? '';
const saveApiKey = async (env: string) => {
const value = keyInputs[env];
const saveApiKey = async (piId: string) => {
const value = keyInputs[piId];
if (value === undefined) return;
setSavingKey(env);
setSavingKey(piId);
try {
await client.put('/server-settings/pi-mono/api-keys', { key: env, value });
await client.put('/server-settings/pi-mono/api-keys', { provider: piId, value });
queryClient.invalidateQueries({ queryKey: ['PI_MONO_API_KEYS'] });
queryClient.invalidateQueries({ queryKey: ['PI_MODELS'] });
setKeyInputs((prev) => {
const next = { ...prev };
delete next[env];
delete next[piId];
return next;
});
} finally {
@@ -143,9 +134,7 @@ export const AIHarnessesSection = () => {
};
const disconnectProvider = async (provider: typeof PI_PROVIDERS[number]) => {
for (const env of provider.env) {
await client.put('/server-settings/pi-mono/api-keys', { key: env, value: '' });
}
await client.put('/server-settings/pi-mono/api-keys', { provider: provider.piId, value: '' });
queryClient.invalidateQueries({ queryKey: ['PI_MONO_API_KEYS'] });
queryClient.invalidateQueries({ queryKey: ['PI_MODELS'] });
if (editingProvider === provider.key) setEditingProvider(null);
@@ -433,69 +422,57 @@ export const AIHarnessesSection = () => {
<span className="text-sm font-medium text-duck-dark dark:text-foreground">Remote Providers</span>
<div className="mt-3 flex flex-col gap-2">
{connectedProviders.map((provider) => (
<div key={provider.key} className="flex flex-col gap-1">
<div className="flex items-center gap-2 mt-1">
<span className="text-duck-dark/60 font-medium text-[11px]">{provider.key}</span>
<button
type="button"
onClick={() => disconnectProvider(provider)}
className="p-0.5 rounded hover:bg-red-500/10 cursor-pointer transition-colors"
title="Disconnect provider"
>
<Trash2 className="h-3 w-3 text-duck-dark/30 hover:text-red-500" />
</button>
</div>
{provider.env.map((env) => (
<div key={env} className="flex items-center gap-2">
<label className="w-52 text-duck-dark/70 shrink-0 truncate" title={env}>{env}</label>
<Input
type={TEXT_FIELDS.has(env) ? 'text' : 'password'}
className="h-7 text-xs flex-1"
placeholder={getStoredMasked(env) || 'Not set'}
value={keyInputs[env] ?? ''}
onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [env]: ev.target.value }))}
/>
<button
type="button"
disabled={keyInputs[env] === undefined || savingKey === env}
onClick={() => saveApiKey(env)}
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
title="Save key"
>
<Save className="h-3.5 w-3.5 text-duck-teal" />
</button>
</div>
))}
<div key={provider.piId} className="flex items-center gap-2">
<label className="w-36 text-duck-dark/70 dark:text-foreground/70 shrink-0 truncate font-medium text-[11px]">{provider.key}</label>
<Input
type="password"
className="h-7 text-xs flex-1"
placeholder={getStoredMasked(provider.piId) || 'Not set'}
value={keyInputs[provider.piId] ?? ''}
onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [provider.piId]: ev.target.value }))}
/>
<button
type="button"
disabled={keyInputs[provider.piId] === undefined || savingKey === provider.piId}
onClick={() => saveApiKey(provider.piId)}
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
title="Save key"
>
<Save className="h-3.5 w-3.5 text-duck-teal" />
</button>
<button
type="button"
onClick={() => disconnectProvider(provider)}
className="shrink-0 p-0.5 rounded hover:bg-red-500/10 cursor-pointer transition-colors"
title="Disconnect provider"
>
<Trash2 className="h-3 w-3 text-duck-dark/30 hover:text-red-500" />
</button>
</div>
))}
{editingProvider && (() => {
const provider = PI_PROVIDERS.find((p) => p.key === editingProvider);
if (!provider || connectedProviders.includes(provider)) return null;
return (
<div key={provider.key} className="flex flex-col gap-1">
<span className="text-duck-dark/60 font-medium text-[11px] mt-1">{provider.key}</span>
{provider.env.map((env) => (
<div key={env} className="flex items-center gap-2">
<label className="w-52 text-duck-dark/70 shrink-0 truncate" title={env}>{env}</label>
<Input
type={TEXT_FIELDS.has(env) ? 'text' : 'password'}
className="h-7 text-xs flex-1"
placeholder="Not set"
value={keyInputs[env] ?? ''}
onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [env]: ev.target.value }))}
autoFocus={env === provider.env[0]}
/>
<button
type="button"
disabled={keyInputs[env] === undefined || savingKey === env}
onClick={() => saveApiKey(env)}
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
title="Save key"
>
<Save className="h-3.5 w-3.5 text-duck-teal" />
</button>
</div>
))}
<div key={provider.piId} className="flex items-center gap-2">
<label className="w-36 text-duck-dark/70 dark:text-foreground/70 shrink-0 truncate font-medium text-[11px]">{provider.key}</label>
<Input
type="password"
className="h-7 text-xs flex-1"
placeholder="Enter API key"
value={keyInputs[provider.piId] ?? ''}
onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [provider.piId]: ev.target.value }))}
autoFocus
/>
<button
type="button"
disabled={keyInputs[provider.piId] === undefined || savingKey === provider.piId}
onClick={() => saveApiKey(provider.piId)}
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
title="Save key"
>
<Save className="h-3.5 w-3.5 text-duck-teal" />
</button>
</div>
);
})()}