UNPKG

@earendil-works/pi-ai

Version:

Unified LLM API with automatic model discovery and provider configuration

230 lines • 9.78 kB
/** * Kimi Code (subscription) OAuth flow * * RFC 8628 device authorization grant against https://auth.kimi.com with JSON * responses. The access token authenticates requests to * https://api.kimi.com/coding as an `Authorization: Bearer` header. */ import { getProviderEnvValue } from "../../utils/provider-env.js"; import { sleep } from "../../utils/sleep.js"; import { pollOAuthDeviceCodeFlow } from "./device-code.js"; import { trustedHttpUrl } from "./trusted-url.js"; const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098"; const DEFAULT_OAUTH_HOST = "https://auth.kimi.com"; const DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60; const DEFAULT_POLL_INTERVAL_SECONDS = 5; const REQUEST_TIMEOUT_MS = 30 * 1000; const REFRESH_MAX_RETRIES = 3; function getOauthHost() { const override = getProviderEnvValue("KIMI_CODE_OAUTH_HOST") || getProviderEnvValue("KIMI_OAUTH_HOST"); return (override || DEFAULT_OAUTH_HOST).replace(/\/+$/, ""); } function requestSignal(signal) { return AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), signal]); } function formUrlEncode(fields) { return new URLSearchParams(fields).toString(); } async function readJson(response) { try { const json = await response.json(); return json && typeof json === "object" ? json : null; } catch { return null; } } async function startDeviceAuthorization(oauthHost, signal) { const response = await fetch(`${oauthHost}/api/oauth/device_authorization`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: formUrlEncode({ client_id: CLIENT_ID }), signal: requestSignal(signal), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(`Kimi Code device authorization failed with status ${response.status}${text ? `: ${text}` : ""}`); } const json = await readJson(response); const deviceCode = json?.device_code; const userCode = json?.user_code; const verificationUri = trustedHttpUrl(json?.verification_uri); const verificationUriComplete = trustedHttpUrl(json?.verification_uri_complete); if (typeof deviceCode !== "string" || typeof userCode !== "string" || !verificationUri || !verificationUriComplete) { throw new Error(`Invalid Kimi Code device authorization response: ${JSON.stringify(json)}`); } const interval = json?.interval; const expiresIn = json?.expires_in; return { deviceCode, userCode, verificationUri, verificationUriComplete, intervalSeconds: typeof interval === "number" && Number.isFinite(interval) && interval > 0 ? interval : DEFAULT_POLL_INTERVAL_SECONDS, expiresInSeconds: typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : DEVICE_CODE_TIMEOUT_SECONDS, }; } function parseTokenResponse(json, operation) { const accessToken = json?.access_token; const refreshToken = json?.refresh_token; const expiresIn = json?.expires_in; if (typeof accessToken !== "string" || !accessToken || typeof refreshToken !== "string" || !refreshToken || typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) { throw new Error(`Kimi Code token ${operation} response missing fields: ${JSON.stringify(json)}`); } return { access: accessToken, refresh: refreshToken, expires: Date.now() + expiresIn * 1000, }; } async function pollForToken(oauthHost, device, signal) { return pollOAuthDeviceCodeFlow({ intervalSeconds: device.intervalSeconds, expiresInSeconds: device.expiresInSeconds, waitBeforeFirstPoll: true, signal, poll: async () => { const response = await fetch(`${oauthHost}/api/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: formUrlEncode({ client_id: CLIENT_ID, device_code: device.deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code", }), signal: requestSignal(signal), }); if (response.status >= 500) { const text = await response.text().catch(() => ""); return { status: "failed", message: `Kimi Code device token request failed with status ${response.status}${text ? `: ${text}` : ""}`, }; } const json = await readJson(response); if (response.ok && typeof json?.access_token === "string") { try { return { status: "complete", value: parseTokenResponse(json, "poll") }; } catch (error) { return { status: "failed", message: error instanceof Error ? error.message : String(error) }; } } const error = json?.error; const description = typeof json?.error_description === "string" ? `: ${json.error_description}` : ""; if (error === "authorization_pending") { return { status: "pending" }; } if (error === "slow_down") { const interval = json?.interval; return { status: "slow_down", intervalSeconds: typeof interval === "number" && interval > 0 ? interval : undefined, }; } if (error === "expired_token") { return { status: "failed", message: "Kimi Code device authorization expired. Please restart login." }; } if (error === "access_denied") { return { status: "failed", message: "Kimi Code login was denied." }; } return { status: "failed", message: `Kimi Code device token request failed (status ${response.status})${typeof error === "string" ? `: ${error}${description}` : ""}`, }; }, }); } function isRetryableRefreshFailure(response) { return response.status === 429 || response.status >= 500; } async function refreshToken(oauthHost, refreshTokenValue, signal) { let lastError; for (let attempt = 0; attempt <= REFRESH_MAX_RETRIES; attempt++) { if (attempt > 0) { await sleep(1000 * 2 ** (attempt - 1), signal); } if (signal.aborted) { throw new Error("Kimi Code token refresh aborted"); } let response; try { response = await fetch(`${oauthHost}/api/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: formUrlEncode({ client_id: CLIENT_ID, grant_type: "refresh_token", refresh_token: refreshTokenValue, }), signal: requestSignal(signal), }); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); continue; } const json = await readJson(response); if (response.ok) { return parseTokenResponse(json, "refresh"); } // Unauthorized: the stored credential is dead; Models clears it and prompts re-login. if (response.status === 401 || response.status === 403 || json?.error === "invalid_grant") { const description = typeof json?.error_description === "string" ? `: ${json.error_description}` : ""; throw new Error(`Kimi Code token refresh unauthorized (status ${response.status})${description}`); } if (isRetryableRefreshFailure(response) && attempt < REFRESH_MAX_RETRIES) { lastError = new Error(`Kimi Code token refresh failed with status ${response.status}`); continue; } const text = JSON.stringify(json); throw new Error(`Kimi Code token refresh failed with status ${response.status}${text ? `: ${text}` : ""}`); } throw lastError ?? new Error("Kimi Code token refresh failed"); } async function loginKimiCoding(interaction) { const oauthHost = getOauthHost(); const device = await startDeviceAuthorization(oauthHost, interaction.signal); interaction.notify({ type: "device_code", userCode: device.userCode, verificationUri: device.verificationUriComplete, intervalSeconds: device.intervalSeconds, expiresInSeconds: device.expiresInSeconds, }); const token = await pollForToken(oauthHost, device, interaction.signal); return { type: "oauth", access: token.access, refresh: token.refresh, expires: token.expires }; } export const kimiCodingOAuth = { name: "Kimi Code (subscription)", isSubscription: true, loginLabel: "Sign in with Kimi Code", login: loginKimiCoding, refresh: async (credential, signal) => { const token = await refreshToken(getOauthHost(), credential.refresh, signal); return { type: "oauth", access: token.access, refresh: token.refresh, expires: token.expires }; }, async toAuth(credential) { return { headers: { Authorization: `Bearer ${credential.access}` } }; }, }; //# sourceMappingURL=kimi-coding.js.map