Migrate to Pi Coding Agent SDK for multi-provider LLM support

Replace the custom Pi API fetch client with the @earendil-works/pi-coding-agent
SDK, enabling support for multiple LLM providers (Anthropic, OpenAI, DeepSeek,
Google Gemini, xAI/ZAI) while maintaining backward compatibility.

Key changes:
- Add piSdk.ts with SDK session management and provider auto-detection
- Refactor client.ts to delegate to SDK adapter, keeping public API surface
- Update documentation to reflect multi-provider environment variables
- Add RPC contracts for LLM model selection and provider configuration
- Update agent runner to support provider-specific tools and parameters

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 00:17:26 -04:00
parent 506d092b2b
commit 0624026af3
30 changed files with 4326 additions and 354 deletions

View File

@@ -27,7 +27,7 @@ export const ui = {
rightPanel: "w-[300px] shrink-0 overflow-y-auto border-l border-[var(--border)] bg-[var(--surface)] p-5",
center: "min-w-0 flex-1 overflow-y-auto p-8 pl-10 pr-10",
spreadsheet: "w-full border-collapse bg-[var(--surface)] font-[var(--font-mono)] text-xs",
command: "flex items-center justify-between gap-6 border-t border-[var(--border)] bg-[var(--surface)] px-5",
command: "grid grid-cols-[1fr_2fr] items-center gap-6 border-t border-[var(--border)] bg-[var(--surface)] px-5",
overlay: "fixed inset-0 z-[500] grid place-items-center bg-black/20",
overlayWide: "fixed inset-0 z-[600] grid place-items-center bg-black/20",
toast: "fixed top-14 right-4 z-[1000] flex flex-col gap-2 max-w-[380px] pointer-events-none",

View File

@@ -68,6 +68,7 @@ type Toast = {
};
type SettingsPanel =
| "display"
| "llm"
| "data-sources"
| "agents"
| "export"
@@ -135,6 +136,9 @@ export function App() {
const [agentRunIdsByAgent, setAgentRunIdsByAgent] = useState<Record<string, string>>({});
const [agentTrace, setAgentTrace] = useState<AgentTraceStep[]>([]);
const [serverSettings, setServerSettings] = useState<ServerSettings | null>(null);
const [llmModels, setLlmModels] = useState<Array<{ provider: string; modelId: string; name: string; available: boolean }>>([]);
const [llmLoading, setLlmLoading] = useState(false);
const [selectedLlmModel, setSelectedLlmModel] = useState<string>("");
const [keybindingsDraft, setKeybindingsDraft] = useState<Record<string, string>>({});
const [workspaceSection, setWorkspaceSection] = useState<WorkspaceSection | null>(null);
const [workspaceSources, setWorkspaceSources] = useState<WorkspaceSource[]>([]);
@@ -292,7 +296,13 @@ export function App() {
setKeybindingsDraft(settings.keybindings ?? {});
}
const server = await rpc.call("settings.get", { scope: "server" });
if (server.ok) setServerSettings(server.data.settings as ServerSettings);
if (server.ok) {
const s = server.data.settings as ServerSettings;
setServerSettings(s);
if (s.llm?.provider && s.llm?.modelId) {
setSelectedLlmModel(`${s.llm.provider}/${s.llm.modelId}`);
}
}
}
void loadSettings();
}, []);
@@ -315,6 +325,35 @@ export function App() {
});
}, []);
// Load LLM models from backend
const loadLlmModels = useCallback(async () => {
setLlmLoading(true);
try {
const result = await rpc.call("model.list", undefined);
if (result.ok) setLlmModels(result.data.models);
} finally {
setLlmLoading(false);
}
}, []);
// Persist LLM model selection
const handleLlmModelChange = useCallback(async (providerModel: string) => {
setSelectedLlmModel(providerModel);
const [provider, ...rest] = providerModel.split("/");
const modelId = rest.join("/");
const llmSettings = { provider, modelId };
setServerSettings((prev) => {
const next = { ...(prev ?? { agentConfigs: {}, dataSources: {}, exportPipelines: {} }), llm: llmSettings } as ServerSettings;
return next;
});
await rpc.call("settings.update", {
scope: "server",
changes: { llm: llmSettings },
});
// Also tell the backend model RPC so it takes effect immediately
await rpc.call("model.set", { provider, modelId });
}, []);
useEffect(() => {
let cancelled = false;
async function load() {
@@ -808,13 +847,20 @@ export function App() {
open={settingsOpen}
onClose={() => setSettingsOpen(false)}
panel={settingsPanel}
onPanelChange={setSettingsPanel}
onPanelChange={(p) => {
setSettingsPanel(p);
if (p === "llm" && llmModels.length === 0) void loadLlmModels();
}}
theme={theme}
onThemeChange={handleThemeChange}
density={density}
onDensityChange={handleDensityChange}
serverSettings={serverSettings}
keybindings={keybindingsDraft}
llmModels={llmModels}
llmLoading={llmLoading}
selectedLlmModel={selectedLlmModel}
onLlmModelChange={handleLlmModelChange}
onServerSettingsChange={async (changes) => {
const next = { ...(serverSettings ?? { agentConfigs: {}, dataSources: {}, exportPipelines: {} }), ...changes } as ServerSettings;
setServerSettings(next);
@@ -3012,6 +3058,10 @@ function SettingsOverlay({
onDensityChange,
serverSettings,
keybindings,
llmModels,
llmLoading,
selectedLlmModel,
onLlmModelChange,
onServerSettingsChange,
onKeybindingsChange,
}: {
@@ -3025,12 +3075,17 @@ function SettingsOverlay({
onDensityChange: (d: Density) => void;
serverSettings: ServerSettings | null;
keybindings: Record<string, string>;
llmModels: Array<{ provider: string; modelId: string; name: string; available: boolean }>;
llmLoading: boolean;
selectedLlmModel: string;
onLlmModelChange: (providerModel: string) => void | Promise<void>;
onServerSettingsChange: (changes: Partial<ServerSettings>) => void | Promise<void>;
onKeybindingsChange: (keybindings: Record<string, string>) => void | Promise<void>;
}) {
if (!open) return null;
const panels: Array<{ key: SettingsPanel; label: string }> = [
{ key: "display", label: "Display" },
{ key: "llm", label: "LLM Provider" },
{ key: "data-sources", label: "Data Sources" },
{ key: "agents", label: "Agents" },
{ key: "export", label: "Export" },
@@ -3106,6 +3161,88 @@ function SettingsOverlay({
</SettingsRow>
</>
)}
{panel === "llm" && (
<>
<p className="text-xs text-[var(--muted)] mb-4">
Select the LLM provider and model used for all agent operations.
Models marked as available have a configured API key.
</p>
{llmLoading && (
<p className="text-xs text-[var(--muted)]">Loading models</p>
)}
{!llmLoading && llmModels.length === 0 && (
<div className="border border-[var(--border)] p-4">
<EmptyState
icon="!"
title="No models detected"
desc="Set ANTHROPIC_API_KEY, OPENAI_API_KEY, ZAI_API_KEY, or another provider key in your environment and restart the app."
/>
</div>
)}
{!llmLoading && llmModels.length > 0 && (
<div className="border border-[var(--border)] bg-[var(--border)]">
{(() => {
// Group models by provider
const groups: Record<string, typeof llmModels> = {};
for (const m of llmModels) {
(groups[m.provider] ??= []).push(m);
}
return Object.entries(groups).map(([provider, models]) => (
<div key={provider}>
<div className="bg-[var(--surface)] px-3 py-1.5 text-[10px] font-[var(--font-mono)] uppercase tracking-wider text-[var(--muted)] border-b border-[var(--border)]">
{provider}
</div>
{models.map((m) => {
const key = `${m.provider}/${m.modelId}`;
const selected = selectedLlmModel === key;
return (
<button
key={key}
className={cx(
"w-full grid grid-cols-[1fr_auto] items-center gap-3 bg-[var(--surface)] px-3 py-2.5 text-left border-b border-[var(--border)] hover:bg-[var(--bg)] transition-colors",
selected && "bg-[var(--bg)] outline outline-1 outline-[var(--accent)] outline-offset-[-1px]",
)}
disabled={!m.available}
onClick={() => void onLlmModelChange(key)}
>
<span className={cx("text-sm", !m.available && "text-[var(--muted)]")}>
{m.name}
</span>
<span className={cx(
"font-[var(--font-mono)] text-[10px] tracking-wider",
m.available
? selected
? "text-[var(--accent)]"
: "text-[var(--muted)]"
: "text-[var(--muted)] opacity-60",
)}>
{m.available
? selected
? "● ACTIVE"
: "AVAILABLE"
: "NO KEY"
}
</span>
</button>
);
})}
</div>
));
})()}
</div>
)}
{serverSettings?.llm && (
<div className="mt-4 border border-[var(--border)] p-3">
<p className="font-[var(--font-mono)] text-[10px] uppercase tracking-wider text-[var(--muted)] mb-1">Current Selection</p>
<p className="text-sm">
<span className="text-[var(--accent)]">{serverSettings.llm.provider}</span>
<span className="text-[var(--muted)]"> / </span>
<span className="font-[var(--font-mono)] text-xs">{serverSettings.llm.modelId}</span>
</p>
</div>
)}
</>
)}
{panel === "data-sources" && (
<>
{Object.entries(serverSettings?.dataSources ?? {}).map(([key, enabled]) => (
@@ -3841,7 +3978,7 @@ function CommandBar({
&gt;
</button>
</div>
<div className="flex min-w-[360px] items-center gap-0 border border-[var(--border)]">
<div className="flex w-full items-center gap-0 border border-[var(--border)]">
<span className="px-3 py-2 text-[var(--muted)]">&gt;</span>
<input
className="flex-1 border-0 bg-transparent px-0 py-2 text-xs outline-none placeholder:text-[var(--muted)]"