import { existsSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import type { Api, Model, SimpleStreamOptions, } from "@earendil-works/pi-ai"; import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent"; import { API_ID, DEFAULT_BASE_URL, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_TIMEOUT_MS, PROVIDER_ID, type XtalpiChatMessage, type XtalpiChatPayload, } from "./protocol.ts"; import { jsonActionResponseFormat } from "./json-action-protocol.ts"; import { readJsonFile as readCompatibleJsonFile } from "./json-file.ts"; import { buildProviderError } from "./errors.ts"; import { envInt } from "./config/legacy-runtime-env.ts"; import { resolveRuntimePolicy, RuntimePolicyConfigurationError, type RuntimePolicy, } from "./config/runtime-policy.ts"; export type ProviderRuntimeConfig = { baseUrl: string; apiKey: string; models: ProviderModelConfig[]; }; // A deferred reference satisfies Pi's provider schema without pretending that // an API key is configured. Pi resolves it only when the environment variable // exists, while /login credentials continue to take precedence. export const XTALPI_API_KEY_REFERENCE = "$XTALPI_PI_TOOLS_API_KEY"; const DEFAULT_MODELS: ProviderModelConfig[] = [ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash (Pi local tools)", api: API_ID, reasoning: false, input: ["text"], contextWindow: 262144, maxTokens: 32768, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro (Pi local tools)", api: API_ID, reasoning: false, input: ["text"], contextWindow: 262144, maxTokens: 32768, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, { id: "xtalpi-science-flagship", name: "晶泰科学旗舰模型 (Pi local tools)", api: API_ID, reasoning: false, input: ["text"], contextWindow: 262144, maxTokens: 32768, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, { id: "xtalpi-science-standard", name: "晶泰科学标准模型 (Pi local tools)", api: API_ID, reasoning: false, input: ["text"], contextWindow: 262144, maxTokens: 32768, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, ]; function isConfigObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function isPlaceholderKey(value: string | undefined): boolean { return !value || value.includes("YOUR_") || value.includes("REPLACE_") || value === "changeme"; } function isDeferredConfigValue(value: string | undefined): boolean { const normalized = String(value || "").trim(); return normalized.startsWith("$") || normalized.startsWith("!"); } function extensionAgentDir(): string { const file = fileURLToPath(import.meta.url); return resolve(dirname(file), "../.."); } function candidateAgentDirs(): string[] { const home = process.env.HOME || ""; return [ process.env.PI_AGENT_DIR || "", home ? join(home, ".pi", "agent") : "", extensionAgentDir(), ].filter(Boolean); } function readJsonFile(file: string): unknown { try { return readCompatibleJsonFile(file); } catch { return undefined; } } function readLocalModelsJson(): Record | undefined { for (const dir of candidateAgentDirs()) { const file = join(dir, "models.json"); if (!existsSync(file)) continue; const json = readJsonFile(file); if (isConfigObject(json)) return json; } return undefined; } function providerFromModels(models: Record | undefined, id: string): Record | undefined { const providers = isConfigObject(models?.providers) ? models.providers : undefined; const provider = providers && isConfigObject(providers[id]) ? providers[id] : undefined; return provider; } function providerModels(provider: Record | undefined): ProviderModelConfig[] | undefined { if (!Array.isArray(provider?.models)) return undefined; const models = provider.models.filter(isConfigObject).map((model) => ({ id: String(model.id || ""), name: String(model.name || model.id || ""), api: API_ID, reasoning: false, input: ["text"] as Array<"text">, contextWindow: Number(model.contextWindow || 262144), maxTokens: Number(model.maxTokens || 32768), cost: isConfigObject(model.cost) ? { input: Number(model.cost.input || 0), output: Number(model.cost.output || 0), cacheRead: Number(model.cost.cacheRead || 0), cacheWrite: Number(model.cost.cacheWrite || 0), } : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, })); return models.length > 0 ? models : undefined; } function stringField(provider: Record | undefined, field: string): string | undefined { const value = provider?.[field]; return typeof value === "string" ? value : undefined; } export function loadRuntimeConfig(): ProviderRuntimeConfig { const models = readLocalModelsJson(); const primary = providerFromModels(models, PROVIDER_ID); const baseUrl = process.env.XTALPI_PI_TOOLS_BASE_URL || process.env.XTALPI_BASE_URL || stringField(primary, "baseUrl") || DEFAULT_BASE_URL; const apiKey = process.env.XTALPI_PI_TOOLS_API_KEY || process.env.XTALPI_API_KEY || [stringField(primary, "apiKey")].find( (value) => !isPlaceholderKey(value) && !isDeferredConfigValue(value), ) || ""; const modelsFromConfig = providerModels(primary); return { baseUrl, apiKey, models: modelsFromConfig || DEFAULT_MODELS, }; } export function normalizeBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/+$/, ""); } export function endpointFor(model: Pick, "baseUrl">, runtimeConfig?: Pick): string { const baseUrl = normalizeBaseUrl(model.baseUrl || runtimeConfig?.baseUrl || DEFAULT_BASE_URL); return `${baseUrl}/chat/completions`; } export function resolveRequestTimeoutMs(options?: Pick): number { const optionTimeoutMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs >= 1000 ? Math.floor(options.timeoutMs) : DEFAULT_TIMEOUT_MS; return envInt("XTALPI_PI_TOOLS_TIMEOUT_MS", optionTimeoutMs, 1000); } export function resolveProviderRuntimePolicy( options?: Pick, ): RuntimePolicy { try { return resolveRuntimePolicy(options ? { options } : {}); } catch (error) { if (error instanceof RuntimePolicyConfigurationError) { throw buildProviderError("configuration_invalid", error.message, { details: { configurationVariable: error.variable, }, cause: error, }); } throw error; } } export function resolveMaxOutputTokens( model: Pick, "maxTokens">, options?: Pick, policy?: Pick | string, ): number { if (typeof policy === "object" && typeof policy.maxOutputTokens === "number") { return Math.min(policy.maxOutputTokens, model.maxTokens || 32768); } const optionMaxTokens = typeof options?.maxTokens === "number" && Number.isFinite(options.maxTokens) && options.maxTokens >= 1 ? Math.floor(options.maxTokens) : DEFAULT_MAX_OUTPUT_TOKENS; const configuredMax = envInt("XTALPI_PI_TOOLS_MAX_OUTPUT_TOKENS", optionMaxTokens, 1); return Math.min(configuredMax, model.maxTokens || 32768); } export function buildChatCompletionPayload( model: Pick, "id" | "maxTokens">, messages: XtalpiChatMessage[], options?: Pick, policy?: Pick | string, ): XtalpiChatPayload { const maxTokens = resolveMaxOutputTokens(model, options, policy); const payload: XtalpiChatPayload = { model: model.id, messages, stream: false, max_tokens: maxTokens, }; const temperature = typeof policy === "object" ? policy.temperature : options?.temperature; if (typeof temperature === "number") { payload.temperature = temperature; } const responseFormat = jsonActionResponseFormat(); if (responseFormat) payload.response_format = responseFormat; return payload; }