Finish backlog cleanup for Pi integration
This commit is contained in:
32
src/cli.ts
32
src/cli.ts
@@ -19,6 +19,7 @@ import { launchPiChat } from "./pi/launch.js";
|
||||
import { CORE_PACKAGE_SOURCES, getOptionalPackagePresetSources, listOptionalPackagePresets } from "./pi/package-presets.js";
|
||||
import { normalizeFeynmanSettings, normalizeThinkingLevel, parseModelSpec } from "./pi/settings.js";
|
||||
import { applyFeynmanPackageManagerEnv } from "./pi/runtime.js";
|
||||
import { getConfiguredServiceTier, normalizeServiceTier, setConfiguredServiceTier } from "./model/service-tier.js";
|
||||
import {
|
||||
authenticateModelProvider,
|
||||
getCurrentModelSpec,
|
||||
@@ -151,6 +152,29 @@ async function handleModelCommand(subcommand: string | undefined, args: string[]
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === "tier") {
|
||||
const requested = args[0];
|
||||
if (!requested) {
|
||||
console.log(getConfiguredServiceTier(feynmanSettingsPath) ?? "not set");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requested === "unset" || requested === "clear" || requested === "off") {
|
||||
setConfiguredServiceTier(feynmanSettingsPath, undefined);
|
||||
console.log("Cleared service tier override");
|
||||
return;
|
||||
}
|
||||
|
||||
const tier = normalizeServiceTier(requested);
|
||||
if (!tier) {
|
||||
throw new Error("Usage: feynman model tier <auto|default|flex|priority|standard_only|unset>");
|
||||
}
|
||||
|
||||
setConfiguredServiceTier(feynmanSettingsPath, tier);
|
||||
console.log(`Service tier set to ${tier}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown model command: ${subcommand}`);
|
||||
}
|
||||
|
||||
@@ -311,6 +335,7 @@ export async function main(): Promise<void> {
|
||||
model: { type: "string" },
|
||||
"new-session": { type: "boolean" },
|
||||
prompt: { type: "string" },
|
||||
"service-tier": { type: "string" },
|
||||
"session-dir": { type: "string" },
|
||||
"setup-preview": { type: "boolean" },
|
||||
thinking: { type: "string" },
|
||||
@@ -437,6 +462,13 @@ export async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
const explicitModelSpec = values.model ?? process.env.FEYNMAN_MODEL;
|
||||
const explicitServiceTier = normalizeServiceTier(values["service-tier"] ?? process.env.FEYNMAN_SERVICE_TIER);
|
||||
if ((values["service-tier"] ?? process.env.FEYNMAN_SERVICE_TIER) && !explicitServiceTier) {
|
||||
throw new Error("Unknown service tier. Use auto, default, flex, priority, or standard_only.");
|
||||
}
|
||||
if (explicitServiceTier) {
|
||||
process.env.FEYNMAN_SERVICE_TIER = explicitServiceTier;
|
||||
}
|
||||
if (explicitModelSpec) {
|
||||
const modelRegistry = createModelRegistry(feynmanAuthPath);
|
||||
const explicitModel = parseModelSpec(explicitModelSpec, modelRegistry);
|
||||
|
||||
65
src/model/service-tier.ts
Normal file
65
src/model/service-tier.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
export const FEYNMAN_SERVICE_TIERS = [
|
||||
"auto",
|
||||
"default",
|
||||
"flex",
|
||||
"priority",
|
||||
"standard_only",
|
||||
] as const;
|
||||
|
||||
export type FeynmanServiceTier = (typeof FEYNMAN_SERVICE_TIERS)[number];
|
||||
|
||||
const SERVICE_TIER_SET = new Set<string>(FEYNMAN_SERVICE_TIERS);
|
||||
const OPENAI_SERVICE_TIERS = new Set<FeynmanServiceTier>(["auto", "default", "flex", "priority"]);
|
||||
const ANTHROPIC_SERVICE_TIERS = new Set<FeynmanServiceTier>(["auto", "standard_only"]);
|
||||
|
||||
function readSettings(settingsPath: string): Record<string, unknown> {
|
||||
try {
|
||||
return JSON.parse(readFileSync(settingsPath, "utf8")) as Record<string, unknown>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeServiceTier(value: string | undefined): FeynmanServiceTier | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return SERVICE_TIER_SET.has(normalized) ? (normalized as FeynmanServiceTier) : undefined;
|
||||
}
|
||||
|
||||
export function getConfiguredServiceTier(settingsPath: string): FeynmanServiceTier | undefined {
|
||||
const settings = readSettings(settingsPath);
|
||||
return normalizeServiceTier(typeof settings.serviceTier === "string" ? settings.serviceTier : undefined);
|
||||
}
|
||||
|
||||
export function setConfiguredServiceTier(settingsPath: string, tier: FeynmanServiceTier | undefined): void {
|
||||
const settings = readSettings(settingsPath);
|
||||
if (tier) {
|
||||
settings.serviceTier = tier;
|
||||
} else {
|
||||
delete settings.serviceTier;
|
||||
}
|
||||
|
||||
mkdirSync(dirname(settingsPath), { recursive: true });
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
export function resolveActiveServiceTier(settingsPath: string): FeynmanServiceTier | undefined {
|
||||
return normalizeServiceTier(process.env.FEYNMAN_SERVICE_TIER) ?? getConfiguredServiceTier(settingsPath);
|
||||
}
|
||||
|
||||
export function resolveProviderServiceTier(
|
||||
provider: string | undefined,
|
||||
tier: FeynmanServiceTier | undefined,
|
||||
): FeynmanServiceTier | undefined {
|
||||
if (!provider || !tier) return undefined;
|
||||
if ((provider === "openai" || provider === "openai-codex") && OPENAI_SERVICE_TIERS.has(tier)) {
|
||||
return tier;
|
||||
}
|
||||
if (provider === "anthropic" && ANTHROPIC_SERVICE_TIERS.has(tier)) {
|
||||
return tier;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -2,12 +2,13 @@ import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
export type PiWebSearchProvider = "auto" | "perplexity" | "gemini";
|
||||
export type PiWebSearchProvider = "auto" | "perplexity" | "exa" | "gemini";
|
||||
|
||||
export type PiWebAccessConfig = Record<string, unknown> & {
|
||||
provider?: PiWebSearchProvider;
|
||||
searchProvider?: PiWebSearchProvider;
|
||||
perplexityApiKey?: string;
|
||||
exaApiKey?: string;
|
||||
geminiApiKey?: string;
|
||||
chromeProfile?: string;
|
||||
};
|
||||
@@ -17,6 +18,7 @@ export type PiWebAccessStatus = {
|
||||
searchProvider: PiWebSearchProvider;
|
||||
requestProvider: PiWebSearchProvider;
|
||||
perplexityConfigured: boolean;
|
||||
exaConfigured: boolean;
|
||||
geminiApiConfigured: boolean;
|
||||
chromeProfile?: string;
|
||||
routeLabel: string;
|
||||
@@ -28,7 +30,7 @@ export function getPiWebSearchConfigPath(home = process.env.HOME ?? homedir()):
|
||||
}
|
||||
|
||||
function normalizeProvider(value: unknown): PiWebSearchProvider | undefined {
|
||||
return value === "auto" || value === "perplexity" || value === "gemini" ? value : undefined;
|
||||
return value === "auto" || value === "perplexity" || value === "exa" || value === "gemini" ? value : undefined;
|
||||
}
|
||||
|
||||
function normalizeNonEmptyString(value: unknown): string | undefined {
|
||||
@@ -52,6 +54,8 @@ function formatRouteLabel(provider: PiWebSearchProvider): string {
|
||||
switch (provider) {
|
||||
case "perplexity":
|
||||
return "Perplexity";
|
||||
case "exa":
|
||||
return "Exa";
|
||||
case "gemini":
|
||||
return "Gemini";
|
||||
default:
|
||||
@@ -63,10 +67,12 @@ function formatRouteNote(provider: PiWebSearchProvider): string {
|
||||
switch (provider) {
|
||||
case "perplexity":
|
||||
return "Pi web-access will use Perplexity for search.";
|
||||
case "exa":
|
||||
return "Pi web-access will use Exa for search.";
|
||||
case "gemini":
|
||||
return "Pi web-access will use Gemini API or Gemini Browser.";
|
||||
default:
|
||||
return "Pi web-access will try Perplexity, then Gemini API, then Gemini Browser.";
|
||||
return "Pi web-access will try Perplexity, then Exa, then Gemini API, then Gemini Browser.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +83,7 @@ export function getPiWebAccessStatus(
|
||||
const searchProvider = normalizeProvider(config.searchProvider) ?? "auto";
|
||||
const requestProvider = normalizeProvider(config.provider) ?? searchProvider;
|
||||
const perplexityConfigured = Boolean(normalizeNonEmptyString(config.perplexityApiKey));
|
||||
const exaConfigured = Boolean(normalizeNonEmptyString(config.exaApiKey));
|
||||
const geminiApiConfigured = Boolean(normalizeNonEmptyString(config.geminiApiKey));
|
||||
const chromeProfile = normalizeNonEmptyString(config.chromeProfile);
|
||||
const effectiveProvider = searchProvider;
|
||||
@@ -86,6 +93,7 @@ export function getPiWebAccessStatus(
|
||||
searchProvider,
|
||||
requestProvider,
|
||||
perplexityConfigured,
|
||||
exaConfigured,
|
||||
geminiApiConfigured,
|
||||
chromeProfile,
|
||||
routeLabel: formatRouteLabel(effectiveProvider),
|
||||
@@ -101,6 +109,7 @@ export function formatPiWebAccessDoctorLines(
|
||||
` search route: ${status.routeLabel}`,
|
||||
` request route: ${status.requestProvider}`,
|
||||
` perplexity api: ${status.perplexityConfigured ? "configured" : "not configured"}`,
|
||||
` exa api: ${status.exaConfigured ? "configured" : "not configured"}`,
|
||||
` gemini api: ${status.geminiApiConfigured ? "configured" : "not configured"}`,
|
||||
` browser profile: ${status.chromeProfile ?? "default Chromium profile"}`,
|
||||
` config path: ${status.configPath}`,
|
||||
|
||||
@@ -7,6 +7,7 @@ export function printSearchStatus(): void {
|
||||
printInfo(`Search route: ${status.routeLabel}`);
|
||||
printInfo(`Request route: ${status.requestProvider}`);
|
||||
printInfo(`Perplexity API configured: ${status.perplexityConfigured ? "yes" : "no"}`);
|
||||
printInfo(`Exa API configured: ${status.exaConfigured ? "yes" : "no"}`);
|
||||
printInfo(`Gemini API configured: ${status.geminiApiConfigured ? "yes" : "no"}`);
|
||||
printInfo(`Browser profile: ${status.chromeProfile ?? "default Chromium profile"}`);
|
||||
printInfo(`Config path: ${status.configPath}`);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { printInfo, printPanel, printSection } from "../ui/terminal.js";
|
||||
import { getCurrentModelSpec } from "../model/commands.js";
|
||||
import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords } from "../model/catalog.js";
|
||||
import { createModelRegistry, getModelsJsonPath } from "../model/registry.js";
|
||||
import { getConfiguredServiceTier } from "../model/service-tier.js";
|
||||
|
||||
function findProvidersMissingApiKey(modelsJsonPath: string): string[] {
|
||||
try {
|
||||
@@ -105,6 +106,7 @@ export function runStatus(options: DoctorOptions): void {
|
||||
printInfo(`Recommended model: ${snapshot.recommendedModel ?? "not available"}`);
|
||||
printInfo(`alphaXiv: ${snapshot.alphaLoggedIn ? snapshot.alphaUser ?? "configured" : "not configured"}`);
|
||||
printInfo(`Web access: pi-web-access (${snapshot.webRouteLabel})`);
|
||||
printInfo(`Service tier: ${getConfiguredServiceTier(options.settingsPath) ?? "not set"}`);
|
||||
printInfo(`Preview: ${snapshot.previewConfigured ? "configured" : "not configured"}`);
|
||||
|
||||
printSection("Paths");
|
||||
@@ -165,6 +167,7 @@ export function runDoctor(options: DoctorOptions): void {
|
||||
console.log(`default model valid: ${modelStatus.modelValid ? "yes" : "no"}`);
|
||||
console.log(`authenticated providers: ${modelStatus.authenticatedProviderCount}`);
|
||||
console.log(`authenticated models: ${modelStatus.authenticatedModelCount}`);
|
||||
console.log(`service tier: ${getConfiguredServiceTier(options.settingsPath) ?? "not set"}`);
|
||||
console.log(`recommended model: ${modelStatus.recommendedModel ?? "not available"}`);
|
||||
if (modelStatus.recommendedModelReason) {
|
||||
console.log(` why: ${modelStatus.recommendedModelReason}`);
|
||||
|
||||
Reference in New Issue
Block a user