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:
+71
-94
@@ -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>
|
||||
);
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user