Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

158 lines
5.0 KiB
TypeScript

/**
* Server-only Pi wiring for the example.
*
* Owns the process-singleton `createPiNodeClient` (the in-process SDK host) and
* the env-seeded model resolution. Imported only from route
* handlers — never from a client component — so the Pi SDK and its file I/O stay
* on the server.
*
* Model resolution mirrors what Pi's own `createAgentSession` does: an explicit
* PI_PROVIDER + PI_MODEL_ID wins, otherwise we fall back to Pi's configured
* default (`settings.json`'s `defaultProvider`/`defaultModel`, read via
* `SettingsManager`). The resolved `Model` is handed to the supervisor as the
* default for every new session, and powers the model-selector catalog and the
* readiness banner. So a user who is simply authenticated with `pi` (and has a
* default model picked) needs no env at all.
*
* `PI_CODING_AGENT_DIR` is Pi's own env var for the agent config dir
* (~/.pi/agent by default); when set, every Pi config source below points at it.
*/
import {
AuthStorage,
ModelRegistry,
SettingsManager,
} from "@earendil-works/pi-coding-agent";
import {
createPiNodeClient,
type PiModelInfo,
type PiRuntimeReadiness,
type PiThinkingLevel,
} from "@assistant-ui/react-pi/node";
import { modelKey } from "./model-key";
const env = (key: string): string | undefined => {
const value = process.env[key];
return value && value.trim() ? value.trim() : undefined;
};
const workspacePath = env("PI_WORKSPACE_PATH") ?? process.cwd();
const agentDir = env("PI_CODING_AGENT_DIR");
const authStorage = AuthStorage.create(
agentDir ? `${agentDir}/auth.json` : undefined,
);
const modelRegistry = ModelRegistry.create(authStorage);
const settingsManager = SettingsManager.create(workspacePath, agentDir);
// Env wins; otherwise fall back to Pi's configured default (settings.json).
const provider = env("PI_PROVIDER") ?? settingsManager.getDefaultProvider();
const modelId = env("PI_MODEL_ID") ?? settingsManager.getDefaultModel();
const modelSource: "env" | "pi-default" =
env("PI_PROVIDER") && env("PI_MODEL_ID") ? "env" : "pi-default";
const seededModel =
provider && modelId ? modelRegistry.find(provider, modelId) : undefined;
export const piClient = createPiNodeClient({
workspacePath,
...(agentDir ? { agentDir } : {}),
...(seededModel ? { model: seededModel } : {}),
});
export type PiModelOption = {
id: string;
name: string;
description?: string;
provider: string;
modelId: string;
/** Thinking levels the model supports; omitted for non-reasoning models. */
efforts?: { id: string; name: string }[];
};
/** Display names for Pi's thinking levels (the UI-facing half of `efforts`;
* which levels a model supports comes from `PiModelInfo`). */
const THINKING_LEVEL_NAMES: Record<PiThinkingLevel, string> = {
off: "Off",
minimal: "Minimal",
low: "Low",
medium: "Medium",
high: "High",
xhigh: "xHigh",
};
const ALL_THINKING_LEVELS = Object.keys(
THINKING_LEVEL_NAMES,
) as PiThinkingLevel[];
const thinkingEfforts = (
model: PiModelInfo,
): { id: string; name: string }[] | undefined => {
if (!model.supportsThinking) return undefined;
// No `availableThinkingLevels` means the model declares no per-level map;
// every level stays selectable (provider defaults apply).
return (model.availableThinkingLevels ?? ALL_THINKING_LEVELS).map((id) => ({
id,
name: THINKING_LEVEL_NAMES[id],
}));
};
export type PiHandshake = {
workspacePath: string;
/** Models Pi can run here (auth-configured first, else the full catalog). */
models: PiModelOption[];
/** The env-seeded selection, shown as the selector's default. */
selectedModelId: string | undefined;
readiness: PiRuntimeReadiness;
};
const computeReadiness = (): PiRuntimeReadiness => {
if (!provider || !modelId) {
return {
state: "missing-model",
message:
"No default model configured. Pick one with `pi`, or set PI_PROVIDER and PI_MODEL_ID.",
};
}
if (!seededModel) {
return {
state: "unavailable-model",
selection: { provider, modelId },
message: `${provider}/${modelId} is not in Pi's model registry.`,
};
}
if (!modelRegistry.hasConfiguredAuth(seededModel)) {
return {
state: "missing-credentials",
provider,
message: `No credentials configured for ${provider}. Authenticate with \`pi\`, then restart the server.`,
};
}
return {
state: "ready",
selection: { provider, modelId },
source: modelSource,
};
};
export const getHandshake = async (): Promise<PiHandshake> => {
const catalog = await piClient.getAvailableModels();
return {
workspacePath,
models: catalog.map((model) => {
const efforts = thinkingEfforts(model);
return {
id: modelKey(model.provider, model.modelId),
name: model.name ?? model.modelId,
description: model.provider,
provider: model.provider,
modelId: model.modelId,
...(efforts ? { efforts } : {}),
};
}),
selectedModelId: seededModel
? modelKey(String(seededModel.provider), seededModel.id)
: undefined,
readiness: computeReadiness(),
};
};