@earendil-works/pi-ai 0.80.2 → 0.80.5

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.
Files changed (176) hide show
  1. package/README.md +1 -1
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +13 -4
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/azure-openai-responses.d.ts.map +1 -1
  6. package/dist/api/azure-openai-responses.js +14 -18
  7. package/dist/api/azure-openai-responses.js.map +1 -1
  8. package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  9. package/dist/api/bedrock-converse-stream.js +21 -9
  10. package/dist/api/bedrock-converse-stream.js.map +1 -1
  11. package/dist/api/google-generative-ai.d.ts.map +1 -1
  12. package/dist/api/google-generative-ai.js +4 -2
  13. package/dist/api/google-generative-ai.js.map +1 -1
  14. package/dist/api/google-vertex.d.ts.map +1 -1
  15. package/dist/api/google-vertex.js +4 -2
  16. package/dist/api/google-vertex.js.map +1 -1
  17. package/dist/api/mistral-conversations.d.ts.map +1 -1
  18. package/dist/api/mistral-conversations.js +1 -1
  19. package/dist/api/mistral-conversations.js.map +1 -1
  20. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  21. package/dist/api/openai-codex-responses.js +62 -42
  22. package/dist/api/openai-codex-responses.js.map +1 -1
  23. package/dist/api/openai-completions.d.ts.map +1 -1
  24. package/dist/api/openai-completions.js +12 -5
  25. package/dist/api/openai-completions.js.map +1 -1
  26. package/dist/api/openai-responses-shared.d.ts.map +1 -1
  27. package/dist/api/openai-responses-shared.js +153 -176
  28. package/dist/api/openai-responses-shared.js.map +1 -1
  29. package/dist/api/openai-responses.d.ts.map +1 -1
  30. package/dist/api/openai-responses.js +6 -16
  31. package/dist/api/openai-responses.js.map +1 -1
  32. package/dist/api/openrouter-images.d.ts.map +1 -1
  33. package/dist/api/openrouter-images.js +2 -1
  34. package/dist/api/openrouter-images.js.map +1 -1
  35. package/dist/api/simple-options.d.ts +3 -2
  36. package/dist/api/simple-options.d.ts.map +1 -1
  37. package/dist/api/simple-options.js +11 -2
  38. package/dist/api/simple-options.js.map +1 -1
  39. package/dist/api/transform-messages.d.ts.map +1 -1
  40. package/dist/api/transform-messages.js +4 -1
  41. package/dist/api/transform-messages.js.map +1 -1
  42. package/dist/image-models.generated.d.ts +34 -19
  43. package/dist/image-models.generated.d.ts.map +1 -1
  44. package/dist/image-models.generated.js +60 -45
  45. package/dist/image-models.generated.js.map +1 -1
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/models.generated.d.ts +1754 -498
  51. package/dist/models.generated.d.ts.map +1 -1
  52. package/dist/providers/amazon-bedrock.models.d.ts +157 -0
  53. package/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
  54. package/dist/providers/amazon-bedrock.models.js +166 -12
  55. package/dist/providers/amazon-bedrock.models.js.map +1 -1
  56. package/dist/providers/anthropic.models.d.ts +6 -190
  57. package/dist/providers/anthropic.models.d.ts.map +1 -1
  58. package/dist/providers/anthropic.models.js +21 -207
  59. package/dist/providers/anthropic.models.js.map +1 -1
  60. package/dist/providers/azure-openai-responses.models.d.ts +84 -0
  61. package/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
  62. package/dist/providers/azure-openai-responses.models.js +72 -0
  63. package/dist/providers/azure-openai-responses.models.js.map +1 -1
  64. package/dist/providers/cerebras.models.d.ts +21 -0
  65. package/dist/providers/cerebras.models.d.ts.map +1 -1
  66. package/dist/providers/cerebras.models.js +18 -0
  67. package/dist/providers/cerebras.models.js.map +1 -1
  68. package/dist/providers/cloudflare-ai-gateway.models.d.ts +21 -0
  69. package/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
  70. package/dist/providers/cloudflare-ai-gateway.models.js +18 -0
  71. package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
  72. package/dist/providers/fireworks.models.d.ts +28 -0
  73. package/dist/providers/fireworks.models.d.ts.map +1 -1
  74. package/dist/providers/fireworks.models.js +21 -2
  75. package/dist/providers/fireworks.models.js.map +1 -1
  76. package/dist/providers/github-copilot.models.d.ts +82 -0
  77. package/dist/providers/github-copilot.models.d.ts.map +1 -1
  78. package/dist/providers/github-copilot.models.js +62 -5
  79. package/dist/providers/github-copilot.models.js.map +1 -1
  80. package/dist/providers/groq.models.d.ts.map +1 -1
  81. package/dist/providers/groq.models.js +1 -1
  82. package/dist/providers/groq.models.js.map +1 -1
  83. package/dist/providers/huggingface.models.d.ts +100 -0
  84. package/dist/providers/huggingface.models.d.ts.map +1 -1
  85. package/dist/providers/huggingface.models.js +90 -0
  86. package/dist/providers/huggingface.models.js.map +1 -1
  87. package/dist/providers/minimax-cn.models.d.ts.map +1 -1
  88. package/dist/providers/minimax-cn.models.js +4 -4
  89. package/dist/providers/minimax-cn.models.js.map +1 -1
  90. package/dist/providers/minimax.models.d.ts.map +1 -1
  91. package/dist/providers/minimax.models.js +4 -4
  92. package/dist/providers/minimax.models.js.map +1 -1
  93. package/dist/providers/mistral.models.d.ts +1 -1
  94. package/dist/providers/mistral.models.d.ts.map +1 -1
  95. package/dist/providers/mistral.models.js +4 -4
  96. package/dist/providers/mistral.models.js.map +1 -1
  97. package/dist/providers/nvidia.models.d.ts +29 -1
  98. package/dist/providers/nvidia.models.d.ts.map +1 -1
  99. package/dist/providers/nvidia.models.js +23 -4
  100. package/dist/providers/nvidia.models.js.map +1 -1
  101. package/dist/providers/openai-codex.models.d.ts +63 -0
  102. package/dist/providers/openai-codex.models.d.ts.map +1 -1
  103. package/dist/providers/openai-codex.models.js +54 -0
  104. package/dist/providers/openai-codex.models.js.map +1 -1
  105. package/dist/providers/openai.models.d.ts +84 -0
  106. package/dist/providers/openai.models.d.ts.map +1 -1
  107. package/dist/providers/openai.models.js +72 -0
  108. package/dist/providers/openai.models.js.map +1 -1
  109. package/dist/providers/opencode-go.models.d.ts.map +1 -1
  110. package/dist/providers/opencode-go.models.js +6 -6
  111. package/dist/providers/opencode-go.models.js.map +1 -1
  112. package/dist/providers/opencode.models.d.ts +132 -0
  113. package/dist/providers/opencode.models.d.ts.map +1 -1
  114. package/dist/providers/opencode.models.js +111 -2
  115. package/dist/providers/opencode.models.js.map +1 -1
  116. package/dist/providers/openrouter.models.d.ts +358 -55
  117. package/dist/providers/openrouter.models.d.ts.map +1 -1
  118. package/dist/providers/openrouter.models.js +395 -138
  119. package/dist/providers/openrouter.models.js.map +1 -1
  120. package/dist/providers/together.models.d.ts +31 -0
  121. package/dist/providers/together.models.d.ts.map +1 -1
  122. package/dist/providers/together.models.js +21 -2
  123. package/dist/providers/together.models.js.map +1 -1
  124. package/dist/providers/vercel-ai-gateway.models.d.ts +446 -36
  125. package/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
  126. package/dist/providers/vercel-ai-gateway.models.js +477 -80
  127. package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  128. package/dist/providers/xai.models.d.ts +22 -0
  129. package/dist/providers/xai.models.d.ts.map +1 -1
  130. package/dist/providers/xai.models.js +18 -0
  131. package/dist/providers/xai.models.js.map +1 -1
  132. package/dist/providers/xiaomi-token-plan-ams.models.d.ts +0 -42
  133. package/dist/providers/xiaomi-token-plan-ams.models.d.ts.map +1 -1
  134. package/dist/providers/xiaomi-token-plan-ams.models.js +9 -45
  135. package/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
  136. package/dist/providers/xiaomi-token-plan-cn.models.d.ts +0 -42
  137. package/dist/providers/xiaomi-token-plan-cn.models.d.ts.map +1 -1
  138. package/dist/providers/xiaomi-token-plan-cn.models.js +9 -45
  139. package/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
  140. package/dist/providers/xiaomi-token-plan-sgp.models.d.ts +0 -42
  141. package/dist/providers/xiaomi-token-plan-sgp.models.d.ts.map +1 -1
  142. package/dist/providers/xiaomi-token-plan-sgp.models.js +9 -45
  143. package/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
  144. package/dist/providers/xiaomi.models.d.ts.map +1 -1
  145. package/dist/providers/xiaomi.models.js +15 -15
  146. package/dist/providers/xiaomi.models.js.map +1 -1
  147. package/dist/types.d.ts +6 -0
  148. package/dist/types.d.ts.map +1 -1
  149. package/dist/types.js.map +1 -1
  150. package/dist/utils/error-body.d.ts +25 -0
  151. package/dist/utils/error-body.d.ts.map +1 -0
  152. package/dist/utils/error-body.js +109 -0
  153. package/dist/utils/error-body.js.map +1 -0
  154. package/dist/utils/estimate.d.ts +17 -0
  155. package/dist/utils/estimate.d.ts.map +1 -0
  156. package/dist/utils/estimate.js +95 -0
  157. package/dist/utils/estimate.js.map +1 -0
  158. package/dist/utils/oauth/device-code.d.ts +2 -0
  159. package/dist/utils/oauth/device-code.d.ts.map +1 -1
  160. package/dist/utils/oauth/device-code.js +15 -2
  161. package/dist/utils/oauth/device-code.js.map +1 -1
  162. package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  163. package/dist/utils/oauth/github-copilot.js +3 -2
  164. package/dist/utils/oauth/github-copilot.js.map +1 -1
  165. package/dist/utils/overflow.d.ts +1 -0
  166. package/dist/utils/overflow.d.ts.map +1 -1
  167. package/dist/utils/overflow.js +3 -0
  168. package/dist/utils/overflow.js.map +1 -1
  169. package/dist/utils/retry.d.ts +12 -0
  170. package/dist/utils/retry.d.ts.map +1 -0
  171. package/dist/utils/retry.js +90 -0
  172. package/dist/utils/retry.js.map +1 -0
  173. package/dist/utils/validation.d.ts.map +1 -1
  174. package/dist/utils/validation.js +9 -21
  175. package/dist/utils/validation.js.map +1 -1
  176. package/package.json +1 -1
@@ -0,0 +1,109 @@
1
+ // Shared normalization for provider HTTP error objects.
2
+ //
3
+ // Endpoints behind a proxy / gateway may return a non-2xx response whose body
4
+ // the provider SDK cannot fold into `error.message`. The SDK error object still
5
+ // carries the HTTP status and the raw/parsed body, but under SDK-specific field
6
+ // names. Provider catch blocks that read only `error.message` therefore drop
7
+ // the body and surface opaque messages like `"403 status code (no body)"` or
8
+ // collapse to `"Unknown: UnknownError"`.
9
+ //
10
+ // `normalizeProviderError` probes the known SDK field shapes (Mistral,
11
+ // `openai`, `@google/genai`, AWS Bedrock) and returns a struct each provider
12
+ // composes into its display string. The `messageCarriesBody` flag captures the
13
+ // Anthropic / `@google/genai` happy path where the SDK already folded the body
14
+ // into the message, so providers can preserve it without double-printing.
15
+ export const MAX_PROVIDER_ERROR_BODY_CHARS = 4000;
16
+ export function normalizeProviderError(error) {
17
+ if (!(error instanceof Error)) {
18
+ return { message: safeJsonStringify(error), messageCarriesBody: false };
19
+ }
20
+ const sdkError = error;
21
+ const status = extractStatus(sdkError);
22
+ const body = extractBody(sdkError);
23
+ const messageCarriesBody = body === undefined || error.message.includes(body);
24
+ return {
25
+ status,
26
+ body,
27
+ message: error.message,
28
+ messageCarriesBody,
29
+ };
30
+ }
31
+ /**
32
+ * Probe the HTTP status, first numeric hit wins, in SDK-field order:
33
+ * `statusCode` (Mistral) → `status` (`openai`, `@google/genai`) →
34
+ * `$metadata.httpStatusCode` (Bedrock) → `$response.statusCode` (Bedrock).
35
+ */
36
+ function extractStatus(error) {
37
+ if (typeof error.statusCode === "number")
38
+ return error.statusCode;
39
+ if (typeof error.status === "number")
40
+ return error.status;
41
+ if (typeof error.$metadata?.httpStatusCode === "number")
42
+ return error.$metadata.httpStatusCode;
43
+ if (typeof error.$response?.statusCode === "number")
44
+ return error.$response.statusCode;
45
+ return undefined;
46
+ }
47
+ /**
48
+ * Probe the raw body reason, first usable hit wins, in SDK-field order:
49
+ * `body` string (Mistral) → `error` parsed JSON body object (`openai` SDK's
50
+ * `this.error`) → `$response.body` (Bedrock). Empty objects are treated as no
51
+ * body so an empty parsed body does not surface as `"{}"`. The chosen body is
52
+ * truncated to the cap.
53
+ */
54
+ function extractBody(error) {
55
+ const bodyText = pickBodyText(error);
56
+ if (bodyText === undefined)
57
+ return undefined;
58
+ const trimmed = bodyText.trim();
59
+ if (trimmed.length === 0)
60
+ return undefined;
61
+ return truncateErrorText(trimmed, MAX_PROVIDER_ERROR_BODY_CHARS);
62
+ }
63
+ function pickBodyText(error) {
64
+ if (typeof error.body === "string")
65
+ return error.body;
66
+ if (isNonEmptyObject(error.error))
67
+ return safeJsonStringify(error.error);
68
+ const responseBody = error.$response?.body;
69
+ if (typeof responseBody === "string")
70
+ return responseBody;
71
+ if (isNonEmptyObject(responseBody))
72
+ return safeJsonStringify(responseBody);
73
+ return undefined;
74
+ }
75
+ function isNonEmptyObject(value) {
76
+ return typeof value === "object" && value !== null && Object.keys(value).length > 0;
77
+ }
78
+ /**
79
+ * Compose a display string from a normalized error. When the message already
80
+ * carries the body (Anthropic / `@google/genai` happy path) or no body/status
81
+ * was extracted, the message is returned unchanged. Otherwise the status and
82
+ * body are surfaced, with an optional provider prefix.
83
+ *
84
+ * - no prefix: `"<status>: <body>"`
85
+ * - prefix: `"<prefix> (<status>): <body>"`
86
+ */
87
+ export function formatProviderError(norm, prefix) {
88
+ if (norm.messageCarriesBody || norm.status === undefined || norm.body === undefined) {
89
+ return prefix !== undefined && norm.status !== undefined
90
+ ? `${prefix} (${norm.status}): ${norm.message}`
91
+ : norm.message;
92
+ }
93
+ return prefix !== undefined ? `${prefix} (${norm.status}): ${norm.body}` : `${norm.status}: ${norm.body}`;
94
+ }
95
+ export function truncateErrorText(text, maxChars) {
96
+ if (text.length <= maxChars)
97
+ return text;
98
+ return `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`;
99
+ }
100
+ export function safeJsonStringify(value) {
101
+ try {
102
+ const serialized = JSON.stringify(value);
103
+ return serialized === undefined ? String(value) : serialized;
104
+ }
105
+ catch {
106
+ return String(value);
107
+ }
108
+ }
109
+ //# sourceMappingURL=error-body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-body.js","sourceRoot":"","sources":["../../src/utils/error-body.ts"],"names":[],"mappings":"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","sourcesContent":["// Shared normalization for provider HTTP error objects.\n//\n// Endpoints behind a proxy / gateway may return a non-2xx response whose body\n// the provider SDK cannot fold into `error.message`. The SDK error object still\n// carries the HTTP status and the raw/parsed body, but under SDK-specific field\n// names. Provider catch blocks that read only `error.message` therefore drop\n// the body and surface opaque messages like `\"403 status code (no body)\"` or\n// collapse to `\"Unknown: UnknownError\"`.\n//\n// `normalizeProviderError` probes the known SDK field shapes (Mistral,\n// `openai`, `@google/genai`, AWS Bedrock) and returns a struct each provider\n// composes into its display string. The `messageCarriesBody` flag captures the\n// Anthropic / `@google/genai` happy path where the SDK already folded the body\n// into the message, so providers can preserve it without double-printing.\n\nexport const MAX_PROVIDER_ERROR_BODY_CHARS = 4000;\n\nexport interface NormalizedProviderError {\n\t/** HTTP status code, when one could be extracted from the SDK error object. */\n\tstatus?: number;\n\t/** Raw HTTP body reason, already trimmed and truncated to the cap. */\n\tbody?: string;\n\t/** `error.message`, or `safeJsonStringify(error)` for a non-`Error` throw. */\n\tmessage: string;\n\t/** True when `message` already contains the body (no separate body to add). */\n\tmessageCarriesBody: boolean;\n}\n\ntype SdkErrorShape = Error & {\n\tstatusCode?: unknown;\n\tstatus?: unknown;\n\tbody?: unknown;\n\terror?: unknown;\n\t$metadata?: { httpStatusCode?: unknown };\n\t$response?: { statusCode?: unknown; body?: unknown };\n};\n\nexport function normalizeProviderError(error: unknown): NormalizedProviderError {\n\tif (!(error instanceof Error)) {\n\t\treturn { message: safeJsonStringify(error), messageCarriesBody: false };\n\t}\n\n\tconst sdkError = error as SdkErrorShape;\n\tconst status = extractStatus(sdkError);\n\tconst body = extractBody(sdkError);\n\tconst messageCarriesBody = body === undefined || error.message.includes(body);\n\n\treturn {\n\t\tstatus,\n\t\tbody,\n\t\tmessage: error.message,\n\t\tmessageCarriesBody,\n\t} satisfies NormalizedProviderError;\n}\n\n/**\n * Probe the HTTP status, first numeric hit wins, in SDK-field order:\n * `statusCode` (Mistral) → `status` (`openai`, `@google/genai`) →\n * `$metadata.httpStatusCode` (Bedrock) → `$response.statusCode` (Bedrock).\n */\nfunction extractStatus(error: SdkErrorShape): number | undefined {\n\tif (typeof error.statusCode === \"number\") return error.statusCode;\n\tif (typeof error.status === \"number\") return error.status;\n\tif (typeof error.$metadata?.httpStatusCode === \"number\") return error.$metadata.httpStatusCode;\n\tif (typeof error.$response?.statusCode === \"number\") return error.$response.statusCode;\n\treturn undefined;\n}\n\n/**\n * Probe the raw body reason, first usable hit wins, in SDK-field order:\n * `body` string (Mistral) → `error` parsed JSON body object (`openai` SDK's\n * `this.error`) → `$response.body` (Bedrock). Empty objects are treated as no\n * body so an empty parsed body does not surface as `\"{}\"`. The chosen body is\n * truncated to the cap.\n */\nfunction extractBody(error: SdkErrorShape): string | undefined {\n\tconst bodyText = pickBodyText(error);\n\tif (bodyText === undefined) return undefined;\n\tconst trimmed = bodyText.trim();\n\tif (trimmed.length === 0) return undefined;\n\treturn truncateErrorText(trimmed, MAX_PROVIDER_ERROR_BODY_CHARS);\n}\n\nfunction pickBodyText(error: SdkErrorShape): string | undefined {\n\tif (typeof error.body === \"string\") return error.body;\n\tif (isNonEmptyObject(error.error)) return safeJsonStringify(error.error);\n\tconst responseBody = error.$response?.body;\n\tif (typeof responseBody === \"string\") return responseBody;\n\tif (isNonEmptyObject(responseBody)) return safeJsonStringify(responseBody);\n\treturn undefined;\n}\n\nfunction isNonEmptyObject(value: unknown): boolean {\n\treturn typeof value === \"object\" && value !== null && Object.keys(value).length > 0;\n}\n\n/**\n * Compose a display string from a normalized error. When the message already\n * carries the body (Anthropic / `@google/genai` happy path) or no body/status\n * was extracted, the message is returned unchanged. Otherwise the status and\n * body are surfaced, with an optional provider prefix.\n *\n * - no prefix: `\"<status>: <body>\"`\n * - prefix: `\"<prefix> (<status>): <body>\"`\n */\nexport function formatProviderError(norm: NormalizedProviderError, prefix?: string): string {\n\tif (norm.messageCarriesBody || norm.status === undefined || norm.body === undefined) {\n\t\treturn prefix !== undefined && norm.status !== undefined\n\t\t\t? `${prefix} (${norm.status}): ${norm.message}`\n\t\t\t: norm.message;\n\t}\n\treturn prefix !== undefined ? `${prefix} (${norm.status}): ${norm.body}` : `${norm.status}: ${norm.body}`;\n}\n\nexport function truncateErrorText(text: string, maxChars: number): string {\n\tif (text.length <= maxChars) return text;\n\treturn `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`;\n}\n\nexport function safeJsonStringify(value: unknown): string {\n\ttry {\n\t\tconst serialized = JSON.stringify(value);\n\t\treturn serialized === undefined ? String(value) : serialized;\n\t} catch {\n\t\treturn String(value);\n\t}\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import type { Context, ImageContent, Message, TextContent, Usage } from "../types.ts";
2
+ export interface ContextUsageEstimate {
3
+ /** Estimated total context tokens. */
4
+ tokens: number;
5
+ /** Tokens reported by the most recent assistant usage block. */
6
+ usageTokens: number;
7
+ /** Estimated tokens after the most recent assistant usage block. */
8
+ trailingTokens: number;
9
+ /** Index of the message that provided usage, or null when none exists. */
10
+ lastUsageIndex: number | null;
11
+ }
12
+ export declare function calculateContextTokens(usage: Usage): number;
13
+ export declare function estimateTextTokens(text: string): number;
14
+ export declare function estimateTextAndImageContentTokens(content: string | Array<TextContent | ImageContent>): number;
15
+ export declare function estimateMessageTokens(message: Message): number;
16
+ export declare function estimateContextTokens(context: Context | readonly Message[]): ContextUsageEstimate;
17
+ //# sourceMappingURL=estimate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate.d.ts","sourceRoot":"","sources":["../../src/utils/estimate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAExG,MAAM,WAAW,oBAAoB;IACpC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAKD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE3D;AAkBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,GAAG,YAAY,CAAC,GAAG,MAAM,CAE7G;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAgB9D;AAiCD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,OAAO,EAAE,GAAG,oBAAoB,CAiBjG","sourcesContent":["import type { AssistantMessage, Context, ImageContent, Message, TextContent, Usage } from \"../types.ts\";\n\nexport interface ContextUsageEstimate {\n\t/** Estimated total context tokens. */\n\ttokens: number;\n\t/** Tokens reported by the most recent assistant usage block. */\n\tusageTokens: number;\n\t/** Estimated tokens after the most recent assistant usage block. */\n\ttrailingTokens: number;\n\t/** Index of the message that provided usage, or null when none exists. */\n\tlastUsageIndex: number | null;\n}\n\nconst CHARS_PER_TOKEN = 4;\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\nfunction safeJsonStringify(value: unknown): string {\n\ttry {\n\t\treturn JSON.stringify(value) ?? \"undefined\";\n\t} catch {\n\t\treturn \"[unserializable]\";\n\t}\n}\n\nfunction estimateTextAndImageContentChars(content: string | Array<TextContent | ImageContent>): number {\n\tif (typeof content === \"string\") return content.length;\n\n\tlet chars = 0;\n\tfor (const block of content) chars += block.type === \"text\" ? block.text.length : ESTIMATED_IMAGE_CHARS;\n\treturn chars;\n}\n\nexport function estimateTextTokens(text: string): number {\n\treturn Math.ceil(text.length / CHARS_PER_TOKEN);\n}\n\nexport function estimateTextAndImageContentTokens(content: string | Array<TextContent | ImageContent>): number {\n\treturn Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);\n}\n\nexport function estimateMessageTokens(message: Message): number {\n\tlet chars = 0;\n\n\tif (message.role === \"user\") return estimateTextAndImageContentTokens(message.content);\n\tif (message.role === \"toolResult\") return estimateTextAndImageContentTokens(message.content);\n\n\tfor (const block of message.content) {\n\t\tif (block.type === \"text\") {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"thinking\") {\n\t\t\tchars += block.thinking.length;\n\t\t} else {\n\t\t\tchars += block.name.length + safeJsonStringify(block.arguments).length;\n\t\t}\n\t}\n\treturn Math.ceil(chars / CHARS_PER_TOKEN);\n}\n\nfunction getLastAssistantUsageInfo(messages: readonly Message[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tif (assistant.stopReason === \"aborted\" || assistant.stopReason === \"error\") continue;\n\t\tif (calculateContextTokens(assistant.usage) > 0) return { usage: assistant.usage, index: i };\n\t}\n\treturn undefined;\n}\n\nfunction estimateMessages(messages: readonly Message[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\tif (usageInfo) {\n\t\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\t\tlet trailingTokens = 0;\n\t\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\t\ttrailingTokens += estimateMessageTokens(messages[i]);\n\t\t}\n\t\treturn { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };\n\t}\n\n\tlet tokens = 0;\n\tfor (const message of messages) tokens += estimateMessageTokens(message);\n\treturn { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };\n}\n\nfunction isMessageArray(value: Context | readonly Message[]): value is readonly Message[] {\n\treturn Array.isArray(value);\n}\n\nexport function estimateContextTokens(context: Context | readonly Message[]): ContextUsageEstimate {\n\tif (isMessageArray(context)) return estimateMessages(context);\n\n\tconst estimate = estimateMessages(context.messages);\n\tif (estimate.lastUsageIndex !== null) return estimate;\n\n\tlet prefixTokens = context.systemPrompt ? estimateTextTokens(context.systemPrompt) : 0;\n\tif (context.tools && context.tools.length > 0) {\n\t\tprefixTokens += estimateTextTokens(safeJsonStringify(context.tools));\n\t}\n\n\treturn {\n\t\ttokens: estimate.tokens + prefixTokens,\n\t\tusageTokens: estimate.usageTokens,\n\t\ttrailingTokens: estimate.trailingTokens + prefixTokens,\n\t\tlastUsageIndex: estimate.lastUsageIndex,\n\t};\n}\n"]}
@@ -0,0 +1,95 @@
1
+ const CHARS_PER_TOKEN = 4;
2
+ const ESTIMATED_IMAGE_CHARS = 4800;
3
+ export function calculateContextTokens(usage) {
4
+ return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
5
+ }
6
+ function safeJsonStringify(value) {
7
+ try {
8
+ return JSON.stringify(value) ?? "undefined";
9
+ }
10
+ catch {
11
+ return "[unserializable]";
12
+ }
13
+ }
14
+ function estimateTextAndImageContentChars(content) {
15
+ if (typeof content === "string")
16
+ return content.length;
17
+ let chars = 0;
18
+ for (const block of content)
19
+ chars += block.type === "text" ? block.text.length : ESTIMATED_IMAGE_CHARS;
20
+ return chars;
21
+ }
22
+ export function estimateTextTokens(text) {
23
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
24
+ }
25
+ export function estimateTextAndImageContentTokens(content) {
26
+ return Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);
27
+ }
28
+ export function estimateMessageTokens(message) {
29
+ let chars = 0;
30
+ if (message.role === "user")
31
+ return estimateTextAndImageContentTokens(message.content);
32
+ if (message.role === "toolResult")
33
+ return estimateTextAndImageContentTokens(message.content);
34
+ for (const block of message.content) {
35
+ if (block.type === "text") {
36
+ chars += block.text.length;
37
+ }
38
+ else if (block.type === "thinking") {
39
+ chars += block.thinking.length;
40
+ }
41
+ else {
42
+ chars += block.name.length + safeJsonStringify(block.arguments).length;
43
+ }
44
+ }
45
+ return Math.ceil(chars / CHARS_PER_TOKEN);
46
+ }
47
+ function getLastAssistantUsageInfo(messages) {
48
+ for (let i = messages.length - 1; i >= 0; i--) {
49
+ const message = messages[i];
50
+ if (message.role !== "assistant")
51
+ continue;
52
+ const assistant = message;
53
+ if (assistant.stopReason === "aborted" || assistant.stopReason === "error")
54
+ continue;
55
+ if (calculateContextTokens(assistant.usage) > 0)
56
+ return { usage: assistant.usage, index: i };
57
+ }
58
+ return undefined;
59
+ }
60
+ function estimateMessages(messages) {
61
+ const usageInfo = getLastAssistantUsageInfo(messages);
62
+ if (usageInfo) {
63
+ const usageTokens = calculateContextTokens(usageInfo.usage);
64
+ let trailingTokens = 0;
65
+ for (let i = usageInfo.index + 1; i < messages.length; i++) {
66
+ trailingTokens += estimateMessageTokens(messages[i]);
67
+ }
68
+ return { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };
69
+ }
70
+ let tokens = 0;
71
+ for (const message of messages)
72
+ tokens += estimateMessageTokens(message);
73
+ return { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };
74
+ }
75
+ function isMessageArray(value) {
76
+ return Array.isArray(value);
77
+ }
78
+ export function estimateContextTokens(context) {
79
+ if (isMessageArray(context))
80
+ return estimateMessages(context);
81
+ const estimate = estimateMessages(context.messages);
82
+ if (estimate.lastUsageIndex !== null)
83
+ return estimate;
84
+ let prefixTokens = context.systemPrompt ? estimateTextTokens(context.systemPrompt) : 0;
85
+ if (context.tools && context.tools.length > 0) {
86
+ prefixTokens += estimateTextTokens(safeJsonStringify(context.tools));
87
+ }
88
+ return {
89
+ tokens: estimate.tokens + prefixTokens,
90
+ usageTokens: estimate.usageTokens,
91
+ trailingTokens: estimate.trailingTokens + prefixTokens,
92
+ lastUsageIndex: estimate.lastUsageIndex,
93
+ };
94
+ }
95
+ //# sourceMappingURL=estimate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate.js","sourceRoot":"","sources":["../../src/utils/estimate.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage, Context, ImageContent, Message, TextContent, Usage } from \"../types.ts\";\n\nexport interface ContextUsageEstimate {\n\t/** Estimated total context tokens. */\n\ttokens: number;\n\t/** Tokens reported by the most recent assistant usage block. */\n\tusageTokens: number;\n\t/** Estimated tokens after the most recent assistant usage block. */\n\ttrailingTokens: number;\n\t/** Index of the message that provided usage, or null when none exists. */\n\tlastUsageIndex: number | null;\n}\n\nconst CHARS_PER_TOKEN = 4;\nconst ESTIMATED_IMAGE_CHARS = 4800;\n\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\nfunction safeJsonStringify(value: unknown): string {\n\ttry {\n\t\treturn JSON.stringify(value) ?? \"undefined\";\n\t} catch {\n\t\treturn \"[unserializable]\";\n\t}\n}\n\nfunction estimateTextAndImageContentChars(content: string | Array<TextContent | ImageContent>): number {\n\tif (typeof content === \"string\") return content.length;\n\n\tlet chars = 0;\n\tfor (const block of content) chars += block.type === \"text\" ? block.text.length : ESTIMATED_IMAGE_CHARS;\n\treturn chars;\n}\n\nexport function estimateTextTokens(text: string): number {\n\treturn Math.ceil(text.length / CHARS_PER_TOKEN);\n}\n\nexport function estimateTextAndImageContentTokens(content: string | Array<TextContent | ImageContent>): number {\n\treturn Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);\n}\n\nexport function estimateMessageTokens(message: Message): number {\n\tlet chars = 0;\n\n\tif (message.role === \"user\") return estimateTextAndImageContentTokens(message.content);\n\tif (message.role === \"toolResult\") return estimateTextAndImageContentTokens(message.content);\n\n\tfor (const block of message.content) {\n\t\tif (block.type === \"text\") {\n\t\t\tchars += block.text.length;\n\t\t} else if (block.type === \"thinking\") {\n\t\t\tchars += block.thinking.length;\n\t\t} else {\n\t\t\tchars += block.name.length + safeJsonStringify(block.arguments).length;\n\t\t}\n\t}\n\treturn Math.ceil(chars / CHARS_PER_TOKEN);\n}\n\nfunction getLastAssistantUsageInfo(messages: readonly Message[]): { usage: Usage; index: number } | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tif (assistant.stopReason === \"aborted\" || assistant.stopReason === \"error\") continue;\n\t\tif (calculateContextTokens(assistant.usage) > 0) return { usage: assistant.usage, index: i };\n\t}\n\treturn undefined;\n}\n\nfunction estimateMessages(messages: readonly Message[]): ContextUsageEstimate {\n\tconst usageInfo = getLastAssistantUsageInfo(messages);\n\tif (usageInfo) {\n\t\tconst usageTokens = calculateContextTokens(usageInfo.usage);\n\t\tlet trailingTokens = 0;\n\t\tfor (let i = usageInfo.index + 1; i < messages.length; i++) {\n\t\t\ttrailingTokens += estimateMessageTokens(messages[i]);\n\t\t}\n\t\treturn { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };\n\t}\n\n\tlet tokens = 0;\n\tfor (const message of messages) tokens += estimateMessageTokens(message);\n\treturn { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };\n}\n\nfunction isMessageArray(value: Context | readonly Message[]): value is readonly Message[] {\n\treturn Array.isArray(value);\n}\n\nexport function estimateContextTokens(context: Context | readonly Message[]): ContextUsageEstimate {\n\tif (isMessageArray(context)) return estimateMessages(context);\n\n\tconst estimate = estimateMessages(context.messages);\n\tif (estimate.lastUsageIndex !== null) return estimate;\n\n\tlet prefixTokens = context.systemPrompt ? estimateTextTokens(context.systemPrompt) : 0;\n\tif (context.tools && context.tools.length > 0) {\n\t\tprefixTokens += estimateTextTokens(safeJsonStringify(context.tools));\n\t}\n\n\treturn {\n\t\ttokens: estimate.tokens + prefixTokens,\n\t\tusageTokens: estimate.usageTokens,\n\t\ttrailingTokens: estimate.trailingTokens + prefixTokens,\n\t\tlastUsageIndex: estimate.lastUsageIndex,\n\t};\n}\n"]}
@@ -2,6 +2,7 @@ type OAuthDeviceCodeIncompletePollResult = {
2
2
  status: "pending";
3
3
  } | {
4
4
  status: "slow_down";
5
+ intervalSeconds?: number;
5
6
  } | {
6
7
  status: "failed";
7
8
  message: string;
@@ -13,6 +14,7 @@ export type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult |
13
14
  export type OAuthDeviceCodePollOptions<T> = {
14
15
  intervalSeconds?: number;
15
16
  expiresInSeconds?: number;
17
+ waitBeforeFirstPoll?: boolean;
16
18
  poll: () => Promise<OAuthDeviceCodePollResult<T>>;
17
19
  signal?: AbortSignal;
18
20
  };
@@ -1 +1 @@
1
- {"version":3,"file":"device-code.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/device-code.ts"],"names":[],"mappings":"AAUA,KAAK,mCAAmC,GACrC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,mCAAmC,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAElH,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,OAAO,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAsBF,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,OAAO,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAsCnG","sourcesContent":["const CANCEL_MESSAGE = \"Login cancelled\";\nconst TIMEOUT_MESSAGE = \"Device flow timed out\";\nconst SLOW_DOWN_TIMEOUT_MESSAGE =\n\t\"Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.\";\nconst MINIMUM_INTERVAL_MS = 1000;\n// RFC 8628 section 3.2: if the authorization server omits `interval`, the client must use 5 seconds.\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\n// RFC 8628 section 3.5: `slow_down` means the polling interval must increase by 5 seconds.\nconst SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;\n\ntype OAuthDeviceCodeIncompletePollResult =\n\t| { status: \"pending\" }\n\t| { status: \"slow_down\" }\n\t| { status: \"failed\"; message: string };\n\nexport type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult | { status: \"complete\"; value: T };\n\nexport type OAuthDeviceCodePollOptions<T> = {\n\tintervalSeconds?: number;\n\texpiresInSeconds?: number;\n\tpoll: () => Promise<OAuthDeviceCodePollResult<T>>;\n\tsignal?: AbortSignal;\n};\n\nfunction abortableSleep(ms: number, signal: AbortSignal | undefined, cancelMessage: string): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(cancelMessage));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(cancelMessage));\n\t\t};\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\nexport async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodePollOptions<T>): Promise<T> {\n\tconst deadline =\n\t\ttypeof options.expiresInSeconds === \"number\"\n\t\t\t? Date.now() + options.expiresInSeconds * 1000\n\t\t\t: Number.POSITIVE_INFINITY;\n\tlet intervalMs = Math.max(\n\t\tMINIMUM_INTERVAL_MS,\n\t\tMath.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000),\n\t);\n\n\tlet slowDownResponses = 0;\n\twhile (Date.now() < deadline) {\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(CANCEL_MESSAGE);\n\t\t}\n\n\t\tconst result = await options.poll();\n\t\tif (result.status === \"complete\") {\n\t\t\treturn result.value;\n\t\t}\n\t\tif (result.status === \"failed\") {\n\t\t\tthrow new Error(result.message);\n\t\t}\n\t\tif (result.status === \"slow_down\") {\n\t\t\tslowDownResponses += 1;\n\t\t\t// RFC 8628 section 3.5: apply this increase to this and all subsequent requests.\n\t\t\tintervalMs = Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);\n\t\t}\n\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs <= 0) {\n\t\t\tbreak;\n\t\t}\n\n\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t}\n\n\tthrow new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);\n}\n"]}
1
+ {"version":3,"file":"device-code.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/device-code.ts"],"names":[],"mappings":"AAUA,KAAK,mCAAmC,GACrC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,mCAAmC,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAElH,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAsBF,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,OAAO,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAoDnG","sourcesContent":["const CANCEL_MESSAGE = \"Login cancelled\";\nconst TIMEOUT_MESSAGE = \"Device flow timed out\";\nconst SLOW_DOWN_TIMEOUT_MESSAGE =\n\t\"Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.\";\nconst MINIMUM_INTERVAL_MS = 1000;\n// RFC 8628 section 3.2: if the authorization server omits `interval`, the client must use 5 seconds.\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\n// RFC 8628 section 3.5: `slow_down` means the polling interval must increase by 5 seconds.\nconst SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;\n\ntype OAuthDeviceCodeIncompletePollResult =\n\t| { status: \"pending\" }\n\t| { status: \"slow_down\"; intervalSeconds?: number }\n\t| { status: \"failed\"; message: string };\n\nexport type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult | { status: \"complete\"; value: T };\n\nexport type OAuthDeviceCodePollOptions<T> = {\n\tintervalSeconds?: number;\n\texpiresInSeconds?: number;\n\twaitBeforeFirstPoll?: boolean;\n\tpoll: () => Promise<OAuthDeviceCodePollResult<T>>;\n\tsignal?: AbortSignal;\n};\n\nfunction abortableSleep(ms: number, signal: AbortSignal | undefined, cancelMessage: string): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(cancelMessage));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(cancelMessage));\n\t\t};\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\nexport async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodePollOptions<T>): Promise<T> {\n\tconst deadline =\n\t\ttypeof options.expiresInSeconds === \"number\"\n\t\t\t? Date.now() + options.expiresInSeconds * 1000\n\t\t\t: Number.POSITIVE_INFINITY;\n\tlet intervalMs = Math.max(\n\t\tMINIMUM_INTERVAL_MS,\n\t\tMath.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000),\n\t);\n\n\tlet slowDownResponses = 0;\n\tif (options.waitBeforeFirstPoll) {\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t\t}\n\t}\n\n\twhile (Date.now() < deadline) {\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(CANCEL_MESSAGE);\n\t\t}\n\n\t\tconst result = await options.poll();\n\t\tif (result.status === \"complete\") {\n\t\t\treturn result.value;\n\t\t}\n\t\tif (result.status === \"failed\") {\n\t\t\tthrow new Error(result.message);\n\t\t}\n\t\tif (result.status === \"slow_down\") {\n\t\t\tslowDownResponses += 1;\n\t\t\t// Use the server-provided interval when given (GitHub reports the new required minimum\n\t\t\t// in `interval`); trusting only a client-tracked value risks polling early forever under\n\t\t\t// WSL/VM clock drift. Otherwise apply RFC 8628 section 3.5: increase by 5 seconds.\n\t\t\tintervalMs =\n\t\t\t\ttypeof result.intervalSeconds === \"number\" &&\n\t\t\t\tNumber.isFinite(result.intervalSeconds) &&\n\t\t\t\tresult.intervalSeconds > 0\n\t\t\t\t\t? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1000))\n\t\t\t\t\t: Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);\n\t\t}\n\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs <= 0) {\n\t\t\tbreak;\n\t\t}\n\n\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t}\n\n\tthrow new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);\n}\n"]}
@@ -29,6 +29,12 @@ export async function pollOAuthDeviceCodeFlow(options) {
29
29
  : Number.POSITIVE_INFINITY;
30
30
  let intervalMs = Math.max(MINIMUM_INTERVAL_MS, Math.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000));
31
31
  let slowDownResponses = 0;
32
+ if (options.waitBeforeFirstPoll) {
33
+ const remainingMs = deadline - Date.now();
34
+ if (remainingMs > 0) {
35
+ await abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);
36
+ }
37
+ }
32
38
  while (Date.now() < deadline) {
33
39
  if (options.signal?.aborted) {
34
40
  throw new Error(CANCEL_MESSAGE);
@@ -42,8 +48,15 @@ export async function pollOAuthDeviceCodeFlow(options) {
42
48
  }
43
49
  if (result.status === "slow_down") {
44
50
  slowDownResponses += 1;
45
- // RFC 8628 section 3.5: apply this increase to this and all subsequent requests.
46
- intervalMs = Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
51
+ // Use the server-provided interval when given (GitHub reports the new required minimum
52
+ // in `interval`); trusting only a client-tracked value risks polling early forever under
53
+ // WSL/VM clock drift. Otherwise apply RFC 8628 section 3.5: increase by 5 seconds.
54
+ intervalMs =
55
+ typeof result.intervalSeconds === "number" &&
56
+ Number.isFinite(result.intervalSeconds) &&
57
+ result.intervalSeconds > 0
58
+ ? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1000))
59
+ : Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
47
60
  }
48
61
  const remainingMs = deadline - Date.now();
49
62
  if (remainingMs <= 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"device-code.js","sourceRoot":"","sources":["../../../src/utils/oauth/device-code.ts"],"names":[],"mappings":"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","sourcesContent":["const CANCEL_MESSAGE = \"Login cancelled\";\nconst TIMEOUT_MESSAGE = \"Device flow timed out\";\nconst SLOW_DOWN_TIMEOUT_MESSAGE =\n\t\"Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.\";\nconst MINIMUM_INTERVAL_MS = 1000;\n// RFC 8628 section 3.2: if the authorization server omits `interval`, the client must use 5 seconds.\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\n// RFC 8628 section 3.5: `slow_down` means the polling interval must increase by 5 seconds.\nconst SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;\n\ntype OAuthDeviceCodeIncompletePollResult =\n\t| { status: \"pending\" }\n\t| { status: \"slow_down\" }\n\t| { status: \"failed\"; message: string };\n\nexport type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult | { status: \"complete\"; value: T };\n\nexport type OAuthDeviceCodePollOptions<T> = {\n\tintervalSeconds?: number;\n\texpiresInSeconds?: number;\n\tpoll: () => Promise<OAuthDeviceCodePollResult<T>>;\n\tsignal?: AbortSignal;\n};\n\nfunction abortableSleep(ms: number, signal: AbortSignal | undefined, cancelMessage: string): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(cancelMessage));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(cancelMessage));\n\t\t};\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\nexport async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodePollOptions<T>): Promise<T> {\n\tconst deadline =\n\t\ttypeof options.expiresInSeconds === \"number\"\n\t\t\t? Date.now() + options.expiresInSeconds * 1000\n\t\t\t: Number.POSITIVE_INFINITY;\n\tlet intervalMs = Math.max(\n\t\tMINIMUM_INTERVAL_MS,\n\t\tMath.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000),\n\t);\n\n\tlet slowDownResponses = 0;\n\twhile (Date.now() < deadline) {\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(CANCEL_MESSAGE);\n\t\t}\n\n\t\tconst result = await options.poll();\n\t\tif (result.status === \"complete\") {\n\t\t\treturn result.value;\n\t\t}\n\t\tif (result.status === \"failed\") {\n\t\t\tthrow new Error(result.message);\n\t\t}\n\t\tif (result.status === \"slow_down\") {\n\t\t\tslowDownResponses += 1;\n\t\t\t// RFC 8628 section 3.5: apply this increase to this and all subsequent requests.\n\t\t\tintervalMs = Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);\n\t\t}\n\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs <= 0) {\n\t\t\tbreak;\n\t\t}\n\n\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t}\n\n\tthrow new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);\n}\n"]}
1
+ {"version":3,"file":"device-code.js","sourceRoot":"","sources":["../../../src/utils/oauth/device-code.ts"],"names":[],"mappings":"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","sourcesContent":["const CANCEL_MESSAGE = \"Login cancelled\";\nconst TIMEOUT_MESSAGE = \"Device flow timed out\";\nconst SLOW_DOWN_TIMEOUT_MESSAGE =\n\t\"Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.\";\nconst MINIMUM_INTERVAL_MS = 1000;\n// RFC 8628 section 3.2: if the authorization server omits `interval`, the client must use 5 seconds.\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\n// RFC 8628 section 3.5: `slow_down` means the polling interval must increase by 5 seconds.\nconst SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;\n\ntype OAuthDeviceCodeIncompletePollResult =\n\t| { status: \"pending\" }\n\t| { status: \"slow_down\"; intervalSeconds?: number }\n\t| { status: \"failed\"; message: string };\n\nexport type OAuthDeviceCodePollResult<T> = OAuthDeviceCodeIncompletePollResult | { status: \"complete\"; value: T };\n\nexport type OAuthDeviceCodePollOptions<T> = {\n\tintervalSeconds?: number;\n\texpiresInSeconds?: number;\n\twaitBeforeFirstPoll?: boolean;\n\tpoll: () => Promise<OAuthDeviceCodePollResult<T>>;\n\tsignal?: AbortSignal;\n};\n\nfunction abortableSleep(ms: number, signal: AbortSignal | undefined, cancelMessage: string): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(cancelMessage));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(cancelMessage));\n\t\t};\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n\nexport async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodePollOptions<T>): Promise<T> {\n\tconst deadline =\n\t\ttypeof options.expiresInSeconds === \"number\"\n\t\t\t? Date.now() + options.expiresInSeconds * 1000\n\t\t\t: Number.POSITIVE_INFINITY;\n\tlet intervalMs = Math.max(\n\t\tMINIMUM_INTERVAL_MS,\n\t\tMath.floor((options.intervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000),\n\t);\n\n\tlet slowDownResponses = 0;\n\tif (options.waitBeforeFirstPoll) {\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs > 0) {\n\t\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t\t}\n\t}\n\n\twhile (Date.now() < deadline) {\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(CANCEL_MESSAGE);\n\t\t}\n\n\t\tconst result = await options.poll();\n\t\tif (result.status === \"complete\") {\n\t\t\treturn result.value;\n\t\t}\n\t\tif (result.status === \"failed\") {\n\t\t\tthrow new Error(result.message);\n\t\t}\n\t\tif (result.status === \"slow_down\") {\n\t\t\tslowDownResponses += 1;\n\t\t\t// Use the server-provided interval when given (GitHub reports the new required minimum\n\t\t\t// in `interval`); trusting only a client-tracked value risks polling early forever under\n\t\t\t// WSL/VM clock drift. Otherwise apply RFC 8628 section 3.5: increase by 5 seconds.\n\t\t\tintervalMs =\n\t\t\t\ttypeof result.intervalSeconds === \"number\" &&\n\t\t\t\tNumber.isFinite(result.intervalSeconds) &&\n\t\t\t\tresult.intervalSeconds > 0\n\t\t\t\t\t? Math.max(MINIMUM_INTERVAL_MS, Math.floor(result.intervalSeconds * 1000))\n\t\t\t\t\t: Math.max(MINIMUM_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);\n\t\t}\n\n\t\tconst remainingMs = deadline - Date.now();\n\t\tif (remainingMs <= 0) {\n\t\t\tbreak;\n\t\t}\n\n\t\tawait abortableSleep(Math.min(intervalMs, remainingMs), options.signal, CANCEL_MESSAGE);\n\t}\n\n\tthrow new Error(slowDownResponses > 0 ? SLOW_DOWN_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"github-copilot.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/github-copilot.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,qBAAqB,CAAC;AAItE,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAuB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAqCrH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS5D;AA4BD,wBAAgB,uBAAuB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CASzF;AA+LD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,YAAY,EAAE,MAAM,EACpB,gBAAgB,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AA8CD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAClD,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvG,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuC5B;AAQD,eAAO,MAAM,kBAAkB,EAAE,SA4BhC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,sBAoCxC,CAAC","sourcesContent":["/**\n * GitHub Copilot OAuth flow\n */\n\nimport type { OAuthAuth, OAuthCredential } from \"../../auth/types.ts\";\nimport { GITHUB_COPILOT_MODELS } from \"../../providers/github-copilot.models.ts\";\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\ntype CopilotCredentials = OAuthCredentials & {\n\tenterpriseUrl?: string;\n\tavailableModelIds: string[];\n};\n\nconst decode = (s: string) => atob(s);\nconst CLIENT_ID = decode(\"SXYxLmI1MDdhMDhjODdlY2ZlOTg=\");\n\nconst COPILOT_HEADERS = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n} as const;\nconst COPILOT_API_VERSION = \"2026-06-01\";\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 DeviceTokenSuccessResponse = {\n\taccess_token: string;\n\ttoken_type?: string;\n\tscope?: string;\n};\n\ntype DeviceTokenErrorResponse = {\n\terror: string;\n\terror_description?: string;\n};\n\nexport function normalizeDomain(input: string): string | null {\n\tconst trimmed = input.trim();\n\tif (!trimmed) return null;\n\ttry {\n\t\tconst url = trimmed.includes(\"://\") ? new URL(trimmed) : new URL(`https://${trimmed}`);\n\t\treturn url.hostname;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction getUrls(domain: string): {\n\tdeviceCodeUrl: string;\n\taccessTokenUrl: string;\n\tcopilotTokenUrl: string;\n} {\n\treturn {\n\t\tdeviceCodeUrl: `https://${domain}/login/device/code`,\n\t\taccessTokenUrl: `https://${domain}/login/oauth/access_token`,\n\t\tcopilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,\n\t};\n}\n\n/**\n * Parse the proxy-ep from a Copilot token and convert to API base URL.\n * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...\n * Returns API URL like https://api.individual.githubcopilot.com\n */\nfunction getBaseUrlFromToken(token: string): string | null {\n\tconst match = token.match(/proxy-ep=([^;]+)/);\n\tif (!match) return null;\n\tconst proxyHost = match[1];\n\t// Convert proxy.xxx to api.xxx\n\tconst apiHost = proxyHost.replace(/^proxy\\./, \"api.\");\n\treturn `https://${apiHost}`;\n}\n\nexport function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {\n\t// If we have a token, extract the base URL from proxy-ep\n\tif (token) {\n\t\tconst urlFromToken = getBaseUrlFromToken(token);\n\t\tif (urlFromToken) return urlFromToken;\n\t}\n\t// Fallback for enterprise or if token parsing fails\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn \"https://api.individual.githubcopilot.com\";\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction isSelectableCopilotModel(item: Record<string, unknown>): boolean {\n\tconst policy = asRecord(item.policy);\n\tconst capabilities = asRecord(item.capabilities);\n\tconst supports = asRecord(capabilities?.supports);\n\treturn item.model_picker_enabled === true && policy?.state !== \"disabled\" && supports?.tool_calls !== false;\n}\n\nfunction parseAvailableCopilotModelIds(raw: unknown): string[] {\n\tconst data = asRecord(raw)?.data;\n\tif (!Array.isArray(data)) {\n\t\tthrow new Error(\"Invalid Copilot models response\");\n\t}\n\n\tconst ids: string[] = [];\n\tfor (const rawItem of data) {\n\t\tconst item = asRecord(rawItem);\n\t\tconst id = item?.id;\n\t\tif (typeof id === \"string\" && item && isSelectableCopilotModel(item)) {\n\t\t\tids.push(id);\n\t\t}\n\t}\n\treturn ids;\n}\n\nasync function fetchAvailableGitHubCopilotModelIds(copilotToken: string, enterpriseDomain?: string): Promise<string[]> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain);\n\tconst raw = await fetchJson(`${baseUrl}/models`, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${copilotToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t\t\"X-GitHub-Api-Version\": COPILOT_API_VERSION,\n\t\t},\n\t\tsignal: AbortSignal.timeout(5000),\n\t});\n\treturn parseAvailableCopilotModelIds(raw);\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\nasync function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {\n\tconst urls = getUrls(domain);\n\tconst data = await fetchJson(urls.deviceCodeUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t},\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: \"read:user\",\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 deviceCode = (data as Record<string, unknown>).device_code;\n\tconst userCode = (data as Record<string, unknown>).user_code;\n\tconst verificationUri = (data as Record<string, unknown>).verification_uri;\n\tconst interval = (data as Record<string, unknown>).interval;\n\tconst expiresIn = (data as Record<string, unknown>).expires_in;\n\n\tif (\n\t\ttypeof deviceCode !== \"string\" ||\n\t\ttypeof userCode !== \"string\" ||\n\t\ttypeof verificationUri !== \"string\" ||\n\t\t(interval !== undefined && typeof interval !== \"number\") ||\n\t\ttypeof expiresIn !== \"number\"\n\t) {\n\t\tthrow new Error(\"Invalid device code response fields\");\n\t}\n\n\t// The verification URI is opened in the user's browser and to prevent `open` from\n\t// opening an executable or similar, we force it to be a URL.\n\tlet parsedUri: URL;\n\ttry {\n\t\tparsedUri = new URL(verificationUri);\n\t} catch {\n\t\tthrow new Error(\"Untrusted verification_uri in device code response\");\n\t}\n\tif (parsedUri.protocol !== \"https:\" && parsedUri.protocol !== \"http:\") {\n\t\tthrow new Error(\"Untrusted verification_uri in device code response\");\n\t}\n\n\treturn {\n\t\tdevice_code: deviceCode,\n\t\tuser_code: userCode,\n\t\tverification_uri: parsedUri.href,\n\t\tinterval,\n\t\texpires_in: expiresIn,\n\t};\n}\n\nasync function pollForGitHubAccessToken(\n\tdomain: string,\n\tdevice: DeviceCodeResponse,\n\tsignal?: AbortSignal,\n): Promise<string> {\n\tconst urls = getUrls(domain);\n\treturn pollOAuthDeviceCodeFlow<string>({\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst raw = await fetchJson(urls.accessTokenUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: device.device_code,\n\t\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenSuccessResponse).access_token === \"string\") {\n\t\t\t\treturn { status: \"complete\", value: (raw as DeviceTokenSuccessResponse).access_token };\n\t\t\t}\n\n\t\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenErrorResponse).error === \"string\") {\n\t\t\t\tconst { error, error_description: description } = raw as DeviceTokenErrorResponse;\n\t\t\t\tif (error === \"authorization_pending\") {\n\t\t\t\t\treturn { status: \"pending\" };\n\t\t\t\t}\n\n\t\t\t\tif (error === \"slow_down\") {\n\t\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t\t}\n\n\t\t\t\tconst descriptionSuffix = description ? `: ${description}` : \"\";\n\t\t\t\treturn { status: \"failed\", message: `Device flow failed: ${error}${descriptionSuffix}` };\n\t\t\t}\n\n\t\t\treturn { status: \"failed\", message: \"Invalid device token response\" };\n\t\t},\n\t});\n}\n\nasync function refreshGitHubCopilotAccessToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst domain = enterpriseDomain || \"github.com\";\n\tconst urls = getUrls(domain);\n\n\tconst raw = await fetchJson(urls.copilotTokenUrl, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${refreshToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t},\n\t});\n\n\tif (!raw || typeof raw !== \"object\") {\n\t\tthrow new Error(\"Invalid Copilot token response\");\n\t}\n\n\tconst token = (raw as Record<string, unknown>).token;\n\tconst expiresAt = (raw as Record<string, unknown>).expires_at;\n\n\tif (typeof token !== \"string\" || typeof expiresAt !== \"number\") {\n\t\tthrow new Error(\"Invalid Copilot token response fields\");\n\t}\n\n\treturn {\n\t\trefresh: refreshToken,\n\t\taccess: token,\n\t\texpires: expiresAt * 1000 - 5 * 60 * 1000,\n\t\tenterpriseUrl: enterpriseDomain,\n\t};\n}\n\n/**\n * Refresh GitHub Copilot token\n */\nexport async function refreshGitHubCopilotToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain);\n\treturn {\n\t\t...credentials,\n\t\tavailableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain),\n\t};\n}\n\n/**\n * Enable a model for the user's GitHub Copilot account.\n * This is required for some models (like Claude, Grok) before they can be used.\n */\nasync function enableGitHubCopilotModel(token: string, modelId: string, enterpriseDomain?: string): Promise<boolean> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);\n\tconst url = `${baseUrl}/models/${modelId}/policy`;\n\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\t...COPILOT_HEADERS,\n\t\t\t\t\"openai-intent\": \"chat-policy\",\n\t\t\t\t\"x-interaction-type\": \"chat-policy\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({ state: \"enabled\" }),\n\t\t});\n\t\treturn response.ok;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Enable all known GitHub Copilot models that may require policy acceptance.\n * Called after successful login to ensure all models are available.\n */\nasync function enableAllGitHubCopilotModels(\n\ttoken: string,\n\tenterpriseDomain?: string,\n\tonProgress?: (model: string, success: boolean) => void,\n): Promise<void> {\n\tconst models = Object.values(GITHUB_COPILOT_MODELS);\n\tawait Promise.all(\n\t\tmodels.map(async (model) => {\n\t\t\tconst success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);\n\t\t\tonProgress?.(model.id, success);\n\t\t}),\n\t);\n}\n\n/**\n * Login with GitHub Copilot OAuth (device code flow)\n *\n * @param options.onDeviceCode - Callback with URL and user code\n * @param options.onPrompt - Callback to prompt user for input\n * @param options.onProgress - Optional progress callback\n * @param options.signal - Optional AbortSignal for cancellation\n */\nexport async function loginGitHubCopilot(options: {\n\tonDeviceCode: (info: OAuthDeviceCodeInfo) => void;\n\tonPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;\n\tonProgress?: (message: string) => void;\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst input = await options.onPrompt({\n\t\tmessage: \"GitHub Enterprise URL/domain (blank for github.com)\",\n\t\tplaceholder: \"company.ghe.com\",\n\t\tallowEmpty: true,\n\t});\n\n\tif (options.signal?.aborted) {\n\t\tthrow new Error(\"Login cancelled\");\n\t}\n\n\tconst trimmed = input.trim();\n\tconst enterpriseDomain = normalizeDomain(input);\n\tif (trimmed && !enterpriseDomain) {\n\t\tthrow new Error(\"Invalid GitHub Enterprise URL/domain\");\n\t}\n\tconst domain = enterpriseDomain || \"github.com\";\n\n\tconst device = await startDeviceFlow(domain);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code,\n\t\tverificationUri: device.verification_uri,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t});\n\n\tconst githubAccessToken = await pollForGitHubAccessToken(domain, device, options.signal);\n\tconst credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined);\n\n\t// Enable all models after successful login\n\toptions.onProgress?.(\"Enabling models...\");\n\tawait enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);\n\n\t// Fetch availability after policy enable so newly enabled models are included,\n\t// while unavailable models are still filtered out.\n\treturn {\n\t\t...credentials,\n\t\tavailableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain ?? undefined),\n\t};\n}\n\nfunction copilotEnterpriseDomain(credential: OAuthCredential): string | undefined {\n\tconst enterpriseUrl = credential.enterpriseUrl;\n\tif (typeof enterpriseUrl !== \"string\" || !enterpriseUrl) return undefined;\n\treturn normalizeDomain(enterpriseUrl) ?? undefined;\n}\n\nexport const githubCopilotOAuth: OAuthAuth = {\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks) {\n\t\tconst credentials = await loginGitHubCopilot({\n\t\t\tonDeviceCode: (info) => callbacks.notify({ type: \"device_code\", ...info }),\n\t\t\tonPrompt: (prompt) =>\n\t\t\t\tcallbacks.prompt({ type: \"text\", message: prompt.message, placeholder: prompt.placeholder }),\n\t\t\tonProgress: (message) => callbacks.notify({ type: \"progress\", message }),\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t\treturn { ...credentials, type: \"oauth\" };\n\t},\n\n\tasync refresh(credential) {\n\t\treturn {\n\t\t\t...(await refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential))),\n\t\t\ttype: \"oauth\",\n\t\t};\n\t},\n\n\t/** Per-credential baseUrl from the token's proxy endpoint replaces the old `modifyModels` rewriting. */\n\tasync toAuth(credential) {\n\t\treturn {\n\t\t\tapiKey: credential.access,\n\t\t\tbaseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)),\n\t\t};\n\t},\n};\n\nexport const githubCopilotOAuthProvider: OAuthProviderInterface = {\n\tid: \"github-copilot\",\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginGitHubCopilot({\n\t\t\tonDeviceCode: callbacks.onDeviceCode,\n\t\t\tonPrompt: callbacks.onPrompt,\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\tconst creds = credentials as CopilotCredentials;\n\t\treturn refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);\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 CopilotCredentials;\n\t\tconst domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;\n\t\tconst baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);\n\t\t// Older stored Pi auth entries do not have account-specific model IDs yet;\n\t\t// keep their existing generated-catalog behavior until the next refresh/login.\n\t\tconst availableModelIds = \"availableModelIds\" in creds ? new Set(creds.availableModelIds) : undefined;\n\n\t\treturn models.flatMap((m) => {\n\t\t\tif (m.provider !== \"github-copilot\") return [m];\n\t\t\tif (availableModelIds && !availableModelIds.has(m.id)) return [];\n\t\t\treturn [{ ...m, baseUrl }];\n\t\t});\n\t},\n};\n"]}
1
+ {"version":3,"file":"github-copilot.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/github-copilot.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,qBAAqB,CAAC;AAItE,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAuB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAsCrH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS5D;AA4BD,wBAAgB,uBAAuB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CASzF;AAgMD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,YAAY,EAAE,MAAM,EACpB,gBAAgB,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AA8CD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAClD,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvG,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuC5B;AAQD,eAAO,MAAM,kBAAkB,EAAE,SA4BhC,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,sBAoCxC,CAAC","sourcesContent":["/**\n * GitHub Copilot OAuth flow\n */\n\nimport type { OAuthAuth, OAuthCredential } from \"../../auth/types.ts\";\nimport { GITHUB_COPILOT_MODELS } from \"../../providers/github-copilot.models.ts\";\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\ntype CopilotCredentials = OAuthCredentials & {\n\tenterpriseUrl?: string;\n\tavailableModelIds: string[];\n};\n\nconst decode = (s: string) => atob(s);\nconst CLIENT_ID = decode(\"SXYxLmI1MDdhMDhjODdlY2ZlOTg=\");\n\nconst COPILOT_HEADERS = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n} as const;\nconst COPILOT_API_VERSION = \"2026-06-01\";\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 DeviceTokenSuccessResponse = {\n\taccess_token: string;\n\ttoken_type?: string;\n\tscope?: string;\n};\n\ntype DeviceTokenErrorResponse = {\n\terror: string;\n\terror_description?: string;\n\tinterval?: number;\n};\n\nexport function normalizeDomain(input: string): string | null {\n\tconst trimmed = input.trim();\n\tif (!trimmed) return null;\n\ttry {\n\t\tconst url = trimmed.includes(\"://\") ? new URL(trimmed) : new URL(`https://${trimmed}`);\n\t\treturn url.hostname;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction getUrls(domain: string): {\n\tdeviceCodeUrl: string;\n\taccessTokenUrl: string;\n\tcopilotTokenUrl: string;\n} {\n\treturn {\n\t\tdeviceCodeUrl: `https://${domain}/login/device/code`,\n\t\taccessTokenUrl: `https://${domain}/login/oauth/access_token`,\n\t\tcopilotTokenUrl: `https://api.${domain}/copilot_internal/v2/token`,\n\t};\n}\n\n/**\n * Parse the proxy-ep from a Copilot token and convert to API base URL.\n * Token format: tid=...;exp=...;proxy-ep=proxy.individual.githubcopilot.com;...\n * Returns API URL like https://api.individual.githubcopilot.com\n */\nfunction getBaseUrlFromToken(token: string): string | null {\n\tconst match = token.match(/proxy-ep=([^;]+)/);\n\tif (!match) return null;\n\tconst proxyHost = match[1];\n\t// Convert proxy.xxx to api.xxx\n\tconst apiHost = proxyHost.replace(/^proxy\\./, \"api.\");\n\treturn `https://${apiHost}`;\n}\n\nexport function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: string): string {\n\t// If we have a token, extract the base URL from proxy-ep\n\tif (token) {\n\t\tconst urlFromToken = getBaseUrlFromToken(token);\n\t\tif (urlFromToken) return urlFromToken;\n\t}\n\t// Fallback for enterprise or if token parsing fails\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn \"https://api.individual.githubcopilot.com\";\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction isSelectableCopilotModel(item: Record<string, unknown>): boolean {\n\tconst policy = asRecord(item.policy);\n\tconst capabilities = asRecord(item.capabilities);\n\tconst supports = asRecord(capabilities?.supports);\n\treturn item.model_picker_enabled === true && policy?.state !== \"disabled\" && supports?.tool_calls !== false;\n}\n\nfunction parseAvailableCopilotModelIds(raw: unknown): string[] {\n\tconst data = asRecord(raw)?.data;\n\tif (!Array.isArray(data)) {\n\t\tthrow new Error(\"Invalid Copilot models response\");\n\t}\n\n\tconst ids: string[] = [];\n\tfor (const rawItem of data) {\n\t\tconst item = asRecord(rawItem);\n\t\tconst id = item?.id;\n\t\tif (typeof id === \"string\" && item && isSelectableCopilotModel(item)) {\n\t\t\tids.push(id);\n\t\t}\n\t}\n\treturn ids;\n}\n\nasync function fetchAvailableGitHubCopilotModelIds(copilotToken: string, enterpriseDomain?: string): Promise<string[]> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain);\n\tconst raw = await fetchJson(`${baseUrl}/models`, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${copilotToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t\t\"X-GitHub-Api-Version\": COPILOT_API_VERSION,\n\t\t},\n\t\tsignal: AbortSignal.timeout(5000),\n\t});\n\treturn parseAvailableCopilotModelIds(raw);\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\nasync function startDeviceFlow(domain: string): Promise<DeviceCodeResponse> {\n\tconst urls = getUrls(domain);\n\tconst data = await fetchJson(urls.deviceCodeUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t},\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: \"read:user\",\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 deviceCode = (data as Record<string, unknown>).device_code;\n\tconst userCode = (data as Record<string, unknown>).user_code;\n\tconst verificationUri = (data as Record<string, unknown>).verification_uri;\n\tconst interval = (data as Record<string, unknown>).interval;\n\tconst expiresIn = (data as Record<string, unknown>).expires_in;\n\n\tif (\n\t\ttypeof deviceCode !== \"string\" ||\n\t\ttypeof userCode !== \"string\" ||\n\t\ttypeof verificationUri !== \"string\" ||\n\t\t(interval !== undefined && typeof interval !== \"number\") ||\n\t\ttypeof expiresIn !== \"number\"\n\t) {\n\t\tthrow new Error(\"Invalid device code response fields\");\n\t}\n\n\t// The verification URI is opened in the user's browser and to prevent `open` from\n\t// opening an executable or similar, we force it to be a URL.\n\tlet parsedUri: URL;\n\ttry {\n\t\tparsedUri = new URL(verificationUri);\n\t} catch {\n\t\tthrow new Error(\"Untrusted verification_uri in device code response\");\n\t}\n\tif (parsedUri.protocol !== \"https:\" && parsedUri.protocol !== \"http:\") {\n\t\tthrow new Error(\"Untrusted verification_uri in device code response\");\n\t}\n\n\treturn {\n\t\tdevice_code: deviceCode,\n\t\tuser_code: userCode,\n\t\tverification_uri: parsedUri.href,\n\t\tinterval,\n\t\texpires_in: expiresIn,\n\t};\n}\n\nasync function pollForGitHubAccessToken(\n\tdomain: string,\n\tdevice: DeviceCodeResponse,\n\tsignal?: AbortSignal,\n): Promise<string> {\n\tconst urls = getUrls(domain);\n\treturn pollOAuthDeviceCodeFlow<string>({\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t\twaitBeforeFirstPoll: true,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst raw = await fetchJson(urls.accessTokenUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\t\t\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\t\t\t},\n\t\t\t\tbody: new URLSearchParams({\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: device.device_code,\n\t\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenSuccessResponse).access_token === \"string\") {\n\t\t\t\treturn { status: \"complete\", value: (raw as DeviceTokenSuccessResponse).access_token };\n\t\t\t}\n\n\t\t\tif (raw && typeof raw === \"object\" && typeof (raw as DeviceTokenErrorResponse).error === \"string\") {\n\t\t\t\tconst { error, error_description: description, interval } = raw as DeviceTokenErrorResponse;\n\t\t\t\tif (error === \"authorization_pending\") {\n\t\t\t\t\treturn { status: \"pending\" };\n\t\t\t\t}\n\n\t\t\t\tif (error === \"slow_down\") {\n\t\t\t\t\treturn { status: \"slow_down\", intervalSeconds: typeof interval === \"number\" ? interval : undefined };\n\t\t\t\t}\n\n\t\t\t\tconst descriptionSuffix = description ? `: ${description}` : \"\";\n\t\t\t\treturn { status: \"failed\", message: `Device flow failed: ${error}${descriptionSuffix}` };\n\t\t\t}\n\n\t\t\treturn { status: \"failed\", message: \"Invalid device token response\" };\n\t\t},\n\t});\n}\n\nasync function refreshGitHubCopilotAccessToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst domain = enterpriseDomain || \"github.com\";\n\tconst urls = getUrls(domain);\n\n\tconst raw = await fetchJson(urls.copilotTokenUrl, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${refreshToken}`,\n\t\t\t...COPILOT_HEADERS,\n\t\t},\n\t});\n\n\tif (!raw || typeof raw !== \"object\") {\n\t\tthrow new Error(\"Invalid Copilot token response\");\n\t}\n\n\tconst token = (raw as Record<string, unknown>).token;\n\tconst expiresAt = (raw as Record<string, unknown>).expires_at;\n\n\tif (typeof token !== \"string\" || typeof expiresAt !== \"number\") {\n\t\tthrow new Error(\"Invalid Copilot token response fields\");\n\t}\n\n\treturn {\n\t\trefresh: refreshToken,\n\t\taccess: token,\n\t\texpires: expiresAt * 1000 - 5 * 60 * 1000,\n\t\tenterpriseUrl: enterpriseDomain,\n\t};\n}\n\n/**\n * Refresh GitHub Copilot token\n */\nexport async function refreshGitHubCopilotToken(\n\trefreshToken: string,\n\tenterpriseDomain?: string,\n): Promise<OAuthCredentials> {\n\tconst credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain);\n\treturn {\n\t\t...credentials,\n\t\tavailableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain),\n\t};\n}\n\n/**\n * Enable a model for the user's GitHub Copilot account.\n * This is required for some models (like Claude, Grok) before they can be used.\n */\nasync function enableGitHubCopilotModel(token: string, modelId: string, enterpriseDomain?: string): Promise<boolean> {\n\tconst baseUrl = getGitHubCopilotBaseUrl(token, enterpriseDomain);\n\tconst url = `${baseUrl}/models/${modelId}/policy`;\n\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\t...COPILOT_HEADERS,\n\t\t\t\t\"openai-intent\": \"chat-policy\",\n\t\t\t\t\"x-interaction-type\": \"chat-policy\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({ state: \"enabled\" }),\n\t\t});\n\t\treturn response.ok;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Enable all known GitHub Copilot models that may require policy acceptance.\n * Called after successful login to ensure all models are available.\n */\nasync function enableAllGitHubCopilotModels(\n\ttoken: string,\n\tenterpriseDomain?: string,\n\tonProgress?: (model: string, success: boolean) => void,\n): Promise<void> {\n\tconst models = Object.values(GITHUB_COPILOT_MODELS);\n\tawait Promise.all(\n\t\tmodels.map(async (model) => {\n\t\t\tconst success = await enableGitHubCopilotModel(token, model.id, enterpriseDomain);\n\t\t\tonProgress?.(model.id, success);\n\t\t}),\n\t);\n}\n\n/**\n * Login with GitHub Copilot OAuth (device code flow)\n *\n * @param options.onDeviceCode - Callback with URL and user code\n * @param options.onPrompt - Callback to prompt user for input\n * @param options.onProgress - Optional progress callback\n * @param options.signal - Optional AbortSignal for cancellation\n */\nexport async function loginGitHubCopilot(options: {\n\tonDeviceCode: (info: OAuthDeviceCodeInfo) => void;\n\tonPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;\n\tonProgress?: (message: string) => void;\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst input = await options.onPrompt({\n\t\tmessage: \"GitHub Enterprise URL/domain (blank for github.com)\",\n\t\tplaceholder: \"company.ghe.com\",\n\t\tallowEmpty: true,\n\t});\n\n\tif (options.signal?.aborted) {\n\t\tthrow new Error(\"Login cancelled\");\n\t}\n\n\tconst trimmed = input.trim();\n\tconst enterpriseDomain = normalizeDomain(input);\n\tif (trimmed && !enterpriseDomain) {\n\t\tthrow new Error(\"Invalid GitHub Enterprise URL/domain\");\n\t}\n\tconst domain = enterpriseDomain || \"github.com\";\n\n\tconst device = await startDeviceFlow(domain);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code,\n\t\tverificationUri: device.verification_uri,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in,\n\t});\n\n\tconst githubAccessToken = await pollForGitHubAccessToken(domain, device, options.signal);\n\tconst credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined);\n\n\t// Enable all models after successful login\n\toptions.onProgress?.(\"Enabling models...\");\n\tawait enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined);\n\n\t// Fetch availability after policy enable so newly enabled models are included,\n\t// while unavailable models are still filtered out.\n\treturn {\n\t\t...credentials,\n\t\tavailableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain ?? undefined),\n\t};\n}\n\nfunction copilotEnterpriseDomain(credential: OAuthCredential): string | undefined {\n\tconst enterpriseUrl = credential.enterpriseUrl;\n\tif (typeof enterpriseUrl !== \"string\" || !enterpriseUrl) return undefined;\n\treturn normalizeDomain(enterpriseUrl) ?? undefined;\n}\n\nexport const githubCopilotOAuth: OAuthAuth = {\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks) {\n\t\tconst credentials = await loginGitHubCopilot({\n\t\t\tonDeviceCode: (info) => callbacks.notify({ type: \"device_code\", ...info }),\n\t\t\tonPrompt: (prompt) =>\n\t\t\t\tcallbacks.prompt({ type: \"text\", message: prompt.message, placeholder: prompt.placeholder }),\n\t\t\tonProgress: (message) => callbacks.notify({ type: \"progress\", message }),\n\t\t\tsignal: callbacks.signal,\n\t\t});\n\t\treturn { ...credentials, type: \"oauth\" };\n\t},\n\n\tasync refresh(credential) {\n\t\treturn {\n\t\t\t...(await refreshGitHubCopilotToken(credential.refresh, copilotEnterpriseDomain(credential))),\n\t\t\ttype: \"oauth\",\n\t\t};\n\t},\n\n\t/** Per-credential baseUrl from the token's proxy endpoint replaces the old `modifyModels` rewriting. */\n\tasync toAuth(credential) {\n\t\treturn {\n\t\t\tapiKey: credential.access,\n\t\t\tbaseUrl: getGitHubCopilotBaseUrl(credential.access, copilotEnterpriseDomain(credential)),\n\t\t};\n\t},\n};\n\nexport const githubCopilotOAuthProvider: OAuthProviderInterface = {\n\tid: \"github-copilot\",\n\tname: \"GitHub Copilot\",\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginGitHubCopilot({\n\t\t\tonDeviceCode: callbacks.onDeviceCode,\n\t\t\tonPrompt: callbacks.onPrompt,\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\tconst creds = credentials as CopilotCredentials;\n\t\treturn refreshGitHubCopilotToken(creds.refresh, creds.enterpriseUrl);\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 CopilotCredentials;\n\t\tconst domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined;\n\t\tconst baseUrl = getGitHubCopilotBaseUrl(creds.access, domain);\n\t\t// Older stored Pi auth entries do not have account-specific model IDs yet;\n\t\t// keep their existing generated-catalog behavior until the next refresh/login.\n\t\tconst availableModelIds = \"availableModelIds\" in creds ? new Set(creds.availableModelIds) : undefined;\n\n\t\treturn models.flatMap((m) => {\n\t\t\tif (m.provider !== \"github-copilot\") return [m];\n\t\t\tif (availableModelIds && !availableModelIds.has(m.id)) return [];\n\t\t\treturn [{ ...m, baseUrl }];\n\t\t});\n\t},\n};\n"]}
@@ -156,6 +156,7 @@ async function pollForGitHubAccessToken(domain, device, signal) {
156
156
  return pollOAuthDeviceCodeFlow({
157
157
  intervalSeconds: device.interval,
158
158
  expiresInSeconds: device.expires_in,
159
+ waitBeforeFirstPoll: true,
159
160
  signal,
160
161
  poll: async () => {
161
162
  const raw = await fetchJson(urls.accessTokenUrl, {
@@ -175,12 +176,12 @@ async function pollForGitHubAccessToken(domain, device, signal) {
175
176
  return { status: "complete", value: raw.access_token };
176
177
  }
177
178
  if (raw && typeof raw === "object" && typeof raw.error === "string") {
178
- const { error, error_description: description } = raw;
179
+ const { error, error_description: description, interval } = raw;
179
180
  if (error === "authorization_pending") {
180
181
  return { status: "pending" };
181
182
  }
182
183
  if (error === "slow_down") {
183
- return { status: "slow_down" };
184
+ return { status: "slow_down", intervalSeconds: typeof interval === "number" ? interval : undefined };
184
185
  }
185
186
  const descriptionSuffix = description ? `: ${description}` : "";
186
187
  return { status: "failed", message: `Device flow failed: ${error}${descriptionSuffix}` };