@dreb/ai 2.42.0 → 2.43.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/models.d.ts +1 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +97 -0
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +102 -32
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +3 -0
- package/dist/models.js.map +1 -1
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +6 -6
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/oauth/kimi-coding.d.ts +13 -3
- package/dist/utils/oauth/kimi-coding.d.ts.map +1 -1
- package/dist/utils/oauth/kimi-coding.js +347 -89
- package/dist/utils/oauth/kimi-coding.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kimi-coding.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/kimi-coding.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAuB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AA2FhG;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUzD;AAoBD,MAAM,MAAM,aAAa,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB,CAAC;AAgDF;;;GAGG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAkB9E;AA8MD,wBAAsB,eAAe,CAAC,OAAO,EAAE;IAC9C,MAAM,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmC5B;AAMD,wBAAsB,sBAAsB,CAC3C,WAAW,EAAE,gBAAgB,EAC7B,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,gBAAgB,CAAC,CA4B3B;AAMD,eAAO,MAAM,uBAAuB,EAAE,sBA8CrC,CAAC","sourcesContent":["/**\n * Kimi For Coding OAuth flow (device code)\n *\n * Authenticates against Moonshot's Kimi API (auth.kimi.com) with scope \"kimi-code\".\n * Uses the device authorization grant flow to obtain access/refresh tokens,\n * then discovers the user's model entitlement via the /models endpoint.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { Api, Model } from \"../../types.js\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.js\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst KIMI_CLI_VERSION = \"1.35.0\";\nconst USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`;\nconst OAUTH_HOST = \"https://auth.kimi.com\";\nconst OAUTH_DEVICE_AUTH_URL = `${OAUTH_HOST}/api/oauth/device_authorization`;\nconst OAUTH_TOKEN_URL = `${OAUTH_HOST}/api/oauth/token`;\nconst OAUTH_CLIENT_ID = \"17e5f671-d194-4dfb-9706-5516cb48c098\";\nconst OAUTH_SCOPE = \"kimi-code\";\nconst OAUTH_DEVICE_GRANT = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst OAUTH_REFRESH_GRANT = \"refresh_token\";\nconst API_BASE_URL = \"https://api.kimi.com/coding/v1\";\n\nconst DEVICE_ID_PATH = path.join(os.homedir(), \".kimi\", \"device_id\");\n\nconst MAX_REFRESH_RETRIES = 3;\n\n// ============================================================================\n// Device ID\n// ============================================================================\n\nfunction generateDeviceId(): string {\n\t// UUID v4 without dashes (hex only, 32 chars)\n\treturn \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\".replace(/x/g, () => Math.floor(Math.random() * 16).toString(16));\n}\n\nfunction getDeviceId(): string {\n\ttry {\n\t\tif (fs.existsSync(DEVICE_ID_PATH)) {\n\t\t\tconst id = fs.readFileSync(DEVICE_ID_PATH, \"utf-8\").trim();\n\t\t\tif (/^[0-9a-f]{32}$/i.test(id)) {\n\t\t\t\treturn id;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Fall through to generate\n\t}\n\n\tconst id = generateDeviceId();\n\ttry {\n\t\tfs.mkdirSync(path.dirname(DEVICE_ID_PATH), { recursive: true });\n\t\tfs.writeFileSync(DEVICE_ID_PATH, id, \"utf-8\");\n\t} catch {\n\t\t// If we can't persist, just use the generated ID for this session\n\t}\n\treturn id;\n}\n\n// ============================================================================\n// Header helpers\n// ============================================================================\n\n/**\n * Strip non-ASCII characters from a string for use in HTTP header values.\n */\nfunction asciiHeaderValue(value: string): string {\n\treturn value.replace(/[^\\x20-\\x7E]/g, \"\");\n}\n\n/**\n * Determine the device model string, mirroring kimi-cli logic.\n */\nfunction kimiDeviceModel(): string {\n\tconst platform = os.platform();\n\tconst machine = os.machine?.() || process.arch;\n\n\tif (platform === \"darwin\") {\n\t\tlet version: string;\n\t\ttry {\n\t\t\tversion = execFileSync(\"sw_vers\", [\"-productVersion\"], { encoding: \"utf-8\", timeout: 3000 }).trim();\n\t\t} catch {\n\t\t\tversion = os.release();\n\t\t}\n\t\treturn `macOS ${version} ${machine}`;\n\t}\n\n\tif (platform === \"win32\") {\n\t\tconst release = os.release();\n\t\tconst buildNumber = Number.parseInt(release.split(\".\").pop() || \"0\", 10);\n\t\tconst label = buildNumber >= 22000 ? \"11\" : \"10\";\n\t\treturn `Windows ${label} ${machine}`;\n\t}\n\n\t// Linux and other\n\treturn `${os.type()} ${os.release()} ${machine}`;\n}\n\n/**\n * Build the standard set of headers required on every Kimi API request.\n */\nexport function buildKimiHeaders(): Record<string, string> {\n\treturn {\n\t\t\"User-Agent\": USER_AGENT,\n\t\t\"X-Msh-Platform\": \"kimi_cli\",\n\t\t\"X-Msh-Version\": KIMI_CLI_VERSION,\n\t\t\"X-Msh-Device-Name\": asciiHeaderValue(os.hostname()),\n\t\t\"X-Msh-Device-Model\": asciiHeaderValue(kimiDeviceModel()),\n\t\t\"X-Msh-Device-Id\": getDeviceId(),\n\t\t\"X-Msh-Os-Version\": asciiHeaderValue(os.version?.() || `${os.type()} ${os.release()}`),\n\t};\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\ntype DeviceCodeResponse = {\n\tdevice_code: string;\n\tuser_code: string;\n\tverification_uri: string;\n\tinterval: number;\n\texpires_in: number;\n};\n\ntype TokenSuccessResponse = {\n\taccess_token: string;\n\trefresh_token: string;\n\texpires_in: number;\n};\n\nexport type KimiModelInfo = {\n\tid: string;\n\tdisplay_name: string;\n\tcontext_length: number;\n\tsupports_reasoning?: boolean;\n\t[key: string]: unknown;\n};\n\ntype KimiCredentials = OAuthCredentials & {\n\tmodelId?: string;\n\tcontextLength?: number;\n\tmodelDisplay?: string;\n};\n\n// ============================================================================\n// Network helpers\n// ============================================================================\n\nasync function fetchJson(url: string, init: RequestInit): Promise<unknown> {\n\tconst response = await fetch(url, init);\n\tif (!response.ok) {\n\t\tconst text = await response.text();\n\t\tthrow new Error(`${response.status} ${response.statusText}: ${text}`);\n\t}\n\treturn response.json();\n}\n\n/**\n * Sleep that can be interrupted by an AbortSignal.\n */\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Login cancelled\"));\n\t\t\treturn;\n\t\t}\n\n\t\tconst timeout = setTimeout(resolve, ms);\n\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new Error(\"Login cancelled\"));\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n// ============================================================================\n// Model discovery\n// ============================================================================\n\n/**\n * List available models from the Kimi API.\n * Returns the model info array from the response's `data` field.\n */\nexport async function listModels(accessToken: string): Promise<KimiModelInfo[]> {\n\tconst raw = await fetchJson(`${API_BASE_URL}/models`, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t\t...buildKimiHeaders(),\n\t\t},\n\t});\n\n\tif (!raw || typeof raw !== \"object\") {\n\t\tthrow new Error(\"Invalid models response\");\n\t}\n\n\tconst data = (raw as Record<string, unknown>).data;\n\tif (!Array.isArray(data)) {\n\t\tthrow new Error(\"Invalid models response: expected data array\");\n\t}\n\n\treturn data as KimiModelInfo[];\n}\n\n// ============================================================================\n// Device flow\n// ============================================================================\n\nasync function startDeviceFlow(): Promise<DeviceCodeResponse> {\n\tconst data = await fetchJson(OAUTH_DEVICE_AUTH_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t...buildKimiHeaders(),\n\t\t},\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: OAUTH_CLIENT_ID,\n\t\t\tscope: OAUTH_SCOPE,\n\t\t}),\n\t});\n\n\tif (!data || typeof data !== \"object\") {\n\t\tthrow new Error(\"Invalid device code response\");\n\t}\n\n\tconst d = data as Record<string, unknown>;\n\tconst device_code = d.device_code;\n\tconst user_code = d.user_code;\n\tconst verification_uri = d.verification_uri;\n\tconst interval = d.interval;\n\tconst expires_in = d.expires_in;\n\n\tif (\n\t\ttypeof device_code !== \"string\" ||\n\t\ttypeof user_code !== \"string\" ||\n\t\ttypeof verification_uri !== \"string\" ||\n\t\ttypeof interval !== \"number\" ||\n\t\ttypeof expires_in !== \"number\"\n\t) {\n\t\tthrow new Error(\"Invalid device code response fields\");\n\t}\n\n\treturn { device_code, user_code, verification_uri, interval, expires_in };\n}\n\nasync function pollForAccessToken(\n\tdeviceCode: string,\n\tintervalSeconds: number,\n\texpiresIn: number,\n\tsignal?: AbortSignal,\n): Promise<TokenSuccessResponse> {\n\tconst deadline = Date.now() + expiresIn * 1000;\n\tlet intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));\n\n\twhile (Date.now() < deadline) {\n\t\tif (signal?.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\n\t\tconst remainingMs = deadline - Date.now();\n\t\tconst waitMs = Math.min(intervalMs, remainingMs);\n\t\tawait abortableSleep(waitMs, signal);\n\n\t\tconst tokenResponse = await fetch(OAUTH_TOKEN_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t...buildKimiHeaders(),\n\t\t\t},\n\t\t\tbody: new URLSearchParams({\n\t\t\t\tclient_id: OAUTH_CLIENT_ID,\n\t\t\t\tdevice_code: deviceCode,\n\t\t\t\tgrant_type: OAUTH_DEVICE_GRANT,\n\t\t\t}),\n\t\t});\n\n\t\t// The token endpoint returns 400 for authorization_pending / slow_down / expired_token.\n\t\t// We must read the body regardless of status to handle the OAuth error codes.\n\t\tconst resp = (await tokenResponse.json()) as Record<string, unknown>;\n\n\t\t// Success: has access_token\n\t\tif (typeof resp.access_token === \"string\") {\n\t\t\treturn resp as unknown as TokenSuccessResponse;\n\t\t}\n\n\t\t// Error response (RFC 8628 §3.5)\n\t\tif (typeof resp.error === \"string\") {\n\t\t\tconst error = resp.error;\n\t\t\tconst description = resp.error_description as string | undefined;\n\t\t\tconst newInterval = resp.interval as number | undefined;\n\n\t\t\tif (error === \"authorization_pending\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (error === \"slow_down\") {\n\t\t\t\tintervalMs =\n\t\t\t\t\ttypeof newInterval === \"number\" && newInterval > 0\n\t\t\t\t\t\t? newInterval * 1000\n\t\t\t\t\t\t: Math.max(1000, intervalMs + 5000);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (error === \"expired_token\") {\n\t\t\t\tthrow new Error(\"Device code expired. Please try logging in again.\");\n\t\t\t}\n\n\t\t\tconst descriptionSuffix = description ? `: ${description}` : \"\";\n\t\t\tthrow new Error(`Device flow failed: ${error}${descriptionSuffix}`);\n\t\t}\n\n\t\t// Unexpected response: valid object but no access_token or error field\n\t\tthrow new Error(`Unexpected token response: ${JSON.stringify(resp)}`);\n\t}\n\n\tthrow new Error(\"Device flow timed out\");\n}\n\n// ============================================================================\n// Refresh with retry\n// ============================================================================\n\nconst RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];\n\nclass RetriableError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"RetriableError\";\n\t}\n}\n\n/**\n * Heuristic to detect network-level errors that should be retried.\n * Fetch throws TypeError on network failures; some runtimes include\n * recognizable substrings in the message.\n */\nfunction isNetworkError(error: Error): boolean {\n\tif (error instanceof TypeError) return true;\n\tconst msg = error.message.toLowerCase();\n\treturn [\"fetch failed\", \"econnrefused\", \"etimedout\", \"enotfound\", \"econnreset\", \"socket hang up\"].some((s) =>\n\t\tmsg.includes(s),\n\t);\n}\n\nasync function refreshWithRetry(refreshToken: string, signal?: AbortSignal): Promise<TokenSuccessResponse> {\n\tlet lastError: Error | undefined;\n\n\tfor (let attempt = 0; attempt < MAX_REFRESH_RETRIES; attempt++) {\n\t\tif (signal?.aborted) {\n\t\t\tthrow new Error(\"Refresh cancelled\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst response = await fetch(OAUTH_TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t...buildKimiHeaders(),\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tclient_id: OAUTH_CLIENT_ID,\n\t\t\t\t\trefresh_token: refreshToken,\n\t\t\t\t\tgrant_type: OAUTH_REFRESH_GRANT,\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Retry on retriable status codes\n\t\t\tif (RETRYABLE_STATUS_CODES.includes(response.status)) {\n\t\t\t\tthrow new RetriableError(`Token refresh failed with status ${response.status}`);\n\t\t\t}\n\n\t\t\tif (!response.ok) {\n\t\t\t\tconst text = await response.text();\n\t\t\t\tthrow new Error(`Token refresh failed: ${response.status} ${response.statusText}: ${text}`);\n\t\t\t}\n\n\t\t\tconst raw = await response.json();\n\t\t\tif (!raw || typeof raw !== \"object\" || typeof (raw as Record<string, unknown>).access_token !== \"string\") {\n\t\t\t\tthrow new Error(\"Invalid token refresh response\");\n\t\t\t}\n\n\t\t\treturn raw as unknown as TokenSuccessResponse;\n\t\t} catch (error) {\n\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\n\t\t\t// Wrap network errors (TypeError from fetch, or common network failure indicators) as retriable\n\t\t\tif (!(lastError instanceof RetriableError) && isNetworkError(lastError)) {\n\t\t\t\tlastError = new RetriableError(lastError.message);\n\t\t\t}\n\n\t\t\t// Retry on retriable errors (network failures or retriable HTTP status codes)\n\t\t\tif (lastError instanceof RetriableError && attempt < MAX_REFRESH_RETRIES - 1) {\n\t\t\t\tconst backoffMs = Math.min(1000 * 2 ** attempt, 10000);\n\t\t\t\tawait abortableSleep(backoffMs, signal);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tthrow lastError;\n\t\t}\n\t}\n\n\tthrow lastError ?? new Error(\"Token refresh failed after retries\");\n}\n\n// ============================================================================\n// Login flow\n// ============================================================================\n\nexport async function loginKimiCoding(options: {\n\tonAuth: (info: { url: string; instructions?: string }) => void;\n\tonProgress?: (message: string) => void;\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst device = await startDeviceFlow();\n\t// Kimi's device page expects user_code as a query parameter\n\tconst authUrl = new URL(device.verification_uri);\n\tauthUrl.searchParams.set(\"user_code\", device.user_code);\n\toptions.onAuth({\n\t\turl: authUrl.toString(),\n\t\tinstructions: `Enter code: ${device.user_code}`,\n\t});\n\n\tconst tokenResp = await pollForAccessToken(device.device_code, device.interval, device.expires_in, options.signal);\n\n\t// Discover model entitlement\n\toptions.onProgress?.(\"Discovering available models...\");\n\tlet models: KimiModelInfo[] = [];\n\ttry {\n\t\tmodels = await listModels(tokenResp.access_token);\n\t} catch {\n\t\t// Proceed without model enrichment if the models endpoint fails\n\t}\n\n\tconst credentials: KimiCredentials = {\n\t\trefresh: tokenResp.refresh_token,\n\t\taccess: tokenResp.access_token,\n\t\texpires: Date.now() + tokenResp.expires_in * 1000,\n\t};\n\n\tif (models.length > 0) {\n\t\tconst primary = models[0];\n\t\tcredentials.modelId = primary.id;\n\t\tcredentials.contextLength = primary.context_length;\n\t\tcredentials.modelDisplay = primary.display_name;\n\t}\n\n\treturn credentials;\n}\n\n// ============================================================================\n// Refresh\n// ============================================================================\n\nexport async function refreshKimiCodingToken(\n\tcredentials: OAuthCredentials,\n\tsignal?: AbortSignal,\n): Promise<OAuthCredentials> {\n\tconst tokenResp = await refreshWithRetry(credentials.refresh, signal);\n\n\t// Re-discover model entitlement\n\tlet models: KimiModelInfo[] = [];\n\ttry {\n\t\tmodels = await listModels(tokenResp.access_token);\n\t} catch {\n\t\t// Proceed without model enrichment if the models endpoint fails\n\t}\n\n\tconst fresh: KimiCredentials = {\n\t\trefresh: tokenResp.refresh_token ?? credentials.refresh,\n\t\taccess: tokenResp.access_token,\n\t\texpires: Date.now() + tokenResp.expires_in * 1000,\n\t\tmodelId: (credentials as KimiCredentials).modelId,\n\t\tcontextLength: (credentials as KimiCredentials).contextLength,\n\t\tmodelDisplay: (credentials as KimiCredentials).modelDisplay,\n\t};\n\n\tif (models.length > 0) {\n\t\tconst primary = models[0];\n\t\tfresh.modelId = primary.id;\n\t\tfresh.contextLength = primary.context_length;\n\t\tfresh.modelDisplay = primary.display_name;\n\t}\n\n\treturn fresh;\n}\n\n// ============================================================================\n// Provider\n// ============================================================================\n\nexport const kimiCodingOAuthProvider: OAuthProviderInterface = {\n\tid: \"kimi-coding-oauth\",\n\tname: \"Kimi For Coding\",\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginKimiCoding({\n\t\t\tonAuth: callbacks.onAuth,\n\t\t\tonProgress: callbacks.onProgress,\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\treturn refreshKimiCodingToken(credentials);\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst creds = credentials as KimiCredentials;\n\t\tconst headers = buildKimiHeaders();\n\n\t\treturn models.map((m) => {\n\t\t\tif (m.provider !== \"kimi-coding-oauth\") return m;\n\n\t\t\tconst updated = {\n\t\t\t\t...m,\n\t\t\t\t// The OAuth coding endpoint accepts OpenAI-style image_url data URLs for\n\t\t\t\t// kimi-for-coding; keep this capability even if static metadata is stale.\n\t\t\t\tinput: Array.from(new Set([...m.input, \"image\" as const])),\n\t\t\t\theaders: { ...headers, ...(m.headers || {}) },\n\t\t\t};\n\n\t\t\tif (creds.modelId) {\n\t\t\t\tupdated.id = creds.modelId;\n\t\t\t}\n\n\t\t\tif (creds.contextLength) {\n\t\t\t\tupdated.contextWindow = creds.contextLength;\n\t\t\t}\n\n\t\t\treturn updated;\n\t\t});\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"kimi-coding.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/kimi-coding.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,gBAAgB,EAAuB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAoHhG;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWzD;AAmCD,MAAM,MAAM,aAAa,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;IAChD,aAAa,CAAC,EAAE;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;QACzB,cAAc,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB,CAAC;AAuIF;;;GAGG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAmBpG;AAyND,wBAAsB,eAAe,CAAC,OAAO,EAAE;IAC9C,MAAM,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqD5B;AAMD,wBAAsB,sBAAsB,CAC3C,WAAW,EAAE,gBAAgB,EAC7B,MAAM,CAAC,EAAE,WAAW,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAiC3B;AAMD,eAAO,MAAM,uBAAuB,EAAE,sBA2KrC,CAAC","sourcesContent":["/**\n * Kimi For Coding OAuth flow (device code)\n *\n * Authenticates against Moonshot's Kimi API (auth.kimi.com) using the current Kimi Code device identity.\n * Uses the device authorization grant flow to obtain access/refresh tokens,\n * then discovers the user's model entitlement via the /models endpoint.\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { Api, Model } from \"../../types.js\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.js\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nfunction readDrebVersion(): string {\n\ttry {\n\t\tconst pkg = JSON.parse(fs.readFileSync(new URL(\"../../../package.json\", import.meta.url), \"utf-8\")) as {\n\t\t\tversion?: unknown;\n\t\t};\n\t\tif (typeof pkg.version === \"string\" && pkg.version.length > 0) return pkg.version;\n\t} catch {\n\t\t// Fall back for unusual bundlers that omit package.json.\n\t}\n\treturn \"unknown\";\n}\n\nconst DREB_VERSION = readDrebVersion();\nconst USER_AGENT = `dreb/${DREB_VERSION}`;\nconst OAUTH_HOST = \"https://auth.kimi.com\";\nconst OAUTH_DEVICE_AUTH_URL = `${OAUTH_HOST}/api/oauth/device_authorization`;\nconst OAUTH_TOKEN_URL = `${OAUTH_HOST}/api/oauth/token`;\nconst OAUTH_CLIENT_ID = \"17e5f671-d194-4dfb-9706-5516cb48c098\";\nconst OAUTH_DEVICE_GRANT = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst OAUTH_REFRESH_GRANT = \"refresh_token\";\nconst API_BASE_URL = \"https://api.kimi.com/coding/v1\";\n\nconst KIMI_CODE_HOME = path.join(os.homedir(), \".kimi-code\");\nconst DEVICE_ID_PATH = path.join(KIMI_CODE_HOME, \"device_id\");\n\nconst MAX_REFRESH_RETRIES = 3;\nconst MAX_DEVICE_FLOW_MS = 15 * 60 * 1000;\n\n// ============================================================================\n// Device ID\n// ============================================================================\n\nlet sessionDeviceId: string | undefined;\n\nfunction getDeviceId(): string {\n\tif (sessionDeviceId) return sessionDeviceId;\n\ttry {\n\t\tif (fs.existsSync(DEVICE_ID_PATH)) {\n\t\t\tconst id = fs.readFileSync(DEVICE_ID_PATH, \"utf-8\").trim();\n\t\t\tif (id.length > 0) {\n\t\t\t\tsessionDeviceId = id;\n\t\t\t\treturn id;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Fall through to generate.\n\t}\n\n\tconst id = randomUUID();\n\tsessionDeviceId = id;\n\ttry {\n\t\tfs.mkdirSync(KIMI_CODE_HOME, { recursive: true, mode: 0o700 });\n\t\tfs.writeFileSync(DEVICE_ID_PATH, id, { encoding: \"utf-8\", mode: 0o600 });\n\t} catch {\n\t\t// If we can't persist, just use the generated ID for this session.\n\t}\n\treturn id;\n}\n\n// ============================================================================\n// Header helpers\n// ============================================================================\n\n/**\n * Strip non-ASCII characters from a string for use in HTTP header values.\n */\nfunction asciiHeaderValue(value: string, fallback = \"unknown\"): string {\n\tconst cleaned = value.replace(/[^\\x20-\\x7E]/g, \"\").trim();\n\treturn cleaned.length > 0 ? cleaned : fallback;\n}\n\nfunction customKimiHeaders(): Record<string, string> {\n\tconst raw = process.env.KIMI_CODE_CUSTOM_HEADERS?.trim();\n\tif (!raw) return {};\n\tconst headers: Record<string, string> = {};\n\tfor (const line of raw.split(\"\\n\")) {\n\t\tconst colon = line.indexOf(\":\");\n\t\tif (colon < 0) continue;\n\t\tconst name = line.slice(0, colon).trim();\n\t\tif (!name) continue;\n\t\theaders[name] = line.slice(colon + 1).trim();\n\t}\n\treturn headers;\n}\n\n/**\n * Determine the device model string, mirroring kimi-cli logic.\n */\nfunction kimiDeviceModel(): string {\n\tconst platform = os.platform();\n\tconst machine = os.arch();\n\n\tif (platform === \"darwin\") {\n\t\tlet version: string;\n\t\ttry {\n\t\t\tversion = execFileSync(\"/usr/bin/sw_vers\", [\"-productVersion\"], { encoding: \"utf-8\", timeout: 3000 }).trim();\n\t\t} catch {\n\t\t\tversion = os.release();\n\t\t}\n\t\treturn `macOS ${version} ${machine}`;\n\t}\n\n\tif (platform === \"win32\") {\n\t\treturn `Windows ${os.release()} ${machine}`;\n\t}\n\n\t// Linux and other\n\treturn `${os.type()} ${os.release()} ${machine}`.trim();\n}\n\n/**\n * Build the standard set of headers required on every Kimi API request.\n */\nexport function buildKimiHeaders(): Record<string, string> {\n\treturn {\n\t\t...customKimiHeaders(),\n\t\t\"User-Agent\": USER_AGENT,\n\t\t\"X-Msh-Platform\": \"kimi_code_cli\",\n\t\t\"X-Msh-Version\": DREB_VERSION,\n\t\t\"X-Msh-Device-Name\": asciiHeaderValue(os.hostname()),\n\t\t\"X-Msh-Device-Model\": asciiHeaderValue(kimiDeviceModel()),\n\t\t\"X-Msh-Device-Id\": getDeviceId(),\n\t\t\"X-Msh-Os-Version\": asciiHeaderValue(os.release()),\n\t};\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\ntype DeviceCodeResponse = {\n\tdevice_code: string;\n\tuser_code: string;\n\tverification_uri_complete: string;\n\tinterval: number;\n\texpires_in: number;\n};\n\ntype TokenSuccessResponse = {\n\taccess_token: string;\n\trefresh_token: string;\n\texpires_in: number;\n};\n\nfunction parseTokenSuccess(response: Record<string, unknown>): TokenSuccessResponse {\n\tif (\n\t\ttypeof response.access_token !== \"string\" ||\n\t\tresponse.access_token.length === 0 ||\n\t\ttypeof response.refresh_token !== \"string\" ||\n\t\tresponse.refresh_token.length === 0 ||\n\t\ttypeof response.expires_in !== \"number\" ||\n\t\t!Number.isFinite(response.expires_in) ||\n\t\tresponse.expires_in <= 0\n\t) {\n\t\tthrow new Error(\"Invalid token response fields\");\n\t}\n\treturn response as unknown as TokenSuccessResponse;\n}\n\nexport type KimiModelInfo = {\n\tid: string;\n\tdisplay_name?: string;\n\tcontext_length: number;\n\tsupports_reasoning?: boolean;\n\tsupports_image_in?: boolean;\n\tsupports_video_in?: boolean;\n\tsupports_tool_use?: boolean;\n\tsupports_thinking_type?: \"only\" | \"no\" | \"both\";\n\tthink_efforts?: {\n\t\tsupport?: boolean;\n\t\tvalid_efforts?: string[];\n\t\tdefault_effort?: string;\n\t};\n\tprotocol?: string;\n\t[key: string]: unknown;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction parseStringArray(value: unknown): string[] | undefined {\n\tif (!Array.isArray(value)) return undefined;\n\tconst out = value.filter((v): v is string => typeof v === \"string\" && v.length > 0);\n\treturn out.length > 0 ? out : undefined;\n}\n\nfunction parseSupportsThinkingType(value: unknown): KimiModelInfo[\"supports_thinking_type\"] | undefined {\n\treturn value === \"only\" || value === \"no\" || value === \"both\" ? value : undefined;\n}\n\nfunction parseModelProtocol(value: unknown): \"anthropic\" | undefined {\n\treturn value === \"anthropic\" ? \"anthropic\" : undefined;\n}\n\nfunction parseThinkEfforts(value: unknown): KimiModelInfo[\"think_efforts\"] | undefined {\n\tif (!isRecord(value) || value.support !== true) return undefined;\n\treturn {\n\t\tsupport: true,\n\t\tvalid_efforts: parseStringArray(value.valid_efforts),\n\t\tdefault_effort:\n\t\t\ttypeof value.default_effort === \"string\" && value.default_effort.length > 0 ? value.default_effort : undefined,\n\t};\n}\n\nfunction toKimiModelInfo(item: unknown): KimiModelInfo | undefined {\n\tif (!isRecord(item) || typeof item.id !== \"string\" || item.id.length === 0) {\n\t\treturn undefined;\n\t}\n\n\tconst contextLength = Number(item.context_length);\n\tif (!Number.isInteger(contextLength) || contextLength <= 0) {\n\t\tthrow new Error(`Kimi Code model \"${item.id}\" must include a positive context_length.`);\n\t}\n\n\tconst displayName = item.display_name;\n\tconst normalizedDisplayName = typeof displayName === \"string\" && displayName.length > 0 ? displayName : undefined;\n\n\tconst optionalBoolean = (value: unknown): boolean | undefined => (typeof value === \"boolean\" ? value : undefined);\n\n\tconst parsed: KimiModelInfo = {\n\t\tid: item.id,\n\t\tcontext_length: contextLength,\n\t\tdisplay_name: normalizedDisplayName,\n\t};\n\n\tconst supportsReasoning = optionalBoolean(item.supports_reasoning);\n\tif (supportsReasoning !== undefined) parsed.supports_reasoning = supportsReasoning;\n\n\tconst supportsImageIn = optionalBoolean(item.supports_image_in);\n\tif (supportsImageIn !== undefined) parsed.supports_image_in = supportsImageIn;\n\n\tconst supportsVideoIn = optionalBoolean(item.supports_video_in);\n\tif (supportsVideoIn !== undefined) parsed.supports_video_in = supportsVideoIn;\n\n\tif (Object.hasOwn(item, \"supports_tool_use\")) {\n\t\tconst supportsToolUse = optionalBoolean(item.supports_tool_use);\n\t\tif (supportsToolUse !== undefined) parsed.supports_tool_use = supportsToolUse;\n\t}\n\n\tconst supportsThinkingType = parseSupportsThinkingType(item.supports_thinking_type);\n\tif (supportsThinkingType !== undefined) parsed.supports_thinking_type = supportsThinkingType;\n\n\tconst protocol = parseModelProtocol(item.protocol);\n\tif (protocol !== undefined) parsed.protocol = protocol;\n\n\tconst thinkEfforts = parseThinkEfforts(item.think_efforts);\n\tif (thinkEfforts !== undefined) parsed.think_efforts = thinkEfforts;\n\n\treturn parsed;\n}\n\ntype KimiCredentials = OAuthCredentials & {\n\t/** Full list of models discovered from the Kimi API. */\n\tmodels?: KimiModelInfo[];\n\t/** @deprecated Kept for legacy credential compatibility; derived from models[0]. */\n\tmodelId?: string;\n\t/** @deprecated Kept for legacy credential compatibility; derived from models[0]. */\n\tcontextLength?: number;\n\t/** @deprecated Kept for legacy credential compatibility; derived from models[0]. */\n\tmodelDisplay?: string;\n};\n\n// ============================================================================\n// Network helpers\n// ============================================================================\n\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nfunction requestSignal(signal?: AbortSignal): AbortSignal {\n\tconst timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n\treturn signal ? AbortSignal.any([signal, timeout]) : timeout;\n}\n\nasync function fetchJson(url: string, init: RequestInit): Promise<unknown> {\n\tconst response = await fetch(url, init);\n\tif (!response.ok) {\n\t\tconst text = await response.text();\n\t\tthrow new Error(`${response.status} ${response.statusText}: ${text}`);\n\t}\n\treturn response.json();\n}\n\n/**\n * Sleep that can be interrupted by an AbortSignal.\n */\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Login cancelled\"));\n\t\t\treturn;\n\t\t}\n\n\t\tconst timeout = setTimeout(resolve, ms);\n\n\t\tsignal?.addEventListener(\n\t\t\t\"abort\",\n\t\t\t() => {\n\t\t\t\tclearTimeout(timeout);\n\t\t\t\treject(new Error(\"Login cancelled\"));\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t});\n}\n\n// ============================================================================\n// Model discovery\n// ============================================================================\n\n/**\n * List available models from the Kimi API.\n * Returns the model info array from the response's `data` field.\n */\nexport async function listModels(accessToken: string, signal?: AbortSignal): Promise<KimiModelInfo[]> {\n\tconst raw = await fetchJson(`${API_BASE_URL}/models`, {\n\t\tsignal: requestSignal(signal),\n\t\theaders: {\n\t\t\t...buildKimiHeaders(),\n\t\t\tAuthorization: `Bearer ${accessToken}`,\n\t\t},\n\t});\n\n\tif (!raw || typeof raw !== \"object\") {\n\t\tthrow new Error(\"Invalid models response\");\n\t}\n\n\tconst data = (raw as Record<string, unknown>).data;\n\tif (!Array.isArray(data)) {\n\t\tthrow new Error(\"Invalid models response: expected data array\");\n\t}\n\n\treturn data.map((item) => toKimiModelInfo(item)).filter((item): item is KimiModelInfo => item !== undefined);\n}\n\n// ============================================================================\n// Device flow\n// ============================================================================\n\nasync function startDeviceFlow(signal?: AbortSignal): Promise<DeviceCodeResponse> {\n\tconst data = await fetchJson(OAUTH_DEVICE_AUTH_URL, {\n\t\tmethod: \"POST\",\n\t\tsignal: requestSignal(signal),\n\t\theaders: {\n\t\t\t...buildKimiHeaders(),\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t},\n\t\tbody: new URLSearchParams({ client_id: OAUTH_CLIENT_ID }),\n\t});\n\n\tif (!data || typeof data !== \"object\") {\n\t\tthrow new Error(\"Invalid device code response\");\n\t}\n\n\tconst d = data as Record<string, unknown>;\n\tconst device_code = d.device_code;\n\tconst user_code = d.user_code;\n\tconst verification_uri_complete =\n\t\ttypeof d.verification_uri_complete === \"string\" && d.verification_uri_complete.length > 0\n\t\t\t? d.verification_uri_complete\n\t\t\t: typeof d.verification_uri === \"string\" && d.verification_uri.length > 0\n\t\t\t\t? `${d.verification_uri}${d.verification_uri.includes(\"?\") ? \"&\" : \"?\"}user_code=${encodeURIComponent(user_code as string)}`\n\t\t\t\t: undefined;\n\tconst interval = d.interval;\n\tconst expires_in = d.expires_in;\n\n\tif (\n\t\ttypeof device_code !== \"string\" ||\n\t\ttypeof user_code !== \"string\" ||\n\t\ttypeof verification_uri_complete !== \"string\" ||\n\t\ttypeof interval !== \"number\" ||\n\t\ttypeof expires_in !== \"number\"\n\t) {\n\t\tthrow new Error(\"Invalid device code response fields\");\n\t}\n\n\treturn { device_code, user_code, verification_uri_complete, interval, expires_in };\n}\n\nclass DeviceCodeExpiredError extends Error {\n\tconstructor() {\n\t\tsuper(\"Device code expired\");\n\t\tthis.name = \"DeviceCodeExpiredError\";\n\t}\n}\n\nasync function pollForAccessToken(\n\tdeviceCode: string,\n\tintervalSeconds: number,\n\texpiresIn: number,\n\tsignal?: AbortSignal,\n\toverallDeadline = Date.now() + MAX_DEVICE_FLOW_MS,\n): Promise<TokenSuccessResponse> {\n\tconst deadline = Math.min(Date.now() + expiresIn * 1000, overallDeadline);\n\tlet intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));\n\n\twhile (Date.now() < deadline) {\n\t\tif (signal?.aborted) {\n\t\t\tthrow new Error(\"Login cancelled\");\n\t\t}\n\n\t\tconst remainingMs = deadline - Date.now();\n\t\tconst waitMs = Math.min(intervalMs, remainingMs);\n\t\tawait abortableSleep(waitMs, signal);\n\n\t\tconst tokenResponse = await fetch(OAUTH_TOKEN_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\tsignal: requestSignal(signal),\n\t\t\theaders: {\n\t\t\t\t...buildKimiHeaders(),\n\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t},\n\t\t\tbody: new URLSearchParams({\n\t\t\t\tclient_id: OAUTH_CLIENT_ID,\n\t\t\t\tdevice_code: deviceCode,\n\t\t\t\tgrant_type: OAUTH_DEVICE_GRANT,\n\t\t\t}),\n\t\t});\n\n\t\t// The token endpoint returns 400 for authorization_pending / slow_down / expired_token.\n\t\t// We must read the body regardless of status to handle the OAuth error codes.\n\t\tconst resp = (await tokenResponse.json()) as Record<string, unknown>;\n\n\t\t// Success: has access_token\n\t\tif (typeof resp.access_token === \"string\") {\n\t\t\treturn parseTokenSuccess(resp);\n\t\t}\n\n\t\t// Error response (RFC 8628 §3.5)\n\t\tif (typeof resp.error === \"string\") {\n\t\t\tconst error = resp.error;\n\t\t\tconst description = resp.error_description as string | undefined;\n\t\t\tconst newInterval = resp.interval as number | undefined;\n\n\t\t\tif (error === \"authorization_pending\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (error === \"slow_down\") {\n\t\t\t\tintervalMs =\n\t\t\t\t\ttypeof newInterval === \"number\" && newInterval > 0\n\t\t\t\t\t\t? newInterval * 1000\n\t\t\t\t\t\t: Math.max(1000, intervalMs + 5000);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (error === \"expired_token\") {\n\t\t\t\tthrow new DeviceCodeExpiredError();\n\t\t\t}\n\n\t\t\tconst descriptionSuffix = description ? `: ${description}` : \"\";\n\t\t\tthrow new Error(`Device flow failed: ${error}${descriptionSuffix}`);\n\t\t}\n\n\t\t// Unexpected response: valid object but no access_token or error field\n\t\tthrow new Error(`Unexpected token response: ${JSON.stringify(resp)}`);\n\t}\n\n\tthrow new Error(\"Device flow timed out\");\n}\n\n// ============================================================================\n// Refresh with retry\n// ============================================================================\n\nconst RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];\n\nclass RetriableError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"RetriableError\";\n\t}\n}\n\n/**\n * Heuristic to detect network-level errors that should be retried.\n * Fetch throws TypeError on network failures; some runtimes include\n * recognizable substrings in the message.\n */\nfunction isNetworkError(error: Error): boolean {\n\tif (error instanceof TypeError || error.name === \"TimeoutError\") return true;\n\tconst msg = error.message.toLowerCase();\n\treturn [\"fetch failed\", \"econnrefused\", \"etimedout\", \"enotfound\", \"econnreset\", \"socket hang up\"].some((s) =>\n\t\tmsg.includes(s),\n\t);\n}\n\nasync function refreshWithRetry(refreshToken: string, signal?: AbortSignal): Promise<TokenSuccessResponse> {\n\tlet lastError: Error | undefined;\n\n\tfor (let attempt = 0; attempt < MAX_REFRESH_RETRIES; attempt++) {\n\t\tif (signal?.aborted) {\n\t\t\tthrow new Error(\"Refresh cancelled\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst response = await fetch(OAUTH_TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tsignal: requestSignal(signal),\n\t\t\t\theaders: {\n\t\t\t\t\t...buildKimiHeaders(),\n\t\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tclient_id: OAUTH_CLIENT_ID,\n\t\t\t\t\trefresh_token: refreshToken,\n\t\t\t\t\tgrant_type: OAUTH_REFRESH_GRANT,\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Retry on retriable status codes\n\t\t\tif (RETRYABLE_STATUS_CODES.includes(response.status)) {\n\t\t\t\tthrow new RetriableError(`Token refresh failed with status ${response.status}`);\n\t\t\t}\n\n\t\t\tif (!response.ok) {\n\t\t\t\tconst text = await response.text();\n\t\t\t\tthrow new Error(`Token refresh failed: ${response.status} ${response.statusText}: ${text}`);\n\t\t\t}\n\n\t\t\tconst raw = await response.json();\n\t\t\tif (!raw || typeof raw !== \"object\") throw new Error(\"Invalid token refresh response\");\n\t\t\treturn parseTokenSuccess(raw as Record<string, unknown>);\n\t\t} catch (error) {\n\t\t\tif (signal?.aborted) throw new Error(\"Refresh cancelled\");\n\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\n\t\t\t// Wrap network errors (TypeError from fetch, or common network failure indicators) as retriable\n\t\t\tif (!(lastError instanceof RetriableError) && isNetworkError(lastError)) {\n\t\t\t\tlastError = new RetriableError(lastError.message);\n\t\t\t}\n\n\t\t\t// Retry on retriable errors (network failures or retriable HTTP status codes)\n\t\t\tif (lastError instanceof RetriableError && attempt < MAX_REFRESH_RETRIES - 1) {\n\t\t\t\tconst backoffMs = Math.min(1000 * 2 ** attempt, 10000);\n\t\t\t\tawait abortableSleep(backoffMs, signal);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tthrow lastError;\n\t\t}\n\t}\n\n\tthrow lastError ?? new Error(\"Token refresh failed after retries\");\n}\n\n// ============================================================================\n// Login flow\n// ============================================================================\n\nexport async function loginKimiCoding(options: {\n\tonAuth: (info: { url: string; instructions?: string }) => void;\n\tonProgress?: (message: string) => void;\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst overallDeadline = Date.now() + MAX_DEVICE_FLOW_MS;\n\tlet tokenResp: TokenSuccessResponse;\n\twhile (true) {\n\t\tconst device = await startDeviceFlow(options.signal);\n\t\toptions.onAuth({\n\t\t\turl: device.verification_uri_complete,\n\t\t\tinstructions: `Enter code: ${device.user_code}`,\n\t\t});\n\n\t\ttry {\n\t\t\ttokenResp = await pollForAccessToken(\n\t\t\t\tdevice.device_code,\n\t\t\t\tdevice.interval,\n\t\t\t\tdevice.expires_in,\n\t\t\t\toptions.signal,\n\t\t\t\toverallDeadline,\n\t\t\t);\n\t\t\tbreak;\n\t\t} catch (error) {\n\t\t\tif (error instanceof DeviceCodeExpiredError && Date.now() < overallDeadline) continue;\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t// Discover model entitlement. Undefined means discovery failed; an empty\n\t// array is a successful, authoritative response.\n\toptions.onProgress?.(\"Discovering available models...\");\n\tlet models: KimiModelInfo[] | undefined;\n\ttry {\n\t\tmodels = await listModels(tokenResp.access_token, options.signal);\n\t} catch {\n\t\tif (options.signal?.aborted) throw new Error(\"Login cancelled\");\n\t\t// Proceed without model enrichment if the models endpoint fails\n\t}\n\n\tconst credentials: KimiCredentials = {\n\t\trefresh: tokenResp.refresh_token,\n\t\taccess: tokenResp.access_token,\n\t\texpires: Date.now() + tokenResp.expires_in * 1000,\n\t};\n\n\tif (models) {\n\t\tcredentials.models = models;\n\t\tconst primary = models[0];\n\t\tif (primary) {\n\t\t\tcredentials.modelId = primary.id;\n\t\t\tcredentials.contextLength = primary.context_length;\n\t\t\tcredentials.modelDisplay = primary.display_name;\n\t\t}\n\t}\n\n\treturn credentials;\n}\n\n// ============================================================================\n// Refresh\n// ============================================================================\n\nexport async function refreshKimiCodingToken(\n\tcredentials: OAuthCredentials,\n\tsignal?: AbortSignal,\n): Promise<OAuthCredentials> {\n\tconst tokenResp = await refreshWithRetry(credentials.refresh, signal);\n\n\t// Re-discover model entitlement. Undefined means discovery failed; an empty\n\t// array is a successful, authoritative response.\n\tlet models: KimiModelInfo[] | undefined;\n\ttry {\n\t\tmodels = await listModels(tokenResp.access_token, signal);\n\t} catch {\n\t\tif (signal?.aborted) throw new Error(\"Refresh cancelled\");\n\t\t// Proceed without model enrichment if the models endpoint fails\n\t}\n\n\tconst oldCreds = credentials as KimiCredentials;\n\tconst fresh: KimiCredentials = {\n\t\trefresh: tokenResp.refresh_token ?? credentials.refresh,\n\t\taccess: tokenResp.access_token,\n\t\texpires: Date.now() + tokenResp.expires_in * 1000,\n\t\tmodels: oldCreds.models,\n\t\tmodelId: oldCreds.modelId,\n\t\tcontextLength: oldCreds.contextLength,\n\t\tmodelDisplay: oldCreds.modelDisplay,\n\t};\n\n\tif (models) {\n\t\tfresh.models = models;\n\t\tconst primary = models[0];\n\t\tfresh.modelId = primary?.id;\n\t\tfresh.contextLength = primary?.context_length;\n\t\tfresh.modelDisplay = primary?.display_name;\n\t}\n\n\treturn fresh;\n}\n\n// ============================================================================\n// Provider\n// ============================================================================\n\nexport const kimiCodingOAuthProvider: OAuthProviderInterface = {\n\tid: \"kimi-coding-oauth\",\n\tname: \"Kimi For Coding\",\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginKimiCoding({\n\t\t\tonAuth: callbacks.onAuth,\n\t\t\tonProgress: callbacks.onProgress,\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\treturn refreshKimiCodingToken(credentials);\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst creds = credentials as KimiCredentials;\n\t\tconst headers = buildKimiHeaders();\n\n\t\tconst staticModels = models.filter((m) => m.provider === \"kimi-coding-oauth\");\n\t\tif (staticModels.length === 0) {\n\t\t\treturn models;\n\t\t}\n\n\t\tconst injectHeaders = (m: Model<Api>): Model<Api> => ({\n\t\t\t...m,\n\t\t\theaders: { ...(m.headers || {}), ...headers },\n\t\t});\n\n\t\tconst staticById = new Map(staticModels.map((m) => [m.id, m]));\n\t\tconst fallbackTemplate = staticById.get(\"kimi-for-coding\") ?? staticModels[0];\n\n\t\t// No discovery (or failed discovery): keep the static fallback list intact.\n\t\t// Legacy credentials describe one discovered model, so enrich or append only\n\t\t// that model rather than collapsing every static entry to the same ID.\n\t\tconst discovered = creds.models;\n\t\tif (!discovered) {\n\t\t\tconst fallbackModels = models.map((m) => {\n\t\t\t\tif (m.provider !== \"kimi-coding-oauth\") return m;\n\n\t\t\t\tconst updated: Model<Api> = {\n\t\t\t\t\t...m,\n\t\t\t\t\t// The OAuth coding endpoint accepts OpenAI-style image_url data URLs;\n\t\t\t\t\t// keep this capability even if static metadata is stale.\n\t\t\t\t\tinput: Array.from(new Set([...m.input, \"image\" as const])),\n\t\t\t\t};\n\t\t\t\tif (creds.modelId === m.id && creds.contextLength) {\n\t\t\t\t\tupdated.contextWindow = creds.contextLength;\n\t\t\t\t}\n\t\t\t\tif (creds.modelId === m.id && creds.modelDisplay) {\n\t\t\t\t\tupdated.name = creds.modelDisplay;\n\t\t\t\t}\n\t\t\t\treturn injectHeaders(updated);\n\t\t\t});\n\n\t\t\tif (creds.modelId && !staticById.has(creds.modelId)) {\n\t\t\t\tfallbackModels.push(\n\t\t\t\t\tinjectHeaders({\n\t\t\t\t\t\t...fallbackTemplate,\n\t\t\t\t\t\tid: creds.modelId,\n\t\t\t\t\t\tname: creds.modelDisplay || creds.modelId,\n\t\t\t\t\t\tcontextWindow: creds.contextLength || fallbackTemplate.contextWindow,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn fallbackModels;\n\t\t}\n\n\t\tconst discoveredInput = (base: Model<Api>, info: KimiModelInfo): Model<Api>[\"input\"] => {\n\t\t\tif (info.supports_image_in === true) return Array.from(new Set([...base.input, \"image\" as const]));\n\t\t\tif (info.supports_image_in === false) return base.input.filter((input) => input !== \"image\");\n\t\t\treturn base.input;\n\t\t};\n\n\t\tconst discoveredCompat = (base: Model<Api>, info: KimiModelInfo): Model<Api>[\"compat\"] => {\n\t\t\tconst valid = info.think_efforts?.support ? info.think_efforts.valid_efforts : undefined;\n\t\t\tif (!valid || valid.length === 0) return base.compat;\n\t\t\tconst efforts = new Set(valid);\n\t\t\tconst declaredDefault = info.think_efforts?.default_effort;\n\t\t\tconst validDefault = declaredDefault && efforts.has(declaredDefault) ? declaredDefault : undefined;\n\t\t\tconst choose = (...preferences: string[]): string =>\n\t\t\t\tpreferences.find((effort) => efforts.has(effort)) ?? validDefault ?? valid[0];\n\t\t\treturn {\n\t\t\t\t...base.compat,\n\t\t\t\treasoningEffortMap: {\n\t\t\t\t\tminimal: choose(\"minimal\", \"low\", \"medium\", \"high\", \"max\"),\n\t\t\t\t\tlow: choose(\"low\", \"minimal\", \"medium\", \"high\", \"max\"),\n\t\t\t\t\tmedium: choose(\"medium\", \"high\", \"low\", \"max\"),\n\t\t\t\t\thigh: choose(\"high\", \"medium\", \"max\", \"low\"),\n\t\t\t\t\txhigh: choose(\"max\", \"xhigh\", \"high\", \"medium\", \"low\"),\n\t\t\t\t},\n\t\t\t};\n\t\t};\n\n\t\t// Apply discovered metadata to a static model, preserving its static shape.\n\t\tconst applyDiscovery = (staticModel: Model<Api>, info: KimiModelInfo): Model<Api> => {\n\t\t\tconst updated: Model<Api> = {\n\t\t\t\t...staticModel,\n\t\t\t\tid: info.id,\n\t\t\t\tname: info.display_name || info.id,\n\t\t\t\tcontextWindow: info.context_length || staticModel.contextWindow,\n\t\t\t\tinput: discoveredInput(staticModel, info),\n\t\t\t\tcompat: discoveredCompat(staticModel, info),\n\t\t\t};\n\t\t\tif (info.supports_thinking_type === \"only\" || info.supports_thinking_type === \"both\") {\n\t\t\t\tupdated.reasoning = true;\n\t\t\t} else if (info.supports_thinking_type === \"no\") {\n\t\t\t\tupdated.reasoning = false;\n\t\t\t} else if (typeof info.supports_reasoning === \"boolean\") {\n\t\t\t\tupdated.reasoning = info.supports_reasoning;\n\t\t\t}\n\t\t\treturn injectHeaders(updated);\n\t\t};\n\n\t\t// Safely template a future/discovered model ID using the static fallback.\n\t\tconst templateDiscovery = (info: KimiModelInfo): Model<Api> => {\n\t\t\tconst templated: Model<Api> = {\n\t\t\t\t...fallbackTemplate,\n\t\t\t\tid: info.id,\n\t\t\t\tname: info.display_name || info.id,\n\t\t\t\tcontextWindow: info.context_length || fallbackTemplate.contextWindow,\n\t\t\t\treasoning:\n\t\t\t\t\tinfo.supports_thinking_type === undefined\n\t\t\t\t\t\t? (info.supports_reasoning ?? false)\n\t\t\t\t\t\t: info.supports_thinking_type !== \"no\",\n\t\t\t\tinput: info.supports_image_in === true ? [\"text\", \"image\"] : [\"text\"],\n\t\t\t\tcompat: discoveredCompat(fallbackTemplate, info),\n\t\t\t};\n\t\t\treturn injectHeaders(templated);\n\t\t};\n\n\t\t// The official client treats only the explicit \"anthropic\" protocol as a\n\t\t// separate wire format; absent and future values use the default Kimi route.\n\t\tconst supportedDiscovered = discovered.filter(\n\t\t\t(info) => info.supports_tool_use !== false && info.protocol !== \"anthropic\",\n\t\t);\n\t\tconst result: Model<Api>[] = [];\n\t\tconst seen = new Set<string>();\n\n\t\t// 1. Walk the original model list to preserve order, replacing discovered\n\t\t// static models and dropping undiscovered entries. A successful response\n\t\t// is authoritative for the subscription's current entitlements.\n\t\tfor (const m of models) {\n\t\t\tif (m.provider !== \"kimi-coding-oauth\") {\n\t\t\t\tresult.push(m);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst info = staticById.has(m.id) ? supportedDiscovered.find((d) => d.id === m.id) : undefined;\n\t\t\tif (info) {\n\t\t\t\tresult.push(applyDiscovery(m, info));\n\t\t\t\tseen.add(info.id);\n\t\t\t}\n\t\t}\n\n\t\t// 2. Discovered models with IDs not present in the static list are templated\n\t\t// from the fallback so future model IDs are safely usable.\n\t\tfor (const info of supportedDiscovered) {\n\t\t\tif (!seen.has(info.id)) {\n\t\t\t\tresult.push(templateDiscovery(info));\n\t\t\t\tseen.add(info.id);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t},\n};\n"]}
|