{"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,sBA8KrC,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\t// `k3-256k` is the cheaper 256k wire variant of `k3`; dreb surfaces both\n\t\t// through the single user-facing `k3` model (auto context tier), so it\n\t\t// must not appear as a separate selectable model.\n\t\tconst supportedDiscovered = discovered.filter(\n\t\t\t(info) => info.supports_tool_use !== false && info.protocol !== \"anthropic\" && info.id !== \"k3-256k\",\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"]}