import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { OpenAISearchProvider, ResolvedPiWebConfig, SearchTarget } from "./types.ts"; export const PI_WEB_VERSION = "0.5.0"; const CHATGPT_AUTH_CLAIM = "https://api.openai.com/auth"; function joinUrl(baseUrl: string, suffix: string): string { return `${baseUrl.replace(/\/+$/, "")}/${suffix.replace(/^\/+/, "")}`; } export function buildSearchEndpoint(provider: OpenAISearchProvider, baseUrl: string): string { const normalized = baseUrl.replace(/\/+$/, ""); if (provider === "openai-codex") { if (/\/codex$/i.test(normalized)) return joinUrl(normalized, "alpha/search"); if (/\/backend-api$/i.test(normalized)) return joinUrl(normalized, "codex/alpha/search"); } return joinUrl(normalized, "alpha/search"); } function decodeBase64Url(value: string): string { const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); const padding = "=".repeat((4 - (normalized.length % 4)) % 4); return Buffer.from(`${normalized}${padding}`, "base64").toString("utf8"); } export function extractChatGptAccountId(accessToken: string): string | undefined { const payload = accessToken.split(".")[1]; if (!payload) return undefined; try { const decoded = JSON.parse(decodeBase64Url(payload)) as Record; const authClaim = decoded[CHATGPT_AUTH_CLAIM]; if (typeof authClaim !== "object" || authClaim === null) return undefined; const accountId = (authClaim as Record).chatgpt_account_id; return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined; } catch { return undefined; } } function accountIdFromStoredCredential(ctx: ExtensionContext): string | undefined { const credential = ctx.modelRegistry.authStorage.get("openai-codex"); if (!credential || credential.type !== "oauth") return undefined; const accountId = (credential as typeof credential & { accountId?: unknown }).accountId; return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined; } export function missingAuthGuidance(provider: OpenAISearchProvider): string { return provider === "openai-codex" ? "Run /login, select OpenAI Codex, finish ChatGPT authorization, then retry web_search." : "Set OPENAI_API_KEY before starting Pi (or configure OpenAI through /login), then retry web_search."; } export async function resolveSearchTarget( ctx: ExtensionContext, config: ResolvedPiWebConfig, ): Promise { if (config.provider === "exa") throw new Error("Exa does not use an OpenAI standalone search target"); const model = ctx.modelRegistry.find(config.provider, config.model); if (!model) { const knownModels = ctx.modelRegistry .getAll() .filter((candidate) => candidate.provider === config.provider) .map((candidate) => candidate.id); const suffix = knownModels.length > 0 ? ` Known ${config.provider} models: ${knownModels.join(", ")}.` : ""; throw new Error( `Configured standalone search model ${config.provider}/${config.model} does not exist in this Pi installation.${suffix} Run /web-config to choose an installed model.`, ); } const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model); if (!auth.ok) throw new Error(`Cannot resolve ${config.provider} authentication: ${auth.error}. ${missingAuthGuidance(config.provider)}`); if (!auth.apiKey) throw new Error(`No authentication is configured for ${config.provider}. ${missingAuthGuidance(config.provider)}`); const headers: Record = { ...auth.headers, accept: "application/json", authorization: `Bearer ${auth.apiKey}`, "content-type": "application/json", version: PI_WEB_VERSION, }; if (config.provider === "openai-codex") { const accountId = accountIdFromStoredCredential(ctx) ?? extractChatGptAccountId(auth.apiKey); if (!accountId) { throw new Error( `The OpenAI Codex credential has no ChatGPT account id. Log out and authenticate again. ${missingAuthGuidance(config.provider)}`, ); } headers["ChatGPT-Account-ID"] = accountId; } return { endpoint: buildSearchEndpoint(config.provider, model.baseUrl), model: model.id, provider: config.provider, headers, }; }