/** * Official Openference provider for Pi. * * - Device-flow OAuth (same AS as Deyin): prints a clickable/copyable link * - Live model discovery from GET /v1/models with Pi-tuned metadata * - Plan RPM soft pacing + transient error retry + context-overflow normalize * * Usage: * pi install npm:@openference/pi-provider * /login openference * /model openference/GLM-5.2 */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { OAuthCredentials, OAuthLoginCallbacks, RefreshModelsContext, } from "@earendil-works/pi-ai"; import { API_KEY_ENV, OPENFERENCE_API, OPENFERENCE_BASE_URL, OPENFERENCE_PROVIDER, OPENFERENCE_USER_AGENT, } from "./constants.ts"; import { FAR_FUTURE_EXPIRES, resolveStartupApiKey } from "./auth.ts"; import { loginWithDeviceFlow, refreshAccessToken, } from "./device-auth.ts"; import { FALLBACK_MODELS, fetchModels, toModelConfig, type OpenferenceModelInfo, } from "./models.ts"; import { fetchAccountLimits } from "./limits.ts"; import { createPacingState, createPacedStream, setPacingRpm } from "./pacing.ts"; import { rewriteOverflowError } from "./overflow.ts"; import { rewriteForRetry } from "./retry.ts"; import { createRetryStream, type BaseStreamFn } from "./retry-stream.ts"; import { buildRefreshedModels, createModelCatalogState } from "./refresh-models.ts"; export { RETRYABLE_ERRORS, STREAM_RETRYABLE_ERRORS, RETRYABLE_PREFIX, retryableErrorFor, rewriteForRetry, isRetryableStreamError, } from "./retry.ts"; export { rewriteOverflowError, OVERFLOW_PATTERN, RATE_LIMIT_PATTERN } from "./overflow.ts"; export { safeContextWindow, perTokenToPerMillion, buildThinkingLevelMap, toModelConfig, } from "./models.ts"; export { minIntervalMsForRpm, fetchAccountLimits } from "./limits.ts"; export { waitMsBeforeNextStart, createPacingState, acquirePacingSlot } from "./pacing.ts"; export { assertOpenferenceHttpsUrl } from "./device-auth.ts"; export { accessTokenFromCredential } from "./credentials.ts"; export { buildRefreshedModels, createModelCatalogState } from "./refresh-models.ts"; import * as piAiCompat from "@earendil-works/pi-ai/compat"; const openaiStreamSimple = ( piAiCompat as unknown as { streamSimpleOpenAICompletions: BaseStreamFn } ).streamSimpleOpenAICompletions; const pacingState = createPacingState(60); async function loadModels(apiKey: string | undefined): Promise { if (!apiKey) return FALLBACK_MODELS; try { const models = await fetchModels(apiKey); if (models.length > 0) return models; } catch (err) { console.warn(`[openference] model discovery failed: ${(err as Error).message}`); } return FALLBACK_MODELS; } async function refreshPacingFromLimits(accessToken: string | undefined): Promise { if (!accessToken) return; try { const limits = await fetchAccountLimits(accessToken); setPacingRpm(pacingState, limits.maxRpm); } catch (err) { console.warn(`[openference] limits fetch failed: ${(err as Error).message}`); } } function isPastableApiKeyCredential(credentials: OAuthCredentials): boolean { const access = typeof credentials.access === "string" ? credentials.access.trim() : ""; const refresh = typeof credentials.refresh === "string" ? credentials.refresh.trim() : ""; return ( access.length > 0 && refresh === access && typeof credentials.expires === "number" && credentials.expires > Date.now() + 365 * 24 * 3600 * 1000 ); } export default async function (pi: ExtensionAPI) { const apiKey = resolveStartupApiKey(); const models = await loadModels(apiKey); const catalog = createModelCatalogState(models); if (!apiKey) { console.info("[openference] no stored credential found; run /login openference"); } else { await refreshPacingFromLimits(apiKey); } // Pace inside each attempt: retry wraps paced openai stream so transient // retries still honor plan RPM. const pacedOpenai = createPacedStream(openaiStreamSimple, pacingState); const streamSimple = createRetryStream(pacedOpenai); pi.registerProvider(OPENFERENCE_PROVIDER, { name: "Openference", baseUrl: OPENFERENCE_BASE_URL, apiKey: `$${API_KEY_ENV}`, api: OPENFERENCE_API, authHeader: true, headers: { "User-Agent": OPENFERENCE_USER_AGENT }, models: models.map(toModelConfig), streamSimple, async refreshModels(context: RefreshModelsContext) { return buildRefreshedModels(context, { catalog, pacingState, }); }, oauth: { name: "Openference", async login(callbacks: OAuthLoginCallbacks): Promise { const method = await callbacks.onSelect({ message: "How do you want to sign in to Openference?", options: [ { id: "device", label: "Browser link (recommended)" }, { id: "apikey", label: "Paste an API key" }, ], }); if (!method || method === "device") { const creds = await loginWithDeviceFlow(callbacks); callbacks.onProgress?.("Signed in. Refreshing model list…"); await refreshPacingFromLimits(creds.access); return creds; } callbacks.onProgress?.("Create an API key at https://openference.com/dashboard"); const key = await callbacks.onPrompt({ message: "Openference API key:", }); if (!key.trim()) throw new Error("No API key entered"); callbacks.onProgress?.("Validating key…"); const ids = await fetchModels(key.trim()); if (ids.length === 0) throw new Error("key returned no models"); callbacks.onProgress?.(`Validated: ${ids.length} models available`); await refreshPacingFromLimits(key.trim()); return { access: key.trim(), refresh: key.trim(), expires: FAR_FUTURE_EXPIRES, }; }, async refreshToken(credentials: OAuthCredentials): Promise { if (isPastableApiKeyCredential(credentials)) { return credentials; } const access = typeof credentials.access === "string" ? credentials.access.trim() : ""; const refresh = typeof credentials.refresh === "string" ? credentials.refresh.trim() : ""; if (!access || !refresh) { throw new Error("Stored credentials are incomplete; run /login openference again"); } // Opaque API keys (non-JWT) never use the OAuth token endpoint. if ( !access.startsWith("eyJ") && !refresh.startsWith("eyJ") && !refresh.startsWith("oar_") ) { return { ...credentials, access, refresh }; } const next = await refreshAccessToken({ ...credentials, access, refresh }); await refreshPacingFromLimits(next.access); return next; }, getApiKey(credentials: OAuthCredentials): string { const access = typeof credentials.access === "string" ? credentials.access.trim() : ""; if (!access) { throw new Error("Stored credentials are incomplete; run /login openference again"); } return access; }, }, }); pi.on("message_end", (event, ctx) => { const overflow = rewriteOverflowError(event.message, ctx.model?.provider); if (overflow) { return { message: { ...event.message, errorMessage: overflow, }, }; } const retry = rewriteForRetry(event.message, ctx.model?.provider); if (!retry) return; return { message: { ...event.message, errorMessage: retry, }, }; }); }