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:
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
>
|
||||
</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)]">></span>
|
||||
<input
|
||||
className="flex-1 border-0 bg-transparent px-0 py-2 text-xs outline-none placeholder:text-[var(--muted)]"
|
||||
|
||||
Reference in New Issue
Block a user