// pi extension adapter: registers the Grok Build provider with pi's coding // agent so /login offers "xAI (Grok subscription)" and the model lists. // // LOAD-BEARING CONSTRAINT: this module must never import anything under // `@earendil-works/pi-ai/(api|providers)/`. Pi's loader explicitly provides // the version-matched `/compat` entry used below; other runtime subpaths can // typecheck but fail at extension load. It must not import provider.ts either. // extension.test.ts guards this with an import grep. import { hasApi, streamSimpleOpenAIResponses } from "@earendil-works/pi-ai/compat"; import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai"; // type-only: erased, no runtime resolution import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent"; import { GROK_MODELS, XAI_BASE_URL, XAI_PROVIDER_ID, loginXai, refreshXai, requireXaiOAuthCredential, withXaiPayloadInvariants, type XaiLoginHost, } from "./xai-grok-build.ts"; // Typed against ProviderConfig's own oauth surface — the exact shape pi // consumes (pi-ai 0.80.8 dropped the standalone OAuthProviderInterface). const oauth: NonNullable = { name: "xAI (Grok subscription)", async login(callbacks: OAuthLoginCallbacks): Promise { return loginXai(toHost(callbacks)); }, async refreshToken(credentials: OAuthCredentials): Promise { return refreshXai(requireXaiOAuthCredential(credentials)); }, getApiKey(credentials: OAuthCredentials): string { // Sync and string-only, so the provider-level baseUrl below carries what // the library adapter's toAuth() returns alongside the key. return requireXaiOAuthCredential(credentials).access; }, }; // Register synchronously inside the factory: pi drains pending registrations // before any session exists, which is what makes `pi --list-models` and // /login both see the provider. export default function xaiGrokBuild(pi: ExtensionAPI): void { pi.registerProvider(XAI_PROVIDER_ID, { name: "xAI Grok Build", baseUrl: XAI_BASE_URL, // OAuth only, on purpose: no `apiKey` entry means pi never offers an // API-key row for this provider and env keys are ignored. The Grok // subscription token is the sole credential in this project. api: "openai-responses", streamSimple(model, context, options) { if (!hasApi(model, "openai-responses")) { throw new TypeError(`xAI Grok Build cannot stream API ${JSON.stringify(model.api)}`); } return streamSimpleOpenAIResponses(model, context, withXaiPayloadInvariants(options)); }, models: GROK_MODELS.map((model) => ({ id: model.id, name: model.name, headers: { ...model.headers }, reasoning: model.reasoning, // Only some catalog entries set thinkingLevelMap; `in` narrows the union. ...("thinkingLevelMap" in model ? { thinkingLevelMap: { ...model.thinkingLevelMap } } : {}), input: [...model.input], cost: { ...model.cost, ...("tiers" in model.cost ? { tiers: model.cost.tiers.map((tier) => ({ ...tier })) } : {}), }, contextWindow: model.contextWindow, maxTokens: model.maxTokens, compat: { ...model.compat }, })), oauth, } satisfies ProviderConfig); } function toHost(cb: OAuthLoginCallbacks): XaiLoginHost { return { deviceCode: (info) => cb.onDeviceCode(info), progress: (message) => cb.onProgress?.(message), ...(cb.signal !== undefined ? { signal: cb.signal } : {}), }; }