@alvin0/ai-agent-sdk-provider-copilot 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2680 @@
1
+ import { AgentSdkError, MISSING_CREDENTIAL_CODE, MODEL_ERROR_CODES, ModelError, safeErrorRecord, waitForSettlement } from "@alvin0/ai-agent-sdk-core";
2
+ import { AgentSdkError as AgentSdkError$1, CREDENTIAL_CAPABILITY_API_VERSION, defineCredentialStore, defineModelProviderPlugin } from "@alvin0/ai-agent-sdk-core/provider";
3
+ import { createRuntimeHttpProvider, defineWireProtocol, observeCredentialOperation } from "@alvin0/ai-agent-sdk-provider-http";
4
+ import { OPENAI_CHAT_COMPLETIONS_PROTOCOL_ID, openAiChatCompletionsProtocol } from "@alvin0/ai-agent-sdk-protocol-openai-chat-completions";
5
+ import { OPENAI_RESPONSES_PROTOCOL_ID, openAiResponsesProtocol } from "@alvin0/ai-agent-sdk-protocol-responses";
6
+
7
+ //#region src/common/error-codes.ts
8
+ /**
9
+ * The Copilot error codes, in the leaf layer so the shared HTTP modules can reach
10
+ * them.
11
+ *
12
+ * The taxonomy BELONGS to `../errors.ts` — that module is the public door, and it
13
+ * re-exports everything here. The definition lives one layer down for a structural
14
+ * reason: `common/` is a leaf, and `common/http.ts` needs
15
+ * `ENDPOINT_ORIGIN_INVALID` and `COPILOT_REDIRECT_REJECTED` to throw. Importing
16
+ * them from a root module would make `common/` depend on the root while the root
17
+ * already depends on `common/`, which is the source-ownership cycle the repo's
18
+ * package-graph check forbids. Duplicating the two strings instead would be worse:
19
+ * a code that exists in two places is a code that can disagree with itself.
20
+ *
21
+ * Read `../errors.ts` for the taxonomy's rationale, including the three situations
22
+ * that deliberately get an EXISTING SDK code rather than a Copilot one.
23
+ *
24
+ * @module ai-agent-sdk/providers/copilot/error-codes
25
+ */
26
+ /**
27
+ * Stable codes for the failures that are specific to Copilot.
28
+ *
29
+ * Frozen, and flat strings rather than a TS enum, for the same reason the core
30
+ * taxonomy is: a consumer routes on the value, and the value has to survive
31
+ * serialization into a log line.
32
+ */
33
+ const COPILOT_ERROR_CODES = Object.freeze({
34
+ /** The token-exchange surface rejected the credential: a PAT, or a non-allowlisted OAuth App. */
35
+ CREDENTIAL_REJECTED: "COPILOT_CREDENTIAL_REJECTED",
36
+ /** Token exchange failed for a reason that is not the credential. */
37
+ TOKEN_EXCHANGE_FAILED: "COPILOT_TOKEN_EXCHANGE_FAILED",
38
+ /** The token-exchange response carried no readable `expires_at`, or was not JSON. */
39
+ TOKEN_MALFORMED: "COPILOT_TOKEN_MALFORMED",
40
+ /** A `*.ghe.com` data-residency tenant has no token-exchange surface. */
41
+ TENANT_UNSUPPORTED: "COPILOT_TENANT_UNSUPPORTED",
42
+ /** The endpoint rejected the request for missing `Editor_Headers`. */
43
+ EDITOR_HEADERS_MISSING: "COPILOT_EDITOR_HEADERS_MISSING",
44
+ /** The target URL is not on the same origin as the configured issuer/base URL. */
45
+ ENDPOINT_ORIGIN_INVALID: "COPILOT_ENDPOINT_ORIGIN_INVALID",
46
+ /** The response was a redirect; this SDK does not follow it. */
47
+ REDIRECT_REJECTED: "COPILOT_REDIRECT_REJECTED",
48
+ /** Device flow: the user denied the request. */
49
+ DEVICE_LOGIN_DENIED: "COPILOT_DEVICE_LOGIN_DENIED",
50
+ /** Device flow: the code expired server-side. */
51
+ DEVICE_LOGIN_EXPIRED: "COPILOT_DEVICE_LOGIN_EXPIRED",
52
+ /** Device flow: the absolute 15-minute bound passed without approval. */
53
+ DEVICE_LOGIN_TIMEOUT: "COPILOT_DEVICE_LOGIN_TIMEOUT",
54
+ /** Device flow: failed for any other reason. */
55
+ DEVICE_LOGIN_FAILED: "COPILOT_DEVICE_LOGIN_FAILED",
56
+ /** A credential commit found a revision other than the expected one. */
57
+ CREDENTIAL_REVISION_CONFLICT: "COPILOT_CREDENTIAL_REVISION_CONFLICT",
58
+ /** The `/models` response was the wrong shape at the structural level. */
59
+ CATALOG_MALFORMED: "COPILOT_CATALOG_MALFORMED",
60
+ /** `endpointOverrides` pinned a model to an endpoint that does not exist. */
61
+ ENDPOINT_OVERRIDE_INVALID: "COPILOT_ENDPOINT_OVERRIDE_INVALID"
62
+ });
63
+
64
+ //#endregion
65
+ //#region src/errors.ts
66
+ /**
67
+ * The Copilot error taxonomy: the codes this provider owns, the two error
68
+ * classes that carry a machine-readable classification beside them, and the one
69
+ * door through which every credential-path error is built.
70
+ *
71
+ * ## What is deliberately NOT here
72
+ *
73
+ * Three situations get an EXISTING code rather than a Copilot one, because a
74
+ * second code for the same situation forces every consumer to write a second
75
+ * branch for it:
76
+ *
77
+ * - **No credential at all** — `MISSING_CREDENTIAL_CODE` from `packages/core`,
78
+ * with a message naming the login command (Requirement 13.4).
79
+ * - **Abort** — the SDK's existing abort code, {@link MODEL_ERROR_CODES.ABORTED}
80
+ * (Requirement 4.6). `CopilotDeviceLoginError` with `reason: 'aborted'` maps to
81
+ * it rather than minting a Copilot abort code.
82
+ * - **HTTP failures of the generation/embedding endpoints** — `MODEL_ERROR_CODES`
83
+ * plus `HTTP_PROVIDER_ERROR_CODES`. In particular there is no
84
+ * `COPILOT_RATE_LIMIT`: a 429 from Copilot is `RATE_LIMIT`, the same as from
85
+ * every other provider (Requirements 13.5, 15.5).
86
+ *
87
+ * @module ai-agent-sdk/providers/copilot/errors
88
+ */
89
+ /**
90
+ * Mirrors `safeProviderFailure` from `provider-http`: a {@link ModelError} keeps
91
+ * its stable code and status and LOSES its message, because a provider message
92
+ * is the one field that can have echoed a request header back at us.
93
+ * @param failure - the serializable twin carried by a {@link ModelError}.
94
+ * @returns a frozen record with no provider-authored text in it.
95
+ */
96
+ function safeModelFailure(failure) {
97
+ return Object.freeze({
98
+ type: "ModelError",
99
+ message: "provider attempt failed; inspect the stable code and request ID",
100
+ code: failure.code,
101
+ ...failure.status === void 0 ? {} : { status: failure.status }
102
+ });
103
+ }
104
+ /**
105
+ * Build the inputs for an error on the Copilot credential path.
106
+ *
107
+ * This is the ONLY door: {@link CopilotTokenExchangeError} and
108
+ * {@link CopilotDeviceLoginError} take a {@link CopilotCredentialFailure} rather
109
+ * than a raw `cause`, so there is no code path that can attach an unfiltered
110
+ * value to a credential-path error. `message` is SDK-authored text; a
111
+ * `GitHub_User_Token` or a `Copilot_Api_Token` is never interpolated into it, and
112
+ * response bodies reach `cause` only after the bounded read has replaced every
113
+ * occurrence of the tokens held in memory with `[REDACTED]` (Requirement 13.7).
114
+ * @param message - SDK-authored, actionable text. No token values.
115
+ * @param cause - the caught value, if any; filtered before it is retained.
116
+ * @returns the sanitized pair an error class accepts.
117
+ */
118
+ function credentialFailure(message, cause) {
119
+ return Object.freeze({
120
+ message,
121
+ cause: cause === void 0 ? void 0 : cause instanceof ModelError ? safeModelFailure(cause.failure) : safeErrorRecord(cause)
122
+ });
123
+ }
124
+ /**
125
+ * A `Copilot_Token_Exchange` that did not produce a token.
126
+ *
127
+ * `kind` exists because `code` alone does not answer the only question a caller
128
+ * has to answer next: `TOKEN_EXCHANGE_FAILED` covers both a 5xx worth waiting out
129
+ * and a 4xx that will fail identically forever.
130
+ */
131
+ var CopilotTokenExchangeError = class extends AgentSdkError {
132
+ /** Retry classification for this failure. */
133
+ kind;
134
+ /**
135
+ * @param failure - message and filtered cause from {@link credentialFailure}.
136
+ * @param code - the Copilot code for this row of the classification table.
137
+ * @param kind - whether a retry could ever succeed.
138
+ */
139
+ constructor(failure, code, kind) {
140
+ super(failure.message, code, failure.cause === void 0 ? void 0 : { cause: failure.cause });
141
+ this.kind = kind;
142
+ }
143
+ };
144
+ /**
145
+ * Codes for the four device-flow outcomes this provider owns.
146
+ *
147
+ * `denied` and `expired` are separate on purpose: "you just declined this" and
148
+ * "the code ran out" lead to different next steps. `aborted` is absent because it
149
+ * maps to the SDK's existing abort code instead.
150
+ */
151
+ const DEVICE_LOGIN_CODES = Object.freeze({
152
+ denied: COPILOT_ERROR_CODES.DEVICE_LOGIN_DENIED,
153
+ expired: COPILOT_ERROR_CODES.DEVICE_LOGIN_EXPIRED,
154
+ timeout: COPILOT_ERROR_CODES.DEVICE_LOGIN_TIMEOUT,
155
+ failed: COPILOT_ERROR_CODES.DEVICE_LOGIN_FAILED
156
+ });
157
+ /**
158
+ * A device login that ended without a `GitHub_User_Token`.
159
+ *
160
+ * The `code` is derived from `reason` rather than passed in, so the two can never
161
+ * disagree — a caller reading `code` and a caller reading `reason` always see the
162
+ * same outcome.
163
+ */
164
+ var CopilotDeviceLoginError = class extends AgentSdkError {
165
+ /** The distinguishable reason the flow ended. */
166
+ reason;
167
+ /**
168
+ * @param failure - message and filtered cause from {@link credentialFailure}.
169
+ * @param reason - the outcome; decides the `code`, with `aborted` mapping to
170
+ * {@link MODEL_ERROR_CODES.ABORTED} rather than a Copilot-specific code.
171
+ */
172
+ constructor(failure, reason) {
173
+ super(failure.message, reason === "aborted" ? MODEL_ERROR_CODES.ABORTED : DEVICE_LOGIN_CODES[reason], failure.cause === void 0 ? void 0 : { cause: failure.cause });
174
+ this.reason = reason;
175
+ }
176
+ };
177
+
178
+ //#endregion
179
+ //#region src/auth.ts
180
+ /**
181
+ * The Universal half of `Copilot_Auth`: the in-memory store doubles, the
182
+ * credential snapshot an operation carries, the one function that turns "no
183
+ * credential" into an actionable error, and the pure predicate that decides
184
+ * whether a token exchange is due.
185
+ *
186
+ * Storage is injected. Paths, the filesystem, and the environment belong to the
187
+ * Node auth package, never this Universal one (Requirement 6.1).
188
+ *
189
+ * ## Two tiers, one of which lives here
190
+ *
191
+ * The long-lived `GitHub_User_Token` is what a store persists; the short-lived
192
+ * `Copilot_Api_Token` obtained from it never reaches a store and lives only in
193
+ * the process cache (Requirements 3.1, 3.3). Exchanging does not consume the
194
+ * long-lived token, so the persisted value is left exactly as it was
195
+ * (Requirement 3.4) — this module has no write path at all for that reason.
196
+ *
197
+ * @module ai-agent-sdk/providers/copilot/auth
198
+ */
199
+ /**
200
+ * The command that produces a credential, named in every message that tells a
201
+ * caller how to fix a credential problem.
202
+ *
203
+ * Exported so the token-exchange path names the SAME command: a 401 and a 403
204
+ * there both end in "sign in again", and two copies of that string are two
205
+ * strings that can drift apart.
206
+ */
207
+ const COPILOT_LOGIN_COMMAND = "npm run provider:copilot:login-device";
208
+ /**
209
+ * An in-memory {@link CopilotAuthStore} — the read/write variant (Requirement 6.4).
210
+ *
211
+ * The read/write variant exists for symmetry with Codex; normal runtime
212
+ * composition uses {@link memoryCopilotCredentialStore}, the compare-and-swap
213
+ * variant.
214
+ * @param initial - the file the store starts with, or nothing for an empty store.
215
+ * @returns a store backed by a single mutable slot.
216
+ */
217
+ function memoryCopilotAuthStore(initial) {
218
+ let current = initial;
219
+ return {
220
+ location: "<memory>",
221
+ read: () => Promise.resolve(current),
222
+ write: (file) => {
223
+ current = file;
224
+ return Promise.resolve();
225
+ }
226
+ };
227
+ }
228
+ /**
229
+ * An in-memory compare-and-swap store, for deterministic runtime and tests
230
+ * (Requirements 6.2, 6.4).
231
+ *
232
+ * Values are `structuredClone`d in BOTH directions, which is the point of this
233
+ * double: a caller that mutates the object it wrote, or the object it read, must
234
+ * not be able to change what the store holds. Without the clone a test could pass
235
+ * for the wrong reason — the store and the caller sharing one object rather than
236
+ * the store having committed anything.
237
+ *
238
+ * A commit whose `expectedRevision` disagrees with the current revision raises
239
+ * {@link COPILOT_ERROR_CODES.CREDENTIAL_REVISION_CONFLICT} (Requirement 6.3), the
240
+ * Copilot-owned code, so exactly one of two concurrent commits wins and the loser
241
+ * can tell why it lost.
242
+ * @param initial - the file the store starts with, or nothing for an empty store.
243
+ * @returns a compare-and-swap store over a single mutable slot.
244
+ */
245
+ function memoryCopilotCredentialStore(initial) {
246
+ let current = initial === void 0 ? void 0 : structuredClone(initial);
247
+ let revision = 0;
248
+ return defineCredentialStore({
249
+ id: "copilot-memory-credentials",
250
+ label: "<memory>",
251
+ async read({ signal }) {
252
+ signal.throwIfAborted();
253
+ return current === void 0 ? void 0 : {
254
+ value: structuredClone(current),
255
+ revision: String(revision)
256
+ };
257
+ },
258
+ async commit(input, { signal }) {
259
+ signal.throwIfAborted();
260
+ const expected = current === void 0 ? null : String(revision);
261
+ if (input.expectedRevision !== expected) throw new AgentSdkError("Copilot credential revision changed before commit", COPILOT_ERROR_CODES.CREDENTIAL_REVISION_CONFLICT);
262
+ current = structuredClone(input.value);
263
+ revision++;
264
+ return { revision: String(revision) };
265
+ }
266
+ });
267
+ }
268
+ /**
269
+ * Require a usable `GitHub_User_Token`, with a message that says how to get one.
270
+ *
271
+ * Three shapes of "there is no credential" — an empty store, a file with no
272
+ * `github` field, and a `github.token` that is the empty string — collapse into
273
+ * the SAME code, because to the person reading the error they are one problem
274
+ * with one fix. That code is the SDK's own {@link MISSING_CREDENTIAL_CODE} rather
275
+ * than a Copilot-specific one, so a consumer does not have to write a second
276
+ * branch for a situation it already handles, and the message carries the command
277
+ * to run `Copilot_Login_Cli` (Requirement 13.4).
278
+ *
279
+ * The token value is never interpolated into the message; only the store's label
280
+ * is (Requirement 13.7).
281
+ * @param file - the credential file, or `undefined` when the store was empty.
282
+ * @param label - the store location named in the diagnostic.
283
+ * @returns the long-lived GitHub token.
284
+ */
285
+ function requireGitHubToken(file, label) {
286
+ const github = file?.github;
287
+ if (github === void 0 || github === null || typeof github.token !== "string" || github.token.length === 0) throw new AgentSdkError(`no GitHub Copilot credentials at ${label}; run \`${COPILOT_LOGIN_COMMAND}\` to sign in`, MISSING_CREDENTIAL_CODE);
288
+ return github;
289
+ }
290
+ /** Exchange this long before the `Copilot_Api_Token` actually expires. */
291
+ const COPILOT_TOKEN_EXCHANGE_MARGIN_MS = 3e5;
292
+ /**
293
+ * Whether a token exchange has to happen before the next request.
294
+ *
295
+ * A pure function of three values that reads no global clock, so a property test
296
+ * can place `now` at every boundary without a fake timer (Requirements 5.2, 5.3).
297
+ * `undefined` — no token yet — is always `true`.
298
+ *
299
+ * `expires_at` is the authority and `refresh_in` is advisory: the hint may only
300
+ * SHORTEN the refresh moment, never lengthen it. The endpoint is allowed to ask
301
+ * for an earlier exchange; it is not allowed to ask this SDK to hold a token past
302
+ * the expiry it announced itself.
303
+ *
304
+ * There is deliberately no fallback branch. `shouldRefresh` in
305
+ * `provider-codex/src/auth.ts` decodes a JWT for `exp` and falls back to a
306
+ * `last_refresh` age when it cannot; a `Copilot_Api_Token` is not a JWT this SDK
307
+ * has any business reading, and the expiry is stated outright in the exchange
308
+ * response body. With no second source, a fallback would have to invent a
309
+ * lifetime, and an invented lifetime violates the no-inference principle.
310
+ *
311
+ * Because the decision is made BEFORE dispatch, a 401 from the Copilot base URL
312
+ * always means the credential is genuinely dead rather than "the token expired
313
+ * mid-flight" — which is what lets auth failures stay non-retryable
314
+ * (Requirement 5.8).
315
+ * @param api - the cached token, or `undefined` when there is none.
316
+ * @param now - current time in epoch milliseconds.
317
+ * @param marginMs - exchange this long before expiry.
318
+ * @returns true when an exchange is due.
319
+ */
320
+ function shouldExchange(api, now, marginMs = COPILOT_TOKEN_EXCHANGE_MARGIN_MS) {
321
+ if (api === void 0) return true;
322
+ const advisory = api.refreshInSeconds === void 0 ? Number.POSITIVE_INFINITY : api.expiresAtMs - api.refreshInSeconds * 1e3;
323
+ return Math.min(api.expiresAtMs - marginMs, advisory) <= now;
324
+ }
325
+
326
+ //#endregion
327
+ //#region src/common/identity.ts
328
+ /**
329
+ * The two Copilot endpoint constants and the editor-header override type, in the
330
+ * leaf layer so every module that has to speak to the Copilot surface can reach
331
+ * them.
332
+ *
333
+ * They BELONG to `../adapter.ts` — that module is the public door, documents the
334
+ * client-identity tradeoff, and re-exports everything here. The values live one
335
+ * layer down for the same structural reason `COPILOT_ERROR_CODES` does:
336
+ * `../catalog.ts` needs {@link COPILOT_BASE_URL} and `../exchange.ts` needs the
337
+ * two editor headers, while `../adapter.ts` builds the catalog reader and the
338
+ * token cache. Declaring the constants in `../adapter.ts` would make that edge
339
+ * run both ways, which the repo's circular-dependency check forbids. Copying the
340
+ * strings instead would be worse: a client identity that exists in two places is
341
+ * a client identity that can disagree with itself.
342
+ *
343
+ * Read `../adapter.ts` for what these values mean, why they are overridable
344
+ * options rather than hidden constants, and what is still outstanding on each.
345
+ *
346
+ * @module ai-agent-sdk/providers/copilot/identity
347
+ */
348
+ /** The Copilot API base. Documented on the re-export in `../adapter.ts`. */
349
+ const COPILOT_BASE_URL = "https://api.githubcopilot.com";
350
+ /** Default `Editor-Version`. Documented on the re-export in `../adapter.ts`. */
351
+ const COPILOT_EDITOR_VERSION = "vscode/1.99.0";
352
+ /** Default `Editor-Plugin-Version`. Documented on the re-export in `../adapter.ts`. */
353
+ const COPILOT_EDITOR_PLUGIN_VERSION = "copilot-chat/0.26.0";
354
+ /**
355
+ * Resolve each editor header from the override first and the exported default
356
+ * second.
357
+ *
358
+ * Per field rather than per object: an override of one header leaves the other at
359
+ * its default instead of dropping it, because a dropped editor header is an HTTP
360
+ * 400 (Requirement 2.4).
361
+ * @param headers - the caller's overrides, when they set any.
362
+ * @returns both header values, neither of them empty.
363
+ */
364
+ function resolveCopilotEditorHeaders(headers) {
365
+ return Object.freeze({
366
+ editorVersion: headers?.editorVersion ?? "vscode/1.99.0",
367
+ editorPluginVersion: headers?.editorPluginVersion ?? "copilot-chat/0.26.0"
368
+ });
369
+ }
370
+
371
+ //#endregion
372
+ //#region src/common/no-follow.ts
373
+ /**
374
+ * The redirect guard every Copilot HTTP call passes through.
375
+ *
376
+ * `redirect: 'manual'` on the request is only half of a no-follow policy: it
377
+ * stops the runtime from following a hop, but it does not stop the CALLER from
378
+ * treating the result as a normal response. This module is the other half — it
379
+ * turns every shape a redirect can take into a structured error before a second
380
+ * request can be dispatched.
381
+ *
382
+ * There are four shapes, and a check for only one of them is a hole:
383
+ *
384
+ * - **A 3xx status** — the ordinary case, visible because `redirect: 'manual'`
385
+ * surfaces the response instead of following it.
386
+ * - **`type === 'opaqueredirect'`** — what a browser returns instead of the 3xx,
387
+ * with the status flattened to `0` and the headers stripped. A status-only
388
+ * check misses this entirely.
389
+ * - **`redirected === true`** — a hop that was already followed, by a runtime or
390
+ * an intermediary that ignored `redirect: 'manual'`.
391
+ * - **`response.url` differing from the requested URL** — the last resort, for a
392
+ * runtime that reports neither of the flags above but still moved the request.
393
+ *
394
+ * The body is RELEASED before the error is thrown. A rejected response whose body
395
+ * is never cancelled holds a socket open for as long as the runtime keeps the
396
+ * stream alive, so the guard cannot leave that to the caller's `finally`.
397
+ *
398
+ * @module ai-agent-sdk/providers/copilot/no-follow
399
+ */
400
+ /**
401
+ * Reject every redirect shape Web fetch exposes, before any second request.
402
+ *
403
+ * @param response - the response as returned by a `redirect: 'manual'` fetch.
404
+ * @param requestedUrl - the absolute URL that was requested, for the
405
+ * `response.url` comparison.
406
+ * @param operation - which Copilot call site is refusing the hop.
407
+ * @param teardownTimeoutMs - bound on the body cancellation, so a stream that
408
+ * never settles cannot hold the rejection open forever.
409
+ * @returns nothing when the response is not a redirect in any of its four shapes.
410
+ * @throws AgentSdkError with `COPILOT_REDIRECT_REJECTED` when it is.
411
+ */
412
+ async function rejectCopilotRedirect(response, requestedUrl, operation, teardownTimeoutMs) {
413
+ const redirectStatus = response.status >= 300 && response.status < 400;
414
+ const responseUrlChanged = response.url.length > 0 && response.url !== requestedUrl;
415
+ if (response.type !== "opaqueredirect" && response.redirected !== true && !redirectStatus && !responseUrlChanged) return;
416
+ if (response.body !== null) await waitForSettlement(response.body.cancel().catch(() => void 0), teardownTimeoutMs);
417
+ throw new AgentSdkError(`Copilot ${operation} rejected a redirect before following it`, COPILOT_ERROR_CODES.REDIRECT_REJECTED);
418
+ }
419
+
420
+ //#endregion
421
+ //#region src/common/http.ts
422
+ /**
423
+ * The one HTTP door for every Copilot call: origin pinning, no-follow, bounded
424
+ * reads, and a caller signal that wins immediately.
425
+ *
426
+ * This is the counterpart of `oauthFetch` in `provider-codex/src/oauth.ts`, kept
427
+ * deliberately close to it — the same guarantees, in the same order, so a reader
428
+ * who knows one knows the other. What differs is scope: Codex has one auth
429
+ * issuer, Copilot has THREE origins, and each is its own option.
430
+ *
431
+ * ## Three origins, three independent pins
432
+ *
433
+ * `oauthIssuer` (`https://github.com`), `githubApiBaseUrl`
434
+ * (`https://api.github.com`) and `baseUrl` (`https://api.githubcopilot.com`) are
435
+ * three separate options, pinned separately by three separate calls to {@link
436
+ * issuerOf}. No module may dispatch a request to an origin other than its own
437
+ * pinned one — the device flow cannot reach the Copilot surface, the Copilot
438
+ * surface cannot reach the token exchange. That is why {@link copilotFetch}
439
+ * demands a {@link CopilotOrigin} rather than reading an origin off a shared
440
+ * options bag: there is no options bag that holds all three, so there is no way
441
+ * to pass the wrong one by forgetting which field applies.
442
+ *
443
+ * The pin is compared BEFORE the request is dispatched (Requirement 3.7). A
444
+ * post-hoc check on the response would already have leaked the `Authorization`
445
+ * header to whatever origin the URL named.
446
+ *
447
+ * ## What is bounded, and why each bound exists
448
+ *
449
+ * - **`redirect: 'manual'` plus {@link rejectCopilotRedirect}** — a followed hop
450
+ * re-sends the credential headers to the redirect target (Requirements 3.8, 7.8).
451
+ * - **A per-request deadline** — a server that accepts the connection and then
452
+ * says nothing must not hang a CLI.
453
+ * - **Bytes AND chunk count on every read** — bytes alone still lets a stream of
454
+ * one-byte chunks pin the event loop, so both are checked (Requirements 4.7, 13.6).
455
+ * - **{@link raceAbort}** — `fetch` honours a signal, but a pending read does not
456
+ * necessarily reject the instant it aborts. Racing makes the caller's signal win
457
+ * immediately rather than eventually (Requirement 4.6).
458
+ * - **{@link positiveSafeInteger} on every configured limit** — a `0`, a `NaN` or
459
+ * a float silently disables a bound, which is worse than rejecting the config.
460
+ *
461
+ * @module ai-agent-sdk/providers/copilot/http
462
+ */
463
+ /** Deadline for one Copilot HTTP request when the caller configures none. */
464
+ const COPILOT_DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
465
+ /** Maximum response bytes retained or parsed when the caller configures none. */
466
+ const COPILOT_DEFAULT_MAX_RESPONSE_BYTES = 1048576;
467
+ /** Maximum response chunks accepted when the caller configures none. */
468
+ const COPILOT_DEFAULT_MAX_RESPONSE_CHUNKS = 1e4;
469
+ /** Bound on releasing a body that is being discarded. Never a caller-visible wait. */
470
+ const TEARDOWN_TIMEOUT_MS = 3e4;
471
+ /**
472
+ * Validate and pin one of the three configurable origins.
473
+ *
474
+ * Two rejections, each for a concrete reason:
475
+ *
476
+ * - **Userinfo** (`https://user:pass@host`) — credentials in a URL would be sent
477
+ * as an extra `Authorization` header the caller never wrote, and they end up in
478
+ * logs. There is no legitimate use for them on any of these three origins.
479
+ * - **`http:` without `allowInsecureIssuer`** — see the option's note.
480
+ *
481
+ * @param field - which option is being pinned; appears in the error message.
482
+ * @param configured - the caller's value, or `undefined` to take the default.
483
+ * @param fallback - the exported default for this field.
484
+ * @param options - read for `allowInsecureIssuer` only.
485
+ * @returns the pin to hand to {@link copilotFetch}.
486
+ * @throws AgentSdkError with `COPILOT_ENDPOINT_ORIGIN_INVALID` when the value is
487
+ * unparsable, carries userinfo, or is cleartext without the opt-in.
488
+ */
489
+ function issuerOf(field, configured, fallback, options = {}) {
490
+ const raw = configured ?? fallback;
491
+ let url;
492
+ try {
493
+ url = new URL(raw);
494
+ } catch (error) {
495
+ throw originError(`Copilot ${field} is not an absolute URL`, error);
496
+ }
497
+ if (url.username.length > 0 || url.password.length > 0) throw originError(`Copilot ${field} must not contain credentials`);
498
+ if (url.protocol !== "https:" && !(options.allowInsecureIssuer === true && url.protocol === "http:")) throw originError(`Copilot ${field} must use https unless allowInsecureIssuer is enabled`);
499
+ return Object.freeze({
500
+ field,
501
+ href: url.href.replace(/\/+$/, ""),
502
+ origin: url.origin
503
+ });
504
+ }
505
+ /**
506
+ * Build an absolute URL on a pinned origin.
507
+ *
508
+ * The path has to be absolute-and-rooted: a relative path resolved against a base
509
+ * is exactly how a URL quietly ends up somewhere else, and a path that is itself
510
+ * absolute (`//evil.tld/x` or `https://evil.tld/x`) would replace the origin
511
+ * outright.
512
+ * @param pinned - the pin from {@link issuerOf}.
513
+ * @param path - a path beginning with a single `/`.
514
+ * @returns the absolute URL string, guaranteed to be on `pinned.origin`.
515
+ * @throws AgentSdkError with `COPILOT_ENDPOINT_ORIGIN_INVALID` when the path could
516
+ * move the request off the pin.
517
+ */
518
+ function copilotUrl(pinned, path) {
519
+ if (!path.startsWith("/") || path.startsWith("//")) throw originError(`Copilot ${pinned.field} path must start with a single '/'`);
520
+ const url = new URL(`${pinned.href}${path}`);
521
+ if (url.origin !== pinned.origin) throw originError(`Copilot ${pinned.field} path must stay on the pinned origin`);
522
+ return url.href;
523
+ }
524
+ /**
525
+ * Dispatch one Copilot request with the origin pin, no-follow and deadline applied.
526
+ *
527
+ * Order matters and is the contract: the pin is compared FIRST, so a URL on the
528
+ * wrong origin never receives the headers; then the request goes out with
529
+ * `redirect: 'manual'`; then the response passes the redirect guard before it is
530
+ * handed back. The body is left unread — {@link readCopilotResponseText} is the
531
+ * bounded reader for it.
532
+ * @param request - the pin, the URL, the call site and the init.
533
+ * @param options - signal, fetch implementation and limits.
534
+ * @returns the response, already cleared by the redirect guard.
535
+ * @throws AgentSdkError with `COPILOT_ENDPOINT_ORIGIN_INVALID` when the URL is off
536
+ * the pin, or `COPILOT_REDIRECT_REJECTED` when the response was a redirect.
537
+ */
538
+ async function copilotFetch(request, options = {}) {
539
+ const url = requestUrl(request);
540
+ const timeoutMs = positiveSafeInteger(options.requestTimeoutMs ?? 3e4, "requestTimeoutMs");
541
+ const timeout = AbortSignal.timeout(timeoutMs);
542
+ const signal = options.signal === void 0 ? timeout : AbortSignal.any([options.signal, timeout]);
543
+ const fetchImpl = options.fetch ?? globalThis.fetch;
544
+ if (typeof fetchImpl !== "function") throw new TypeError("Copilot HTTP requires fetch");
545
+ const response = await raceAbort(Promise.resolve(fetchImpl(url, {
546
+ ...request.init,
547
+ signal,
548
+ redirect: "manual"
549
+ })), signal);
550
+ await rejectCopilotRedirect(response, url, request.operation, TEARDOWN_TIMEOUT_MS);
551
+ return response;
552
+ }
553
+ /**
554
+ * Read a response body as text, bounded on bytes and on chunk count.
555
+ *
556
+ * A declared `content-length` over the limit is refused before a single chunk is
557
+ * read; the running totals then catch a body that lies about its length or sends
558
+ * none. Either way the reader is cancelled rather than abandoned.
559
+ * @param response - a response already cleared by {@link copilotFetch}.
560
+ * @param options - limits and the signal to race the read against.
561
+ * @returns the decoded text, or `''` when there was no body.
562
+ * @throws RangeError when a configured bound is exceeded.
563
+ */
564
+ async function readCopilotResponseText(response, options = {}) {
565
+ const maxBytes = positiveSafeInteger(options.maxResponseBytes ?? 1048576, "maxResponseBytes");
566
+ const maxChunks = positiveSafeInteger(options.maxResponseChunks ?? 1e4, "maxResponseChunks");
567
+ const declared = Number(response.headers.get("content-length"));
568
+ if (Number.isFinite(declared) && declared > maxBytes) {
569
+ if (response.body !== null) await waitForSettlement(response.body.cancel().catch(() => void 0), TEARDOWN_TIMEOUT_MS);
570
+ throw new RangeError(`Copilot HTTP response exceeds the ${maxBytes}-byte limit`);
571
+ }
572
+ if (response.body === null) return "";
573
+ const timeout = AbortSignal.timeout(positiveSafeInteger(options.requestTimeoutMs ?? 3e4, "requestTimeoutMs"));
574
+ const signal = options.signal === void 0 ? timeout : AbortSignal.any([options.signal, timeout]);
575
+ const reader = response.body.getReader();
576
+ const decoder = new TextDecoder();
577
+ let bytes = 0;
578
+ let chunks = 0;
579
+ let result = "";
580
+ try {
581
+ while (true) {
582
+ const next = await raceAbort(reader.read(), signal);
583
+ if (next.done) return result + decoder.decode();
584
+ if (next.value === void 0) continue;
585
+ chunks++;
586
+ bytes += next.value.byteLength;
587
+ if (chunks > maxChunks || bytes > maxBytes) {
588
+ await waitForSettlement(reader.cancel().catch(() => void 0), TEARDOWN_TIMEOUT_MS);
589
+ throw new RangeError("Copilot HTTP response exceeds its configured resource limit");
590
+ }
591
+ result += decoder.decode(next.value, { stream: true });
592
+ }
593
+ } finally {
594
+ reader.releaseLock();
595
+ }
596
+ }
597
+ /**
598
+ * Settle as soon as either the pending work or the signal does.
599
+ *
600
+ * An already-aborted signal rejects synchronously rather than after one turn, so
601
+ * a caller who aborts before the call never dispatches the request at all.
602
+ * @param pending - the work to race.
603
+ * @param signal - the signal that gets to win.
604
+ * @returns the pending value, when it arrives first.
605
+ */
606
+ function raceAbort(pending, signal) {
607
+ if (signal.aborted) return Promise.reject(abortReason(signal));
608
+ return new Promise((resolve, reject) => {
609
+ const abort = () => {
610
+ cleanup();
611
+ reject(abortReason(signal));
612
+ };
613
+ const cleanup = () => signal.removeEventListener("abort", abort);
614
+ signal.addEventListener("abort", abort, { once: true });
615
+ pending.then((value) => {
616
+ cleanup();
617
+ resolve(value);
618
+ }, (error) => {
619
+ cleanup();
620
+ reject(error);
621
+ });
622
+ });
623
+ }
624
+ /**
625
+ * Accept a configured limit only when it can actually bound anything.
626
+ *
627
+ * `0`, a negative, a float and `NaN` all disable a bound silently, so each one is
628
+ * rejected instead of normalized.
629
+ * @param value - the configured number.
630
+ * @param field - the option name, for the message.
631
+ * @returns the value, unchanged.
632
+ * @throws RangeError when the value cannot serve as a bound.
633
+ */
634
+ function positiveSafeInteger(value, field) {
635
+ if (!Number.isSafeInteger(value) || value < 1) throw new RangeError(`Copilot HTTP ${field} must be a positive safe integer`);
636
+ return value;
637
+ }
638
+ /**
639
+ * Resolve the request URL and compare it against the pin, before anything is sent.
640
+ *
641
+ * Userinfo is rejected here as well as in {@link issuerOf}: `URL.origin` ignores
642
+ * it, so a URL on the right origin can still carry credentials the caller never
643
+ * intended to send.
644
+ */
645
+ function requestUrl(request) {
646
+ let url;
647
+ try {
648
+ url = new URL(request.url);
649
+ } catch (error) {
650
+ throw originError(`Copilot ${request.operation} target is not an absolute URL`, error);
651
+ }
652
+ if (url.username.length > 0 || url.password.length > 0) throw originError(`Copilot ${request.operation} target must not contain credentials`);
653
+ if (url.origin !== request.pinned.origin) throw originError(`Copilot ${request.operation} target origin '${url.origin}' is not the pinned ${request.pinned.field} origin '${request.pinned.origin}'`);
654
+ return url.href;
655
+ }
656
+ function originError(message, cause) {
657
+ return new AgentSdkError(message, COPILOT_ERROR_CODES.ENDPOINT_ORIGIN_INVALID, cause === void 0 ? void 0 : { cause });
658
+ }
659
+ function abortReason(signal) {
660
+ return signal.reason ?? new AgentSdkError("Copilot HTTP request aborted", MODEL_ERROR_CODES.ABORTED);
661
+ }
662
+
663
+ //#endregion
664
+ //#region src/catalog.ts
665
+ /**
666
+ * `Copilot_Catalog`: read `GET /models`, then PARTITION what came back.
667
+ *
668
+ * Discovery is the right default for this surface (Requirement 8.1): which models
669
+ * an account may call depends on its plan, on its organisation's policy, and on
670
+ * the editor identity the request presents, so no hardcoded list is correct for
671
+ * two accounts at once. Passing `models` explicitly skips discovery entirely
672
+ * (Requirement 8.5) — that decision belongs to the adapter, which simply does not
673
+ * call this module in that case.
674
+ *
675
+ * ## Two levels of wrongness, two different answers
676
+ *
677
+ * The defensive read runs in a fixed order, and the order IS the contract:
678
+ *
679
+ * ```text
680
+ * 1. redirect (every shape) ⇒ COPILOT_REDIRECT_REJECTED
681
+ * 2. declared content-length over the limit ⇒ RangeError, body cancelled
682
+ * 3. accumulated bytes/chunks over the limit ⇒ RangeError, reader cancelled
683
+ * 4. body is not JSON, root is not an object,
684
+ * or `data` is not an array ⇒ COPILOT_CATALOG_MALFORMED
685
+ * 5. entry count over maxCatalogModels ⇒ COPILOT_CATALOG_MALFORMED
686
+ * 6. entry: id is not a non-empty string ⇒ omitted 'model-id-missing'
687
+ * 7. entry: capabilities.type unrecognized ⇒ omitted 'capability-type-unrecognized'
688
+ * ```
689
+ *
690
+ * Steps 4 and 5 are STRUCTURAL, and a structural mismatch is an error rather than
691
+ * a starting point for a guess (Requirement 8.8): a model list inferred from a
692
+ * body this SDK could not read is a list nobody can be held to. Steps 6 and 7 are
693
+ * at ENTRY level, and there the entry is dropped while the rest of the catalog
694
+ * survives — one unfamiliar entry must not kill every model that still works.
695
+ *
696
+ * Dropping rather than listing-with-a-flag is the same judgement in the other
697
+ * direction (Requirements 9.4, 9.5): listing a model this SDK cannot dispatch is
698
+ * worse than not listing it, because it shows up in a selector and then fails at
699
+ * call time, far from the cause.
700
+ *
701
+ * ## Metadata is translated, never invented
702
+ *
703
+ * Every field of {@link ProviderCatalogModel} is filled only from a field the
704
+ * endpoint actually supplied (Requirement 8.4). The trap is
705
+ * `inputModalities`: with no vision signal at all the field is ABSENT, NOT
706
+ * `['text']`. An explicit list without `image` is a NEGATIVE claim the registry
707
+ * acts on — it projects images to text — so inventing `['text']` would silently
708
+ * strip images from every request to a model that may well accept them. Absent
709
+ * means unknown, and unknown is what the endpoint said.
710
+ *
711
+ * `declaredEndpoint` follows the same rule and stays `undefined` when the catalog
712
+ * discloses nothing. `undefined` is NOT "not supported"; the router treats the two
713
+ * states differently (Requirement 8.6).
714
+ *
715
+ * ## The catalog is advisory
716
+ *
717
+ * `omitted` does not fail anything. A dispatched request to an omitted id still
718
+ * goes out — it just takes the router's default branch — and a real error from the
719
+ * endpoint remains the final word whenever metadata and behaviour disagree
720
+ * (Requirement 8.6).
721
+ *
722
+ * @module ai-agent-sdk/providers/copilot/catalog
723
+ */
724
+ /** Path of the catalog surface, relative to the pinned Copilot base URL. */
725
+ const COPILOT_CATALOG_PATH = "/models";
726
+ /** Maximum raw catalog bytes when the caller configures none. */
727
+ const COPILOT_DEFAULT_MAX_CATALOG_BYTES = 4194304;
728
+ /** Maximum catalog entries accepted when the caller configures none. */
729
+ const COPILOT_DEFAULT_MAX_CATALOG_MODELS = 2048;
730
+ /** Maximum catalog response chunks accepted when the caller configures none. */
731
+ const COPILOT_DEFAULT_MAX_CATALOG_CHUNKS = 1e4;
732
+ /** Catalog request deadline when the caller configures none. */
733
+ const COPILOT_DEFAULT_CATALOG_TIMEOUT_MS = 3e4;
734
+ /**
735
+ * Resolve the per-read bounds, rejecting a value that cannot bound anything.
736
+ *
737
+ * Validation happens here rather than at the read, so a `0` or a `NaN` in the
738
+ * configuration is a construction-time error instead of a silently disabled limit
739
+ * discovered under load (Requirement 8.2).
740
+ * @param options - the caller's catalog options.
741
+ * @returns the four resolved bounds plus the insecure-HTTP opt-in.
742
+ * @throws RangeError when a configured bound is not a positive safe integer.
743
+ */
744
+ function resolveCopilotCatalogLimits(options = {}) {
745
+ return Object.freeze({
746
+ maxBytes: positiveSafeInteger(options.maxCatalogBytes ?? 4194304, "maxCatalogBytes"),
747
+ maxModels: positiveSafeInteger(options.maxCatalogModels ?? 2048, "maxCatalogModels"),
748
+ maxChunks: positiveSafeInteger(options.maxCatalogChunks ?? 1e4, "maxCatalogChunks"),
749
+ timeoutMs: positiveSafeInteger(options.catalogTimeoutMs ?? 3e4, "catalogTimeoutMs"),
750
+ ...options.allowInsecureHttp === void 0 ? {} : { allowInsecureHttp: options.allowInsecureHttp }
751
+ });
752
+ }
753
+ /**
754
+ * Forward the three cache-policy options, and only the ones that were set.
755
+ *
756
+ * A conditional spread rather than defaults: `provider-http` owns catalog caching,
757
+ * and a default written here would override the runtime's own without anyone
758
+ * asking for it (Requirement 8.7).
759
+ * @param options - the caller's catalog options.
760
+ * @returns an object carrying only the cache options the caller supplied.
761
+ */
762
+ function copilotCatalogCacheOptions(options = {}) {
763
+ return {
764
+ ...options.catalogTtlMs === void 0 ? {} : { catalogTtlMs: options.catalogTtlMs },
765
+ ...options.catalogStaleTtlMs === void 0 ? {} : { catalogStaleTtlMs: options.catalogStaleTtlMs },
766
+ ...options.catalogFailureBackoffMs === void 0 ? {} : { catalogFailureBackoffMs: options.catalogFailureBackoffMs }
767
+ };
768
+ }
769
+ /**
770
+ * Read `GET {baseUrl}/models` and partition it.
771
+ *
772
+ * The base URL is re-pinned here from `context.baseUrl` rather than trusted as a
773
+ * string: the catalog is the first Copilot call an adapter makes, and a pin
774
+ * compared before dispatch is the only check that runs before the resolved
775
+ * `Authorization` header leaves the process.
776
+ * @param context - the discovery context `provider-http` supplies: base URL,
777
+ * already-resolved headers, and the operation's signal.
778
+ * @param limits - bounds from {@link resolveCopilotCatalogLimits}.
779
+ * @param fetchImpl - HTTP implementation, injected for tests and non-browser runtimes.
780
+ * @returns the partitioned snapshot; an empty one when the endpoint answered a
781
+ * non-2xx status, because a catalog that could not be fetched is advisory too.
782
+ * @throws AgentSdkError with `COPILOT_REDIRECT_REJECTED` on any redirect shape, or
783
+ * `COPILOT_CATALOG_MALFORMED` when the response is the wrong shape structurally.
784
+ * @throws RangeError when the response exceeds a configured bound.
785
+ */
786
+ async function discoverCopilotModels(context, limits, fetchImpl) {
787
+ const pinned = issuerOf("baseUrl", context.baseUrl.href, COPILOT_BASE_URL, { ...limits.allowInsecureHttp === void 0 ? {} : { allowInsecureIssuer: limits.allowInsecureHttp } });
788
+ const url = copilotUrl(pinned, COPILOT_CATALOG_PATH);
789
+ const http = {
790
+ signal: context.signal,
791
+ fetch: fetchImpl,
792
+ requestTimeoutMs: limits.timeoutMs,
793
+ maxResponseBytes: limits.maxBytes,
794
+ maxResponseChunks: limits.maxChunks,
795
+ ...limits.allowInsecureHttp === void 0 ? {} : { allowInsecureIssuer: limits.allowInsecureHttp }
796
+ };
797
+ const response = await copilotFetch({
798
+ pinned,
799
+ url,
800
+ operation: "model catalog",
801
+ init: {
802
+ method: "GET",
803
+ headers: context.headers
804
+ }
805
+ }, http);
806
+ if (!response.ok) {
807
+ if (response.body !== null) await response.body.cancel().catch(() => void 0);
808
+ return EMPTY_SNAPSHOT;
809
+ }
810
+ return partitionCopilotCatalog(parseCatalogBody(await readCopilotResponseText(response, http)), limits.maxModels);
811
+ }
812
+ /**
813
+ * Partition an already-read catalog body.
814
+ *
815
+ * Exported separately from the fetch so the partition is testable — and readable —
816
+ * as what it is: a pure function from a parsed body to three lists.
817
+ * @param body - the parsed root object of the catalog response.
818
+ * @param maxModels - entry-count ceiling; exceeding it is structural, not per-entry.
819
+ * @returns the partitioned snapshot.
820
+ * @throws AgentSdkError with `COPILOT_CATALOG_MALFORMED` when `data` is not an
821
+ * array or holds more than `maxModels` entries.
822
+ */
823
+ function partitionCopilotCatalog(body, maxModels) {
824
+ const data = body.data;
825
+ if (!Array.isArray(data)) throw malformed$1("Copilot model catalog `data` must be an array");
826
+ if (data.length > maxModels) throw malformed$1(`Copilot model catalog exceeds the ${maxModels}-model limit`);
827
+ const generation = [];
828
+ const embedding = [];
829
+ const omitted = [];
830
+ for (const candidate of data) {
831
+ const entry = isRecord(candidate) ? candidate : {};
832
+ const id = typeof entry.id === "string" ? entry.id : "";
833
+ if (id.length === 0) {
834
+ omitted.push({
835
+ id,
836
+ reason: "model-id-missing"
837
+ });
838
+ continue;
839
+ }
840
+ const type = entry.capabilities?.type;
841
+ if (type === "chat") {
842
+ generation.push(generationModel(id, entry));
843
+ continue;
844
+ }
845
+ if (type === "embeddings") {
846
+ embedding.push(embeddingModel(id, entry));
847
+ continue;
848
+ }
849
+ omitted.push({
850
+ id,
851
+ reason: "capability-type-unrecognized"
852
+ });
853
+ }
854
+ return Object.freeze({
855
+ generation: Object.freeze(generation),
856
+ embedding: Object.freeze(embedding),
857
+ omitted: Object.freeze(omitted)
858
+ });
859
+ }
860
+ /** The snapshot returned when there is nothing to report, frozen and shared. */
861
+ const EMPTY_SNAPSHOT = Object.freeze({
862
+ generation: Object.freeze([]),
863
+ embedding: Object.freeze([]),
864
+ omitted: Object.freeze([])
865
+ });
866
+ /** Modalities claimed when — and only when — a vision signal was actually present. */
867
+ const TEXT_AND_IMAGE = Object.freeze(["text", "image"]);
868
+ /**
869
+ * Translate one `type: 'chat'` entry, filling only what the endpoint supplied.
870
+ *
871
+ * `name` is not defaulted to `id`: a display label the endpoint did not send is a
872
+ * label this layer would be inventing, and the layer that renders a selector
873
+ * already falls back to the id.
874
+ */
875
+ function generationModel(id, entry) {
876
+ const limits = entry.capabilities?.limits;
877
+ const supports = entry.capabilities?.supports;
878
+ const vision = entry.vision === true || supports?.vision === true;
879
+ const contextWindow = positiveInteger(limits?.max_context_window_tokens);
880
+ const maxTokens = positiveInteger(limits?.max_output_tokens);
881
+ return Object.freeze({
882
+ model: Object.freeze({
883
+ id,
884
+ ...typeof entry.name === "string" && entry.name.length > 0 ? { name: entry.name } : {},
885
+ ...contextWindow === void 0 ? {} : { contextWindow },
886
+ ...maxTokens === void 0 ? {} : { maxTokens },
887
+ ...vision ? { inputModalities: TEXT_AND_IMAGE } : {}
888
+ }),
889
+ declaredEndpoint: declaredEndpointOf$1(supports)
890
+ });
891
+ }
892
+ /** Translate one `type: 'embeddings'` entry, under the same fill-only-what-was-said rule. */
893
+ function embeddingModel(id, entry) {
894
+ const capabilities = entry.capabilities;
895
+ const limits = capabilities?.limits;
896
+ const maxInputTokens = positiveInteger(limits?.max_context_window_tokens);
897
+ const maxInputs = positiveInteger(limits?.max_inputs);
898
+ const dimensions = capabilities?.supports?.dimensions;
899
+ return Object.freeze({
900
+ id,
901
+ ...typeof entry.name === "string" && entry.name.length > 0 ? { name: entry.name } : {},
902
+ ...typeof capabilities?.family === "string" && capabilities.family.length > 0 ? { family: capabilities.family } : {},
903
+ ...maxInputTokens === void 0 ? {} : { maxInputTokens },
904
+ ...maxInputs === void 0 ? {} : { maxInputs },
905
+ ...typeof dimensions === "boolean" ? { supportsDimensions: dimensions } : {}
906
+ });
907
+ }
908
+ /**
909
+ * Read the endpoint disclosure, and only a disclosure.
910
+ *
911
+ * `supports.responses === true` says `/responses`; `false` says `/chat/completions`
912
+ * — the endpoint stated something either way. Anything else, including the field
913
+ * being absent or holding a non-boolean, is UNKNOWN and stays `undefined`, which
914
+ * is a different state from "not supported" (Requirement 8.6).
915
+ */
916
+ function declaredEndpointOf$1(supports) {
917
+ const responses = supports?.responses;
918
+ if (responses === true) return "responses";
919
+ if (responses === false) return "chat-completions";
920
+ }
921
+ /**
922
+ * Parse the catalog body, treating an unreadable body as structural.
923
+ *
924
+ * Both failures land on the same code because they are the same problem: the
925
+ * response is not a catalog, and there is nothing here to guess a model list from
926
+ * (Requirement 8.8).
927
+ */
928
+ function parseCatalogBody(text) {
929
+ let parsed;
930
+ try {
931
+ parsed = JSON.parse(text);
932
+ } catch (error) {
933
+ throw malformed$1("Copilot model catalog is not valid JSON", error);
934
+ }
935
+ if (!isRecord(parsed)) throw malformed$1("Copilot model catalog must be a JSON object");
936
+ return parsed;
937
+ }
938
+ /** Accept a numeric metadata field only when it can serve as a capacity. */
939
+ function positiveInteger(value) {
940
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
941
+ }
942
+ /** A JSON object, excluding arrays — `data` being at the root is not a catalog. */
943
+ function isRecord(value) {
944
+ return typeof value === "object" && value !== null && !Array.isArray(value);
945
+ }
946
+ function malformed$1(message, cause) {
947
+ return new AgentSdkError(message, COPILOT_ERROR_CODES.CATALOG_MALFORMED, cause === void 0 ? void 0 : { cause });
948
+ }
949
+
950
+ //#endregion
951
+ //#region src/common/store-capture.ts
952
+ /**
953
+ * Store capture: decide which credential store variant the caller passed, and
954
+ * take a snapshot of its identity and methods.
955
+ *
956
+ * The counterpart of `captureCodexStore`, and it holds the same two lines:
957
+ *
958
+ * - **No accessors.** Every property is read through
959
+ * `Object.getOwnPropertyDescriptor`, and a descriptor without a `value` is
960
+ * REJECTED rather than invoked. Telling the two variants apart must not run a
961
+ * line of the caller's code, because a getter here would run during provider
962
+ * construction, in an order the caller cannot see.
963
+ * - **No I/O.** Methods are captured, not called. Nothing touches storage at
964
+ * construction time; the first read happens when an operation asks for a
965
+ * credential.
966
+ *
967
+ * Methods are invoked through `Reflect.apply` with the original object as the
968
+ * receiver, so a store written against `this` keeps working after capture.
969
+ *
970
+ * @module ai-agent-sdk/providers/copilot/store-capture
971
+ */
972
+ /** Capture store identity and methods without invoking accessors or doing storage I/O. */
973
+ function captureCopilotStore(value) {
974
+ try {
975
+ if (value === null || typeof value !== "object") throw new TypeError("store must be an object");
976
+ const marker = dataValue(value, "kind", false);
977
+ if (marker === void 0) return captureLegacy(value);
978
+ if (marker !== "credential-store" || dataValue(value, "apiVersion") !== CREDENTIAL_CAPABILITY_API_VERSION) throw new TypeError("unsupported credential-store marker");
979
+ const id = boundedString(dataValue(value, "id"), 128, "credential store id");
980
+ const label = boundedString(dataValue(value, "label"), 256, "credential store label");
981
+ const read = capturedMethod(value, "read");
982
+ const commit = capturedMethod(value, "commit");
983
+ return Object.freeze({
984
+ kind: "versioned",
985
+ label,
986
+ store: Object.freeze({
987
+ kind: "credential-store",
988
+ apiVersion: CREDENTIAL_CAPABILITY_API_VERSION,
989
+ id,
990
+ label,
991
+ read,
992
+ commit
993
+ })
994
+ });
995
+ } catch (error) {
996
+ throw new AgentSdkError$1("Copilot authStore credential store is invalid", "CREDENTIAL_STORE_INVALID", { cause: error });
997
+ }
998
+ }
999
+ function captureLegacy(source) {
1000
+ const location = boundedString(dataValue(source, "location"), 1024, "Copilot auth store location");
1001
+ const read = capturedMethod(source, "read");
1002
+ const write = capturedMethod(source, "write");
1003
+ return Object.freeze({
1004
+ kind: "legacy",
1005
+ label: location,
1006
+ store: Object.freeze({
1007
+ location,
1008
+ read,
1009
+ write
1010
+ })
1011
+ });
1012
+ }
1013
+ function capturedMethod(source, key) {
1014
+ const method = dataValue(source, key);
1015
+ if (typeof method !== "function") throw new TypeError(`${String(key)} must be a function`);
1016
+ return (...args) => Reflect.apply(method, source, args);
1017
+ }
1018
+ /**
1019
+ * Read an own-or-inherited DATA property. An accessor anywhere on the prototype
1020
+ * chain is an error: reading it would run caller code during construction.
1021
+ */
1022
+ function dataValue(source, key, required = true) {
1023
+ let owner = source;
1024
+ while (owner !== null) {
1025
+ const descriptor = Object.getOwnPropertyDescriptor(owner, key);
1026
+ if (descriptor !== void 0) {
1027
+ if (!("value" in descriptor)) throw new TypeError(`${String(key)} must not be an accessor`);
1028
+ return descriptor.value;
1029
+ }
1030
+ owner = Object.getPrototypeOf(owner);
1031
+ }
1032
+ if (!required) return void 0;
1033
+ throw new TypeError(`missing ${String(key)}`);
1034
+ }
1035
+ function boundedString(value, maxLength, label) {
1036
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) throw new TypeError(`${label} must be a bounded non-empty string`);
1037
+ return value;
1038
+ }
1039
+
1040
+ //#endregion
1041
+ //#region src/dual-protocol.ts
1042
+ /** Protocol id the HTTP layer reports for every Copilot request. */
1043
+ const COPILOT_DUAL_PROTOCOL_ID = "copilot-dual";
1044
+ /**
1045
+ * Conservative defaults, matching each sub-protocol's own defaults where the two
1046
+ * agree.
1047
+ *
1048
+ * `store: false` because retaining prompts on someone else's server is an explicit
1049
+ * decision, `include` carries `reasoning.encrypted_content` because without it a
1050
+ * reasoning model loses its chain of thought across a tool call, and
1051
+ * `parallelToolCalls: false` because older gateways reject the field outright.
1052
+ */
1053
+ const COPILOT_DEFAULT_DIALECT = Object.freeze({
1054
+ sampling: true,
1055
+ maxOutputTokens: true,
1056
+ structuredOutputs: true,
1057
+ tools: true,
1058
+ store: false,
1059
+ include: Object.freeze(["reasoning.encrypted_content"]),
1060
+ reasoningSummary: "auto",
1061
+ streamUsage: true,
1062
+ systemRole: "system",
1063
+ parallelToolCalls: false
1064
+ });
1065
+ /**
1066
+ * Project the Copilot dialect onto the Responses dialect.
1067
+ *
1068
+ * PURE and TOTAL: every {@link CopilotDialect} flag has exactly one destination
1069
+ * here or none at all. `tools`, `streamUsage`, `systemRole` and
1070
+ * `parallelToolCalls` have no Responses destination and are DROPPED rather than
1071
+ * bent into a nearby flag — Responses declares tools from the request itself and
1072
+ * has no `reasoning_effort`-style neighbour worth guessing at.
1073
+ *
1074
+ * `reasoningSummary: 'none'` is expressed by ABSENCE, because that is how the
1075
+ * Responses serializer spells "ask for no summary" (`summary` is only sent when
1076
+ * the knob is defined). {@link resolvedResponsesDialect} therefore drops the
1077
+ * sub-protocol's own `reasoningSummary` default before merging, so this branch of
1078
+ * the projection is not overwritten by it.
1079
+ * @param dialect - the resolved Copilot dialect for this request.
1080
+ * @returns the Responses knobs this dialect determines, and only those.
1081
+ */
1082
+ function toResponsesDialect(dialect) {
1083
+ return {
1084
+ sampling: dialect.sampling,
1085
+ maxOutputTokens: dialect.maxOutputTokens,
1086
+ structuredOutputs: dialect.structuredOutputs,
1087
+ store: dialect.store,
1088
+ include: [...dialect.include],
1089
+ ...dialect.reasoningSummary === "none" ? {} : { reasoningSummary: dialect.reasoningSummary },
1090
+ ...dialect.promptCacheKey === void 0 ? {} : { promptCacheKey: dialect.promptCacheKey }
1091
+ };
1092
+ }
1093
+ /**
1094
+ * Project the Copilot dialect onto the Chat Completions dialect.
1095
+ *
1096
+ * PURE and TOTAL, same rule as {@link toResponsesDialect}: `store`, `include` and
1097
+ * `reasoningSummary` have no Chat Completions destination and are dropped —
1098
+ * `reasoningEffort` is a different knob (how hard to think, not whether to report
1099
+ * a summary), so mapping onto it would be a guess dressed as a translation.
1100
+ *
1101
+ * Two flags change type on the way across, and each mapping is total:
1102
+ *
1103
+ * | Copilot | Chat Completions |
1104
+ * | --- | --- |
1105
+ * | `maxOutputTokens: true` | `maxTokensField: 'max_tokens'` |
1106
+ * | `maxOutputTokens: false` | `maxTokensField: false` |
1107
+ * | `structuredOutputs: true` | `structuredOutputs: 'json-schema'` |
1108
+ * | `structuredOutputs: false` | `structuredOutputs: false` |
1109
+ *
1110
+ * The accepted cost of the first row: a caller cannot reach
1111
+ * `'max_completion_tokens'` through {@link CopilotDialect}. Copilot's
1112
+ * `/chat/completions` takes `max_tokens`, and the models that demand the newer
1113
+ * spelling are the ones the router sends to `/responses` anyway, so the boolean
1114
+ * buys a flag a caller can reason about and costs a spelling no Copilot model has
1115
+ * been observed to need.
1116
+ * @param dialect - the resolved Copilot dialect for this request.
1117
+ * @returns the Chat Completions knobs this dialect determines, and only those.
1118
+ */
1119
+ function toChatCompletionsDialect(dialect) {
1120
+ return {
1121
+ sampling: dialect.sampling,
1122
+ maxTokensField: dialect.maxOutputTokens ? "max_tokens" : false,
1123
+ structuredOutputs: dialect.structuredOutputs ? "json-schema" : false,
1124
+ tools: dialect.tools,
1125
+ streamUsage: dialect.streamUsage,
1126
+ systemRole: dialect.systemRole,
1127
+ parallelToolCalls: dialect.parallelToolCalls,
1128
+ ...dialect.promptCacheKey === void 0 ? {} : { promptCacheKey: dialect.promptCacheKey }
1129
+ };
1130
+ }
1131
+ /**
1132
+ * Build the composite protocol.
1133
+ *
1134
+ * @param options - the router, the two sub-protocols, and the optional observer.
1135
+ * @returns a `RuntimeWireProtocol<CopilotDialect>` with id `'copilot-dual'`.
1136
+ */
1137
+ function copilotDualProtocol(options) {
1138
+ const { router, responses, chat } = options;
1139
+ const onDecision = options.onDecision;
1140
+ /**
1141
+ * The Responses dialect for one request.
1142
+ *
1143
+ * Merged with the SUB-PROTOCOL's defaults, not the composite's: the composite's
1144
+ * defaults are already inside `dialect` by the time the runtime calls us, and
1145
+ * what is missing is everything the Responses dialect knows about but Copilot
1146
+ * does not expose (`messagePhase`). `reasoningSummary` is dropped from the base
1147
+ * because the projection owns that key outright — see {@link toResponsesDialect}.
1148
+ */
1149
+ const resolvedResponsesDialect = (dialect) => {
1150
+ const { reasoningSummary: _ownedByProjection, ...base } = responses.defaultDialect;
1151
+ return Object.freeze({
1152
+ ...base,
1153
+ ...toResponsesDialect(dialect)
1154
+ });
1155
+ };
1156
+ /**
1157
+ * The Chat Completions dialect for one request.
1158
+ *
1159
+ * Same rule; the sub-protocol supplies `path`, `stop`, `seed` and
1160
+ * `reasoningEffort`, which {@link CopilotDialect} deliberately does not expose.
1161
+ */
1162
+ const resolvedChatDialect = (dialect) => Object.freeze({
1163
+ ...chat.defaultDialect,
1164
+ ...toChatCompletionsDialect(dialect)
1165
+ });
1166
+ /** Route one request, reporting the decision on the way through. */
1167
+ const decide = (request) => {
1168
+ const decision = router.decide(request.model.id);
1169
+ report(decision);
1170
+ return decision;
1171
+ };
1172
+ /** Hand the decision to the observer, swallowing whatever it does with it. */
1173
+ const report = (decision) => {
1174
+ if (onDecision === void 0) return;
1175
+ try {
1176
+ onDecision(decision);
1177
+ } catch {}
1178
+ };
1179
+ return defineWireProtocol({
1180
+ id: COPILOT_DUAL_PROTOCOL_ID,
1181
+ defaultDialect: COPILOT_DEFAULT_DIALECT,
1182
+ endpointPath: (request, dialect) => decide(request).endpoint === "responses" ? responses.endpointPath(request, resolvedResponsesDialect(dialect)) : chat.endpointPath(request, resolvedChatDialect(dialect)),
1183
+ ...protocolHeadersOf(responses, chat),
1184
+ serialize: (request, dialect) => router.decide(request.model.id).endpoint === "responses" ? responses.serialize(request, resolvedResponsesDialect(dialect)) : chat.serialize(request, resolvedChatDialect(dialect)),
1185
+ translate: (events, request, displayName) => router.decide(request.model.id).endpoint === "responses" ? responses.translate(events, request, displayName) : chat.translate(events, request, displayName)
1186
+ });
1187
+ }
1188
+ /**
1189
+ * The composite's `protocolHeaders`, or nothing.
1190
+ *
1191
+ * This is the one method whose signature carries NO `ProtocolRequest`, so the
1192
+ * routing key that makes the other three work is absent here. Two things follow.
1193
+ * The union of both branches' headers is wrong — it would put a header belonging
1194
+ * to the branch NOT taken on the wire. And a guess is wrong for the same reason.
1195
+ * So the composite exposes a header set only when both branches produce the SAME
1196
+ * one, in which case that set is the selected branch's set whichever branch is
1197
+ * selected; when they diverge, it exposes none, and a protocol header that only
1198
+ * one branch needs has to travel through the adapter's request-scoped header path
1199
+ * where the model id is in hand.
1200
+ *
1201
+ * Today neither sub-protocol declares `protocolHeaders` — Copilot's mandatory
1202
+ * headers are endpoint identity (`editor-version`, `editor-plugin-version`), not
1203
+ * protocol facts — so this returns nothing and the branch above is the
1204
+ * forward-looking half of the rule.
1205
+ * @param responses - the `/responses` sub-protocol.
1206
+ * @param chat - the `/chat/completions` sub-protocol.
1207
+ * @returns a one-key spread carrying `protocolHeaders`, or an empty one.
1208
+ */
1209
+ function protocolHeadersOf(responses, chat) {
1210
+ const fromResponses = responses.protocolHeaders;
1211
+ const fromChat = chat.protocolHeaders;
1212
+ if (fromResponses === void 0 && fromChat === void 0) return {};
1213
+ return { protocolHeaders: (dialect) => {
1214
+ const left = fromResponses?.(Object.freeze({
1215
+ ...responses.defaultDialect,
1216
+ ...toResponsesDialect(dialect)
1217
+ })) ?? {};
1218
+ return sameHeaders(left, fromChat?.(Object.freeze({
1219
+ ...chat.defaultDialect,
1220
+ ...toChatCompletionsDialect(dialect)
1221
+ })) ?? {}) ? Object.freeze({ ...left }) : Object.freeze({});
1222
+ } };
1223
+ }
1224
+ /** Whether two header maps are equal name-for-name and value-for-value. */
1225
+ function sameHeaders(left, right) {
1226
+ const names = Object.keys(left);
1227
+ if (names.length !== Object.keys(right).length) return false;
1228
+ return names.every((name) => left[name] === right[name]);
1229
+ }
1230
+
1231
+ //#endregion
1232
+ //#region src/exchange.ts
1233
+ /**
1234
+ * `Copilot_Token_Exchange`: turn the long-lived `GitHub_User_Token` into the
1235
+ * short-lived `Copilot_Api_Token` the Copilot surface accepts.
1236
+ *
1237
+ * ```text
1238
+ * GET https://api.github.com/copilot_internal/v2/token
1239
+ * Authorization: Bearer ghu_…
1240
+ * Accept: application/json
1241
+ * Editor-Version / Editor-Plugin-Version
1242
+ * → 200 { token, expires_at, refresh_in?, endpoints?: { api?: string }, … }
1243
+ * ```
1244
+ *
1245
+ * ## The classification order is the contract
1246
+ *
1247
+ * Nine rows, in this order, each for a concrete reason:
1248
+ *
1249
+ * ```text
1250
+ * 1. host is 'ghe.com' or ends with a '.ghe.com' label ⇒ TENANT_UNSUPPORTED (before any I/O)
1251
+ * 2. origin is not the pinned githubApiBaseUrl origin ⇒ ENDPOINT_ORIGIN_INVALID (before any I/O)
1252
+ * 3. the response is a redirect, in any of its shapes ⇒ REDIRECT_REJECTED
1253
+ * 4. HTTP 404 ⇒ TENANT_UNSUPPORTED
1254
+ * 5. HTTP 401 ⇒ CREDENTIAL_REJECTED (permanent)
1255
+ * 6. HTTP 403 ⇒ CREDENTIAL_REJECTED (permanent)
1256
+ * 7. HTTP 429, HTTP 5xx, a network error, or a timeout ⇒ TOKEN_EXCHANGE_FAILED (transient)
1257
+ * 8. any remaining 4xx ⇒ TOKEN_EXCHANGE_FAILED (permanent)
1258
+ * 9. body is not JSON, or expires_at is unreadable ⇒ TOKEN_MALFORMED
1259
+ * ```
1260
+ *
1261
+ * Rows 1 and 2 run BEFORE a request is dispatched. A data-residency tenant has no
1262
+ * token-exchange surface at all, so asking it is pointless; and an origin check
1263
+ * performed after the fact would already have handed the bearer token to whatever
1264
+ * origin the URL named (Requirements 3.6, 3.7).
1265
+ *
1266
+ * Row 1 detects the tenant by DOMAIN LABEL SUFFIX, never by substring: with
1267
+ * `includes('ghe.com')`, `ghe.com.evil.tld` and `notghe.com` would both be
1268
+ * misread as data-residency tenants, one of which is an attacker-chosen host.
1269
+ *
1270
+ * Row 6 does the most work of the nine. The endpoint answers 403 both for a
1271
+ * personal access token and for a token minted by an OAuth App that is not on
1272
+ * GitHub's allowlist, and the response does not distinguish the two — so the
1273
+ * message names BOTH possibilities alongside the single instruction that helps in
1274
+ * either case (Requirements 3.5, 13.2).
1275
+ *
1276
+ * ## What is read from the body, and what is refused
1277
+ *
1278
+ * `expires_at` is MANDATORY and has to be a positive finite number: without it
1279
+ * there is no second source for the lifetime, and an invented TTL is exactly the
1280
+ * inference this SDK does not make. `refresh_in` is advisory and a bad value is
1281
+ * dropped rather than fatal — it can only shorten the refresh moment, so losing
1282
+ * it costs nothing. `endpoints.api` is read and exposed for diagnostics but is
1283
+ * NEVER used as the base URL: a server-designated base URL is a redirect under
1284
+ * another name, and this SDK does not follow provider-controlled redirects
1285
+ * (DD-6, Requirements 3.8, 7.8).
1286
+ *
1287
+ * @module ai-agent-sdk/providers/copilot/exchange
1288
+ */
1289
+ /** GitHub's API base, where the token-exchange surface lives. */
1290
+ const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
1291
+ /** Path of the token-exchange surface. */
1292
+ const COPILOT_TOKEN_EXCHANGE_PATH = "/copilot_internal/v2/token";
1293
+ /** Marker used in place of a credential value that appeared in a response body. */
1294
+ const REDACTED$1 = "[REDACTED]";
1295
+ /**
1296
+ * Exchange a `GitHub_User_Token` for a `Copilot_Api_Token`.
1297
+ *
1298
+ * The long-lived credential is NOT consumed: nothing here writes to a store, and
1299
+ * the persisted value is left exactly as it was (Requirement 3.4).
1300
+ * @param github - the long-lived GitHub user token. Its value never reaches an
1301
+ * error message, and any occurrence of it in a response body is redacted before
1302
+ * the body is retained as a cause (Requirement 13.7).
1303
+ * @param options - base URL override, injected fetch, signal, and the read bounds.
1304
+ * @returns the short-lived token plus its expiry and the advisory fields.
1305
+ * @throws AgentSdkError with `COPILOT_ENDPOINT_ORIGIN_INVALID` or
1306
+ * `COPILOT_REDIRECT_REJECTED`, or {@link CopilotTokenExchangeError} with
1307
+ * `COPILOT_TENANT_UNSUPPORTED`, `COPILOT_CREDENTIAL_REJECTED`,
1308
+ * `COPILOT_TOKEN_EXCHANGE_FAILED` or `COPILOT_TOKEN_MALFORMED`, per the
1309
+ * classification order in the module note.
1310
+ */
1311
+ async function exchangeCopilotToken(github, options = {}) {
1312
+ rejectDataResidencyTenant(options.githubApiBaseUrl);
1313
+ const pinned = issuerOf("githubApiBaseUrl", options.githubApiBaseUrl, DEFAULT_GITHUB_API_BASE_URL, options);
1314
+ const url = copilotUrl(pinned, COPILOT_TOKEN_EXCHANGE_PATH);
1315
+ const host = new URL(pinned.origin).hostname;
1316
+ let response;
1317
+ try {
1318
+ response = await copilotFetch({
1319
+ pinned,
1320
+ url,
1321
+ operation: "token exchange",
1322
+ init: {
1323
+ method: "GET",
1324
+ headers: exchangeHeaders(github, options.editorHeaders)
1325
+ }
1326
+ }, options);
1327
+ } catch (error) {
1328
+ throw transportFailure(error, host, options);
1329
+ }
1330
+ const secrets = [github.token, ...options.additionalSecrets ?? []];
1331
+ if (!response.ok) throw await statusFailure(response, host, secrets, options);
1332
+ return readApiToken(await readCopilotResponseText(response, options), secrets);
1333
+ }
1334
+ /**
1335
+ * Row 1: refuse a `*.ghe.com` tenant by domain label, before anything is sent.
1336
+ *
1337
+ * An unparsable value is left alone rather than reported here — {@link issuerOf}
1338
+ * owns that message, and reporting it as a tenant problem would name the wrong
1339
+ * cause.
1340
+ * @param configured - the caller's `githubApiBaseUrl`, when they set one.
1341
+ * @throws CopilotTokenExchangeError with `COPILOT_TENANT_UNSUPPORTED`, naming the
1342
+ * detected domain (Requirement 13.3).
1343
+ */
1344
+ function rejectDataResidencyTenant(configured) {
1345
+ if (configured === void 0) return;
1346
+ let host;
1347
+ try {
1348
+ host = new URL(configured).hostname;
1349
+ } catch {
1350
+ return;
1351
+ }
1352
+ if (!isDataResidencyHost(host)) return;
1353
+ throw new CopilotTokenExchangeError(credentialFailure(tenantMessage(host)), COPILOT_ERROR_CODES.TENANT_UNSUPPORTED, "permanent");
1354
+ }
1355
+ /**
1356
+ * Whether a hostname belongs to the `ghe.com` data-residency namespace.
1357
+ *
1358
+ * Matched on DOMAIN LABELS, which is the whole point: `ghe.com.evil.tld` and
1359
+ * `notghe.com` are not data-residency hosts, and a substring test would call both
1360
+ * of them one.
1361
+ * @param host - a hostname, without a port.
1362
+ * @returns true for `ghe.com` itself and for any host under it.
1363
+ */
1364
+ function isDataResidencyHost(host) {
1365
+ const normalized = host.toLowerCase().replace(/\.$/, "");
1366
+ return normalized === "ghe.com" || normalized.endsWith(".ghe.com");
1367
+ }
1368
+ /**
1369
+ * Headers for the exchange request.
1370
+ *
1371
+ * Both editor headers are mandatory: with either one missing the endpoint answers
1372
+ * HTTP 400 and the request never runs. An override of one leaves the other at its
1373
+ * exported default rather than dropping it.
1374
+ * @param github - the credential whose value goes in `Authorization`.
1375
+ * @param headers - per-call overrides for the editor identity.
1376
+ * @returns the header map for the request init.
1377
+ */
1378
+ function exchangeHeaders(github, headers) {
1379
+ return {
1380
+ authorization: `Bearer ${github.token}`,
1381
+ accept: "application/json",
1382
+ "editor-version": headers?.editorVersion ?? "vscode/1.99.0",
1383
+ "editor-plugin-version": headers?.editorPluginVersion ?? "copilot-chat/0.26.0"
1384
+ };
1385
+ }
1386
+ /**
1387
+ * Rows 3 and 7 on the dispatch path: keep the structural refusals, classify the
1388
+ * rest as transient.
1389
+ *
1390
+ * Four kinds of failure pass through UNCHANGED, because wrapping each one would
1391
+ * replace a precise diagnosis with a vaguer one:
1392
+ *
1393
+ * - the origin refusal and the redirect refusal, which are rows 2 and 3 and
1394
+ * already carry their own codes;
1395
+ * - a caller abort, which keeps the SDK's abort code rather than becoming a
1396
+ * Copilot failure the caller did not ask about (Requirement 4.6);
1397
+ * - a `RangeError` from a bound, which names the limit that was exceeded and is
1398
+ * neither a network fault nor a server fault (Requirement 13.6).
1399
+ *
1400
+ * Everything else — DNS, connection reset, TLS, and the per-request deadline —
1401
+ * is transient: it is exactly the class of failure that a later attempt can win.
1402
+ * @param error - the caught value.
1403
+ * @param host - the host that was contacted, for the message.
1404
+ * @param options - read for the caller's signal only.
1405
+ * @returns the value to throw.
1406
+ */
1407
+ function transportFailure(error, host, options) {
1408
+ if (options.signal?.aborted === true) return error;
1409
+ if (error instanceof RangeError) return error;
1410
+ if (error instanceof AgentSdkError && (error.code === COPILOT_ERROR_CODES.ENDPOINT_ORIGIN_INVALID || error.code === COPILOT_ERROR_CODES.REDIRECT_REJECTED)) return error;
1411
+ return new CopilotTokenExchangeError(credentialFailure(`Copilot token exchange could not reach ${host}; the request failed before a response`, error), COPILOT_ERROR_CODES.TOKEN_EXCHANGE_FAILED, "transient");
1412
+ }
1413
+ /**
1414
+ * Rows 4 through 8: classify a response that arrived but was not a success.
1415
+ *
1416
+ * The body is read through the bounded reader first, so the cause carries the
1417
+ * endpoint's own words — with every occurrence of the credential replaced —
1418
+ * rather than nothing at all (Requirements 13.6, 13.7). A read that fails is not
1419
+ * allowed to hide the status: the classification stands either way.
1420
+ * @param response - a non-ok response, already cleared by the redirect guard.
1421
+ * @param host - the host that answered, for the tenant message.
1422
+ * @param secrets - every live credential value to redact out of the body.
1423
+ * @param options - the read bounds and the signal.
1424
+ * @returns the classified error to throw.
1425
+ */
1426
+ async function statusFailure(response, host, secrets, options) {
1427
+ const body = await readFailureBody(response, secrets, options);
1428
+ const cause = body === void 0 ? void 0 : new Error(body);
1429
+ if (response.status === 404) return new CopilotTokenExchangeError(credentialFailure(tenantMessage(host), cause), COPILOT_ERROR_CODES.TENANT_UNSUPPORTED, "permanent");
1430
+ if (response.status === 401) return new CopilotTokenExchangeError(credentialFailure(`the Copilot token-exchange surface rejected the stored GitHub credential (HTTP 401); run \`${COPILOT_LOGIN_COMMAND}\` to sign in again`, cause), COPILOT_ERROR_CODES.CREDENTIAL_REJECTED, "permanent");
1431
+ if (response.status === 403) return new CopilotTokenExchangeError(credentialFailure(`the Copilot token-exchange surface refused this credential type (HTTP 403). It accepts only a token minted by an OAuth App on GitHub's allowlist: a personal access token cannot be used here, and neither can a token from an OAuth App that is not allowlisted. Run \`${COPILOT_LOGIN_COMMAND}\` to sign in with the supported client.`, cause), COPILOT_ERROR_CODES.CREDENTIAL_REJECTED, "permanent");
1432
+ const transient = response.status >= 500 || response.status === 429;
1433
+ return new CopilotTokenExchangeError(credentialFailure(`Copilot token exchange failed (HTTP ${response.status})`, cause), COPILOT_ERROR_CODES.TOKEN_EXCHANGE_FAILED, transient ? "transient" : "permanent");
1434
+ }
1435
+ /**
1436
+ * Read an error body within the configured bounds, redacted, or give up quietly.
1437
+ *
1438
+ * Giving up quietly is deliberate: the status has already decided the
1439
+ * classification, and a body that could not be read must not turn a precise 403
1440
+ * into a read error.
1441
+ * @param response - the non-ok response.
1442
+ * @param secrets - every live credential value to redact.
1443
+ * @param options - the read bounds and the signal.
1444
+ * @returns the redacted text, or `undefined` when it could not be read.
1445
+ */
1446
+ async function readFailureBody(response, secrets, options) {
1447
+ try {
1448
+ const text = await readCopilotResponseText(response, options);
1449
+ return text.length === 0 ? void 0 : redact(text, secrets);
1450
+ } catch {
1451
+ return;
1452
+ }
1453
+ }
1454
+ /**
1455
+ * Row 9: read the success body, requiring exactly what cannot be inferred.
1456
+ *
1457
+ * `token` and `expires_at` are mandatory; everything else is advisory and a
1458
+ * useless value is dropped rather than raised, because a dropped hint changes
1459
+ * nothing about correctness while a raised one would fail an exchange that
1460
+ * actually produced a usable token.
1461
+ * @param raw - the bounded response text.
1462
+ * @param secrets - every live credential value to redact out of the cause.
1463
+ * @returns the token this exchange produced.
1464
+ * @throws CopilotTokenExchangeError with `COPILOT_TOKEN_MALFORMED` when the body
1465
+ * is not a JSON object, `token` is not a non-empty string, or `expires_at` is
1466
+ * not a positive finite number.
1467
+ */
1468
+ function readApiToken(raw, secrets) {
1469
+ let parsed;
1470
+ try {
1471
+ parsed = JSON.parse(raw);
1472
+ } catch (error) {
1473
+ throw malformed("the Copilot token-exchange response was not JSON", error);
1474
+ }
1475
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw malformed("the Copilot token-exchange response was not a JSON object");
1476
+ const body = parsed;
1477
+ const token = body["token"];
1478
+ if (typeof token !== "string" || token.length === 0) throw malformed("the Copilot token-exchange response carried no token");
1479
+ const expiresAt = body["expires_at"];
1480
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= 0) throw malformed("the Copilot token-exchange response carried no readable expires_at; this SDK does not invent a token lifetime", new Error(redact(raw, secrets)));
1481
+ const refreshIn = body["refresh_in"];
1482
+ const declared = declaredEndpointOf(body["endpoints"]);
1483
+ return Object.freeze({
1484
+ token,
1485
+ expiresAtMs: expiresAt * 1e3,
1486
+ ...typeof refreshIn === "number" && Number.isFinite(refreshIn) && refreshIn > 0 ? { refreshInSeconds: refreshIn } : {},
1487
+ ...declared === void 0 ? {} : { declaredApiEndpoint: declared }
1488
+ });
1489
+ }
1490
+ /**
1491
+ * Read `endpoints.api` for diagnostics only.
1492
+ *
1493
+ * Never returned as something to request against — see {@link
1494
+ * CopilotApiToken.declaredApiEndpoint} and DD-6.
1495
+ * @param endpoints - the `endpoints` member of the response, unvalidated.
1496
+ * @returns the declared API endpoint, when it is a non-empty string.
1497
+ */
1498
+ function declaredEndpointOf(endpoints) {
1499
+ if (typeof endpoints !== "object" || endpoints === null) return void 0;
1500
+ const api = endpoints["api"];
1501
+ return typeof api === "string" && api.length > 0 ? api : void 0;
1502
+ }
1503
+ /**
1504
+ * The one tenant message, so the two paths that reach row 1 and row 4 say the
1505
+ * same thing.
1506
+ * @param host - the detected host, named in the message (Requirement 13.3).
1507
+ * @returns the message text.
1508
+ */
1509
+ function tenantMessage(host) {
1510
+ return `'${host}' is a GitHub data-residency tenant, which does not provide the Copilot token-exchange surface; use a github.com account for this provider`;
1511
+ }
1512
+ /**
1513
+ * Replace every occurrence of every live credential value in text with
1514
+ * {@link REDACTED}.
1515
+ *
1516
+ * A response body is endpoint-authored text, and an endpoint that echoes the
1517
+ * `Authorization` header back is exactly how a token ends up in a log. The list
1518
+ * is plural because Requirement 13.7 covers BOTH tokens: this request carries one
1519
+ * of them, and the other arrives through
1520
+ * {@link CopilotExchangeOptions.additionalSecrets}.
1521
+ * @param text - the body text.
1522
+ * @param secrets - the credential values; empty entries are ignored.
1523
+ * @returns the text with every value removed.
1524
+ */
1525
+ function redact(text, secrets) {
1526
+ let result = text;
1527
+ for (const secret of secrets) {
1528
+ if (secret.length === 0) continue;
1529
+ result = result.split(secret).join(REDACTED$1);
1530
+ }
1531
+ return result;
1532
+ }
1533
+ /**
1534
+ * Build the row-9 error.
1535
+ * @param message - SDK-authored text.
1536
+ * @param cause - the caught value or the redacted body, when there is one.
1537
+ * @returns the malformed-token error.
1538
+ */
1539
+ function malformed(message, cause) {
1540
+ return new CopilotTokenExchangeError(credentialFailure(message, cause), COPILOT_ERROR_CODES.TOKEN_MALFORMED, "permanent");
1541
+ }
1542
+ /** Provider name recorded on the credential-operation observation by default. */
1543
+ const COPILOT_PROVIDER_ID = "copilot";
1544
+ /**
1545
+ * Build a token cache over one set of exchange settings.
1546
+ *
1547
+ * ## The mistake this is written to avoid
1548
+ *
1549
+ * The shared exchange gets its OWN `AbortController` plus its own deadline, and
1550
+ * NEVER any single caller's signal. Were the caller's signal handed to it, the
1551
+ * first caller to abort would cancel the exchange every other caller is waiting
1552
+ * on, and those callers would fail for a reason that has nothing to do with them.
1553
+ * Instead each caller — the one that started the exchange included — races the
1554
+ * shared promise against its OWN signal: an aborted caller leaves, and the
1555
+ * exchange still completes for everyone else (Property 18).
1556
+ *
1557
+ * The observation therefore counts exchanges actually DISPATCHED rather than
1558
+ * callers served, which is what makes the coalescing observable instead of merely
1559
+ * claimed (Property 52). It is recorded with the `'refresh'` operation name: that
1560
+ * parameter's union is closed at `'resolve' | 'refresh' | 'login'`, widening it
1561
+ * would change a public type of `provider-http`, and Requirement 18.4 forbids
1562
+ * that — see DD-7.
1563
+ *
1564
+ * ## Two paths deliberately absent
1565
+ *
1566
+ * There is no revision-conflict recovery, unlike `provider-codex`. That path
1567
+ * exists there because a Codex refresh token rotates and is single-use, so a lost
1568
+ * race destroys a credential. A `GitHub_User_Token` does not rotate and an
1569
+ * exchange does not consume it, so two racing processes simply exchange twice —
1570
+ * and a branch no situation reaches is a branch nothing verifies (DD-8).
1571
+ *
1572
+ * Nothing here writes to a store. The `Copilot_Api_Token` is never persisted: it
1573
+ * lives ~25 minutes, so persisting it would add a second secret on disk, a second
1574
+ * write path, and a new state to reason about, to save one request inside a
1575
+ * 25-minute window (DD-9, Requirement 3.3).
1576
+ *
1577
+ * A failure is returned to every waiting caller as-is and never retried here — an
1578
+ * endpoint that rejected the credential will reject it again, and this layer has
1579
+ * no way to change that (Requirement 5.8, Property 19).
1580
+ * @param options - exchange settings, the observation provider name, the margin
1581
+ * and the clock. `options.signal` is deliberately IGNORED for the exchange
1582
+ * itself; per-caller cancellation travels through `operation.signal`.
1583
+ * @returns a cache over a single credential slot.
1584
+ */
1585
+ function createCopilotTokenCache(options = {}) {
1586
+ const provider = options.providerId ?? "copilot";
1587
+ const now = options.now ?? (() => Date.now());
1588
+ const marginMs = options.marginMs === void 0 ? COPILOT_TOKEN_EXCHANGE_MARGIN_MS : positiveSafeInteger(options.marginMs, "marginMs");
1589
+ let entry;
1590
+ let inflight;
1591
+ let inflightToken;
1592
+ let ticket = 0;
1593
+ return {
1594
+ async acquire(source, operation, context) {
1595
+ operation.signal.throwIfAborted();
1596
+ const github = requireGitHubToken(source.file, source.label);
1597
+ const cached = entry;
1598
+ if (cached !== void 0 && cached.sourceToken === github.token && cached.sourceRevision === source.revision && !shouldExchange(cached.api, now(), marginMs)) return cached.api;
1599
+ if (inflight !== void 0 && inflightToken === github.token) return await raceAbort(inflight, operation.signal);
1600
+ ticket++;
1601
+ const pending = runExchange(ticket);
1602
+ inflight = pending;
1603
+ inflightToken = github.token;
1604
+ return await raceAbort(pending, operation.signal);
1605
+ /**
1606
+ * Dispatch the one shared exchange and record its result.
1607
+ * @param slot - this exchange's ticket, so a later exchange's teardown does
1608
+ * not clear a newer in-flight one.
1609
+ * @returns the exchanged token.
1610
+ */
1611
+ async function runExchange(slot) {
1612
+ const held = entry?.api.token;
1613
+ try {
1614
+ const api = await observeCredentialOperation(context, provider, "refresh", () => exchangeCopilotToken(github, {
1615
+ ...options,
1616
+ ...held === void 0 ? {} : { additionalSecrets: [...options.additionalSecrets ?? [], held] },
1617
+ signal: sharedExchangeSignal(options)
1618
+ }));
1619
+ entry = Object.freeze({
1620
+ api,
1621
+ sourceToken: github.token,
1622
+ sourceRevision: source.revision
1623
+ });
1624
+ return api;
1625
+ } finally {
1626
+ if (ticket === slot) {
1627
+ inflight = void 0;
1628
+ inflightToken = void 0;
1629
+ }
1630
+ }
1631
+ }
1632
+ },
1633
+ invalidate() {
1634
+ entry = void 0;
1635
+ }
1636
+ };
1637
+ }
1638
+ /**
1639
+ * The shared exchange's own cancellation source: one controller, driven by one
1640
+ * deadline, and reachable by no caller.
1641
+ *
1642
+ * The deadline is what makes the controller more than ceremony. `copilotFetch`
1643
+ * bounds its own dispatch, but the bounded body read afterwards races only the
1644
+ * signal it was given — so without a deadline on this signal a stalled read would
1645
+ * hold the in-flight slot open indefinitely and every coalesced caller with it.
1646
+ * @param options - read for `requestTimeoutMs`.
1647
+ * @returns a signal that aborts on the exchange deadline and on nothing else.
1648
+ * @throws RangeError when `requestTimeoutMs` cannot serve as a bound.
1649
+ */
1650
+ function sharedExchangeSignal(options) {
1651
+ const controller = new AbortController();
1652
+ const deadline = AbortSignal.timeout(positiveSafeInteger(options.requestTimeoutMs ?? 3e4, "requestTimeoutMs"));
1653
+ deadline.addEventListener("abort", () => {
1654
+ controller.abort(deadline.reason);
1655
+ }, { once: true });
1656
+ return controller.signal;
1657
+ }
1658
+
1659
+ //#endregion
1660
+ //#region src/router.ts
1661
+ /**
1662
+ * `Copilot_Endpoint_Router`: decide, ONCE per model id, which endpoint a
1663
+ * generation request is dispatched to.
1664
+ *
1665
+ * ## The decision order, and what is deliberately missing from it
1666
+ *
1667
+ * ```text
1668
+ * 1. endpointOverrides[modelId] ⇒ source 'override'
1669
+ * 2. catalog disclosure (via learn()) ⇒ source 'catalog'
1670
+ * 3. responses-model prefix allowlist ⇒ source 'allowlist'
1671
+ * 4. /chat/completions ⇒ source 'default'
1672
+ * ```
1673
+ *
1674
+ * What is missing is a PROBE. Trying `/responses` to find out whether a model
1675
+ * accepts it was rejected (DD-4): a probe is a real request that spends real
1676
+ * quota and needs a real prompt, so it has an observable side effect on the
1677
+ * user's account purely to answer a metadata question. Its result is not safely
1678
+ * cacheable across accounts either, since which models an account may call
1679
+ * depends on its plan.
1680
+ *
1681
+ * ## Why the default is `/chat/completions`
1682
+ *
1683
+ * The two ways of guessing wrong are ASYMMETRIC:
1684
+ *
1685
+ * | Guessed wrong | Consequence |
1686
+ * | --- | --- |
1687
+ * | Model supports `/responses`, we sent `/chat/completions` | works, minus some Responses-specific features |
1688
+ * | Model does not support `/responses`, we sent `/responses` | HTTP 400, dead request |
1689
+ *
1690
+ * Losing a feature is recoverable at the next call; losing the call is not. So
1691
+ * the fallback leans to the endpoint every Copilot generation model answers.
1692
+ *
1693
+ * ## Why `decisions` is append-only
1694
+ *
1695
+ * Requirement 9.7 asks that the endpoint chosen for a `Logical_Call` hold for
1696
+ * that whole call, retries included. The catalog has a TTL and may refresh
1697
+ * between two retries, so a router that recomputed could answer `/responses` on
1698
+ * the first attempt and `/chat/completions` on the second — one logical call
1699
+ * split across two wire protocols, with a serialized body that no longer matches
1700
+ * the endpoint it is going to.
1701
+ *
1702
+ * This module makes that STRUCTURALLY impossible rather than conventionally
1703
+ * avoided: once a model id has a decision, no code path rewrites it.
1704
+ * {@link CopilotEndpointRouter.learn} only adds keys that have no decision yet,
1705
+ * so a later catalog refresh returning different metadata changes nothing.
1706
+ *
1707
+ * The accepted cost (DD-5): a model misclassified on the first call keeps that
1708
+ * classification for the lifetime of the adapter instance. `endpointOverrides`
1709
+ * is the instant fix, `--models` is the discovery path, and rebuilding the
1710
+ * runtime is the reset. The trade is an invariant with no exceptions instead of
1711
+ * an invariant that holds "unless the catalog refreshed".
1712
+ *
1713
+ * @module ai-agent-sdk/providers/copilot/router
1714
+ */
1715
+ /**
1716
+ * Model id prefixes dispatched to `/responses` when the catalog says nothing.
1717
+ *
1718
+ * Exported and overridable for the same reason `COPILOT_EDITOR_VERSION` is: this
1719
+ * is a fact about a remote endpoint that WILL go stale, and a user has to be able
1720
+ * to correct it without waiting for a release.
1721
+ * `CopilotProviderOptions.responsesModelPrefixes` ADDS to this list rather than
1722
+ * replacing it, so an override cannot silently drop the prefixes shipped here.
1723
+ *
1724
+ * Kept deliberately short. A prefix that matches too much pushes models toward
1725
+ * the endpoint where guessing wrong costs the request (see the module note), so
1726
+ * an absent prefix is the cheaper error.
1727
+ */
1728
+ const COPILOT_RESPONSES_MODEL_PREFIXES = Object.freeze(["codex-", "gpt-5"]);
1729
+ /** The two endpoints, so an override value can be checked against something. */
1730
+ const COPILOT_ENDPOINTS = Object.freeze(["responses", "chat-completions"]);
1731
+ /** Protocol id per endpoint, the one place the two are tied together. */
1732
+ const PROTOCOL_IDS = Object.freeze({
1733
+ "responses": OPENAI_RESPONSES_PROTOCOL_ID,
1734
+ "chat-completions": OPENAI_CHAT_COMPLETIONS_PROTOCOL_ID
1735
+ });
1736
+ /**
1737
+ * Build a router for one adapter instance.
1738
+ *
1739
+ * Overrides are validated HERE, not at dispatch: a typo in
1740
+ * `endpointOverrides` is a configuration mistake, and a configuration mistake
1741
+ * that surfaces while building the provider is cheaper than one that surfaces on
1742
+ * the first request to one particular model.
1743
+ * @param options - overrides and the merged prefix allowlist.
1744
+ * @returns a router whose `decisions` map only ever grows.
1745
+ * @throws AgentSdkError with `COPILOT_ENDPOINT_OVERRIDE_INVALID` when an
1746
+ * override pins an endpoint that does not exist.
1747
+ */
1748
+ function createCopilotEndpointRouter(options = {}) {
1749
+ const overrides = validateOverrides(options.overrides ?? {});
1750
+ const prefixes = normalizePrefixes(options.prefixes ?? COPILOT_RESPONSES_MODEL_PREFIXES);
1751
+ const decisions = /* @__PURE__ */ new Map();
1752
+ /** Write a decision for a key that has none. The single mutation point. */
1753
+ const record = (modelId, endpoint, source) => {
1754
+ const decision = Object.freeze({
1755
+ model: modelId,
1756
+ endpoint,
1757
+ protocolId: PROTOCOL_IDS[endpoint],
1758
+ source
1759
+ });
1760
+ decisions.set(modelId, decision);
1761
+ return decision;
1762
+ };
1763
+ /**
1764
+ * The decision order for a key with no recorded decision.
1765
+ *
1766
+ * `declared` is `undefined` for {@link CopilotEndpointRouter.decide}, because a
1767
+ * bare dispatch carries no catalog metadata — a disclosure only arrives through
1768
+ * {@link CopilotEndpointRouter.learn}. `undefined` is UNKNOWN, so it falls
1769
+ * through to the allowlist; `'chat-completions'` is a stated fact and stops
1770
+ * there with `source: 'catalog'`.
1771
+ */
1772
+ const resolve = (modelId, declared) => {
1773
+ const override = overrides[modelId];
1774
+ if (override !== void 0) return record(modelId, override, "override");
1775
+ if (declared !== void 0) return record(modelId, declared, "catalog");
1776
+ if (matchesPrefix(modelId, prefixes)) return record(modelId, "responses", "allowlist");
1777
+ return record(modelId, "chat-completions", "default");
1778
+ };
1779
+ return Object.freeze({
1780
+ decide(modelId) {
1781
+ return decisions.get(modelId) ?? resolve(modelId, void 0);
1782
+ },
1783
+ learn(models) {
1784
+ for (const entry of models) {
1785
+ const modelId = entry.model.id;
1786
+ if (decisions.has(modelId)) continue;
1787
+ resolve(modelId, entry.declaredEndpoint);
1788
+ }
1789
+ },
1790
+ snapshot() {
1791
+ return Object.freeze([...decisions.values()]);
1792
+ }
1793
+ });
1794
+ }
1795
+ /**
1796
+ * Copy the overrides and reject any value that is not an endpoint.
1797
+ *
1798
+ * A copy rather than the caller's object, so a later mutation of what was passed
1799
+ * in cannot introduce an unvalidated endpoint after construction.
1800
+ */
1801
+ function validateOverrides(overrides) {
1802
+ const validated = Object.create(null);
1803
+ for (const [modelId, endpoint] of Object.entries(overrides)) {
1804
+ if (!COPILOT_ENDPOINTS.includes(endpoint)) throw new AgentSdkError(`Copilot endpoint override for model '${modelId}' must be one of ${COPILOT_ENDPOINTS.map((value) => `'${value}'`).join(", ")}`, COPILOT_ERROR_CODES.ENDPOINT_OVERRIDE_INVALID);
1805
+ validated[modelId] = endpoint;
1806
+ }
1807
+ return Object.freeze(validated);
1808
+ }
1809
+ /**
1810
+ * Lower-case the prefixes and drop the ones that cannot select anything.
1811
+ *
1812
+ * An empty string is dropped rather than honoured: as a prefix it matches every
1813
+ * model id, which would route the whole catalog to `/responses` — the direction
1814
+ * where guessing wrong costs the request. Dropping it leaves the shipped
1815
+ * prefixes intact, which is what a caller adding to the list asked for.
1816
+ */
1817
+ function normalizePrefixes(prefixes) {
1818
+ const normalized = [];
1819
+ for (const prefix of prefixes) {
1820
+ if (typeof prefix !== "string" || prefix.length === 0) continue;
1821
+ const lower = prefix.toLowerCase();
1822
+ if (!normalized.includes(lower)) normalized.push(lower);
1823
+ }
1824
+ return Object.freeze(normalized);
1825
+ }
1826
+ /** Case-insensitive prefix match; Copilot model ids are lower-case in practice. */
1827
+ function matchesPrefix(modelId, prefixes) {
1828
+ const lower = modelId.toLowerCase();
1829
+ return prefixes.some((prefix) => lower.startsWith(prefix));
1830
+ }
1831
+
1832
+ //#endregion
1833
+ //#region src/adapter.ts
1834
+ /**
1835
+ * The Copilot provider: the Copilot API surface, authenticated with a GitHub user
1836
+ * token this project's own credential store holds.
1837
+ *
1838
+ * ## Configured, not subclassed
1839
+ *
1840
+ * `copilotAdapter` is built with `createRuntimeHttpProvider` and extends nothing
1841
+ * (Requirement 7.1). Everything Copilot needs beyond a plain API-key provider —
1842
+ * a two-tier credential, a token exchange with its own cache, two wire protocols
1843
+ * on one route, endpoint-driven discovery — is expressed as DATA:
1844
+ * `auth: { kind: 'dynamic' }` for the credential path, a composite protocol for
1845
+ * the two endpoints, `discoverModels` for the catalog. That is the point of the
1846
+ * exercise: the configuration path is proven by the provider with the most
1847
+ * demanding requirements in this repository, not by the simplest one.
1848
+ *
1849
+ * ## Client identity
1850
+ *
1851
+ * `COPILOT_EDITOR_VERSION` and `COPILOT_EDITOR_PLUGIN_VERSION` are two of the
1852
+ * three `Client_Identity_Constants` in this package; the third is
1853
+ * `COPILOT_OAUTH_CLIENT_ID` in `./oauth.ts`. Their defaults make this SDK
1854
+ * identify itself AS AN EDITOR CLIENT on every request to the Copilot surface.
1855
+ *
1856
+ * They are EXPORTED, OVERRIDABLE constants — not hidden values — precisely
1857
+ * because of that: presenting as another client is something the caller should be
1858
+ * able to read off the source and change without forking, so each one is a named
1859
+ * option (`editorHeaders`) with a visible default. Same reason
1860
+ * `CODEX_CLIENT_VERSION` is an exported constant in `provider-codex`. All three
1861
+ * values will also go stale, which is a second reason to keep them where a caller
1862
+ * can reach them.
1863
+ *
1864
+ * Use your own account, and prefer a provider's official first-party surface for
1865
+ * production. The README and the "Client identity" section of the docs carry the
1866
+ * full tradeoff.
1867
+ *
1868
+ * @module ai-agent-sdk/providers/copilot/adapter
1869
+ */
1870
+ /** Registry id, provider family, and observation label when the caller sets none. */
1871
+ const COPILOT_ROUTE_ID = "copilot";
1872
+ /** Display name reported by the adapter and the plugin. */
1873
+ const COPILOT_DISPLAY_NAME = "GitHub Copilot";
1874
+ /** Never-aborting logger sink for a resolve that arrives without a context. */
1875
+ const NULL_LOGGER$1 = Object.freeze({
1876
+ child: () => NULL_LOGGER$1,
1877
+ trace: () => void 0,
1878
+ debug: () => void 0,
1879
+ info: () => void 0,
1880
+ warn: () => void 0,
1881
+ error: () => void 0,
1882
+ fatal: () => void 0
1883
+ });
1884
+ /** Marker written in place of a token value that appeared in a response body. */
1885
+ const REDACTED = "[REDACTED]";
1886
+ /**
1887
+ * The two transport-owned headers, sent on every request (Requirement 2.3).
1888
+ *
1889
+ * `content-type` cannot come from the auth layer — `provider-http` owns the name
1890
+ * at the transport layer and refuses a second owner — so it is declared here,
1891
+ * where it is allowed and where it is visible.
1892
+ */
1893
+ const COPILOT_TRANSPORT_HEADERS = Object.freeze({
1894
+ "content-type": "application/json",
1895
+ accept: "text/event-stream"
1896
+ });
1897
+ /** Bytes read from a non-success response when the caller configures no bound. */
1898
+ const DEFAULT_MAX_ERROR_BODY_BYTES = 1048576;
1899
+ function copilotAdapter(options) {
1900
+ return buildCopilotAdapter(options, captureCopilotStore(options?.authStore));
1901
+ }
1902
+ /**
1903
+ * The one adapter body, shared by both store variants and by the plugin.
1904
+ *
1905
+ * Four things are worth reading closely.
1906
+ *
1907
+ * **`auth.resolve` is the only place a token enters a request.** It reads the
1908
+ * store, demands a long-lived token, then asks the cache — which decides on its
1909
+ * own whether an exchange is due. `provider-http` calls `resolve` ONCE PER
1910
+ * OPERATION (Requirement 7.2), not once per retry, so the number of store reads
1911
+ * equals the number of operations and every attempt of one operation carries the
1912
+ * credential and the endpoint from a single snapshot.
1913
+ *
1914
+ * **`requireGitHubToken` runs BEFORE `cache.acquire`.** With no credential the
1915
+ * failure is `MISSING_CREDENTIAL` naming the login command, rather than an HTTP
1916
+ * error from an exchange that never had anything to exchange (Requirement 13.4).
1917
+ *
1918
+ * **`x-request-id` is the CLIENT's id, not the server's.** It exists to line up
1919
+ * two logs and carries nothing about the user.
1920
+ *
1921
+ * **`router.learn` only ADDS.** A catalog refresh never rewrites a decision that
1922
+ * already exists, which is how Requirement 9.7 holds structurally rather than by
1923
+ * convention.
1924
+ *
1925
+ * Every absent option is spread away instead of passed as `undefined`: a key
1926
+ * carrying `undefined` still overrides the runtime's own default, which turns
1927
+ * "I did not configure this" into "I configured this to nothing"
1928
+ * (Requirement 8.7).
1929
+ * @param options - the caller's options, either store variant.
1930
+ * @param captured - the already-captured store.
1931
+ * @returns the configured adapter.
1932
+ */
1933
+ function buildCopilotAdapter(options, captured) {
1934
+ const router = createCopilotEndpointRouter({
1935
+ overrides: options.endpointOverrides ?? {},
1936
+ prefixes: [...COPILOT_RESPONSES_MODEL_PREFIXES, ...options.responsesModelPrefixes ?? []]
1937
+ });
1938
+ const editorHeaders = resolveCopilotEditorHeaders(options.editorHeaders);
1939
+ const secrets = createCopilotSecrets();
1940
+ const catalogLimits = resolveCopilotCatalogLimits(options);
1941
+ const providerId = options.id ?? "copilot";
1942
+ const cache = options.tokenCache ?? createCopilotTokenCache({
1943
+ providerId,
1944
+ editorHeaders,
1945
+ ...options.githubApiBaseUrl === void 0 ? {} : { githubApiBaseUrl: options.githubApiBaseUrl },
1946
+ ...options.exchangeMarginMs === void 0 ? {} : { marginMs: options.exchangeMarginMs },
1947
+ ...options.requestTimeoutMs === void 0 ? {} : { requestTimeoutMs: options.requestTimeoutMs },
1948
+ ...options.maxResponseBytes === void 0 ? {} : { maxResponseBytes: options.maxResponseBytes },
1949
+ ...options.maxResponseChunks === void 0 ? {} : { maxResponseChunks: options.maxResponseChunks },
1950
+ ...options.allowInsecureHttp === void 0 ? {} : { allowInsecureIssuer: options.allowInsecureHttp },
1951
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
1952
+ });
1953
+ const sessionId = options.dialect?.promptCacheKey ?? randomId();
1954
+ return createRuntimeHttpProvider({
1955
+ displayName: COPILOT_DISPLAY_NAME,
1956
+ protocol: copilotDualProtocol({
1957
+ router,
1958
+ responses: openAiResponsesProtocol,
1959
+ chat: openAiChatCompletionsProtocol,
1960
+ ...options.onEndpointDecision === void 0 ? {} : { onDecision: options.onEndpointDecision }
1961
+ }),
1962
+ baseUrl: options.baseUrl ?? "https://api.githubcopilot.com",
1963
+ ...options.allowInsecureHttp === void 0 ? {} : { allowInsecureHttp: options.allowInsecureHttp },
1964
+ dialect: {
1965
+ ...options.dialect,
1966
+ promptCacheKey: sessionId
1967
+ },
1968
+ auth: {
1969
+ kind: "dynamic",
1970
+ resolve: async ({ signal, context }) => {
1971
+ const operation = {
1972
+ signal,
1973
+ logger: context?.logger ?? NULL_LOGGER$1
1974
+ };
1975
+ const snapshot = await readCopilotSnapshot(captured, operation);
1976
+ const github = requireGitHubToken(snapshot.file, snapshot.label);
1977
+ secrets.remember("github", github.token);
1978
+ const api = await cache.acquire(snapshot, operation, context);
1979
+ secrets.remember("api", api.token);
1980
+ return {
1981
+ authorization: `Bearer ${api.token}`,
1982
+ "editor-version": editorHeaders.editorVersion,
1983
+ "editor-plugin-version": editorHeaders.editorPluginVersion,
1984
+ "x-request-id": randomId()
1985
+ };
1986
+ }
1987
+ },
1988
+ ...options.models === void 0 ? { discoverModels: async (context) => {
1989
+ const snapshot = await discoverCopilotModels(context, catalogLimits, options.fetch ?? globalThis.fetch);
1990
+ router.learn(snapshot.generation);
1991
+ return snapshot.generation.map((entry) => entry.model);
1992
+ } } : { models: options.models },
1993
+ ...copilotCatalogCacheOptions(options),
1994
+ ...options.maxCatalogModels === void 0 ? {} : { maxCatalogModels: options.maxCatalogModels },
1995
+ ...options.maxCatalogBytes === void 0 ? {} : { maxCatalogBytes: options.maxCatalogBytes },
1996
+ ...options.defaultMaxTokens === void 0 ? {} : { defaultMaxTokens: options.defaultMaxTokens },
1997
+ ...options.defaultContextWindow === void 0 ? {} : { defaultContextWindow: options.defaultContextWindow },
1998
+ ...transportLimits(options),
1999
+ baseHeaders: COPILOT_TRANSPORT_HEADERS,
2000
+ ...options.retryPolicy === void 0 ? {} : { retryPolicy: options.retryPolicy },
2001
+ ...options.requestLogger === void 0 ? {} : { requestLogger: options.requestLogger },
2002
+ errorCode: (status, detail) => isMissingEditorHeaderFailure(status, detail) ? COPILOT_ERROR_CODES.EDITOR_HEADERS_MISSING : void 0,
2003
+ fetch: copilotProviderFetch(options, secrets)
2004
+ });
2005
+ }
2006
+ /**
2007
+ * The transactional plugin for installing the Copilot provider.
2008
+ *
2009
+ * Composition follows `codexPlugin`: `id` defaults to {@link COPILOT_ROUTE_ID},
2010
+ * `family` is `'copilot'`, `routes` defaults to `[id]`, and a string
2011
+ * `defaultModel` requires exactly one route so the model target's provider can be
2012
+ * inferred (Requirements 7.4, 7.5).
2013
+ *
2014
+ * One difference from Codex: there is no overload per store variant. The
2015
+ * compare-and-swap store is the main path here, the read/write variant exists for
2016
+ * symmetry, and {@link copilotAdapter} is where it is accepted (Requirement 6.2).
2017
+ * The marker is checked at construction rather than at setup so a wrong store is
2018
+ * reported while the runtime is being composed, not on the first generation.
2019
+ * @param options - the same options {@link copilotAdapter} takes, CAS store only.
2020
+ * @returns a composable plugin registering one Copilot adapter.
2021
+ * @throws TypeError when `authStore` is not the compare-and-swap variant, or when
2022
+ * a string `defaultModel` is paired with anything but exactly one route.
2023
+ */
2024
+ function copilotPlugin(options) {
2025
+ if (!isCredentialStoreInput(options?.authStore)) throw new TypeError("copilotPlugin requires a Copilot credential store (the CAS variant)");
2026
+ const id = options.id ?? "copilot";
2027
+ const routes = Object.freeze([...options.routes ?? [id]]);
2028
+ return defineModelProviderPlugin({
2029
+ id,
2030
+ family: "copilot",
2031
+ displayName: COPILOT_DISPLAY_NAME,
2032
+ routes,
2033
+ ...runtimeDefaultModel(options.defaultModel, routes),
2034
+ setup(registrar) {
2035
+ const adapter = buildCopilotAdapter(options, captureCopilotStore(options.authStore));
2036
+ const remove = registrar.registerAdapter(adapter);
2037
+ return () => {
2038
+ remove();
2039
+ };
2040
+ }
2041
+ });
2042
+ }
2043
+ /**
2044
+ * Marker inspection only: no accessor is invoked and no method is captured.
2045
+ *
2046
+ * Full capture stays deferred to {@link buildCopilotAdapter}, so this check
2047
+ * cannot be the thing that runs the caller's code.
2048
+ * @param value - the `authStore` as passed in.
2049
+ * @returns true when it carries the credential-store marker.
2050
+ */
2051
+ function isCredentialStoreInput(value) {
2052
+ if (typeof value !== "object" || value === null) return false;
2053
+ const marker = Object.getOwnPropertyDescriptor(value, "kind");
2054
+ return marker !== void 0 && "value" in marker && marker.value === "credential-store";
2055
+ }
2056
+ /**
2057
+ * Resolve `defaultModel`, demanding one route for the string form.
2058
+ *
2059
+ * A string names a model but not a provider, and the provider is inferred from
2060
+ * the route. With two routes there is no answer, and picking the first would
2061
+ * install a default nobody chose (Requirement 7.5).
2062
+ * @param value - the caller's default model, when they set one.
2063
+ * @param routes - the routes this plugin installs.
2064
+ * @returns a one-key spread carrying `defaultModel`, or an empty one.
2065
+ * @throws TypeError when a string is paired with anything but exactly one route.
2066
+ */
2067
+ function runtimeDefaultModel(value, routes) {
2068
+ if (value === void 0) return {};
2069
+ if (typeof value !== "string") return { defaultModel: value };
2070
+ if (routes.length !== 1) throw new TypeError("A string defaultModel requires exactly one Copilot route");
2071
+ return { defaultModel: Object.freeze({
2072
+ provider: routes[0] ?? "copilot",
2073
+ id: value
2074
+ }) };
2075
+ }
2076
+ /**
2077
+ * Read the credential store once, through whichever variant was captured.
2078
+ *
2079
+ * The read/write variant has no revisions, so its snapshot revision is `null` —
2080
+ * which the token cache compares just as strictly as a real revision, it simply
2081
+ * never changes on its own.
2082
+ * @param captured - the captured store.
2083
+ * @param operation - the calling operation, whose signal bounds the read.
2084
+ * @returns the file, its revision and the store label, as one snapshot.
2085
+ * @throws AgentSdkError with the SDK's missing-credential code when the store is
2086
+ * empty (Requirement 13.4).
2087
+ */
2088
+ async function readCopilotSnapshot(captured, operation) {
2089
+ const record = captured.kind === "versioned" ? await captured.store.read(operation) : {
2090
+ value: await captured.store.read(),
2091
+ revision: null
2092
+ };
2093
+ return Object.freeze({
2094
+ file: requireCopilotFile(record?.value, captured.label),
2095
+ revision: record?.revision ?? null,
2096
+ label: captured.label
2097
+ });
2098
+ }
2099
+ /**
2100
+ * Demand a credential file, reusing the one message that says how to get one.
2101
+ *
2102
+ * `requireGitHubToken` owns the message and the code for all three shapes of "no
2103
+ * credential", so it is asked first. The throw after it is UNREACHABLE — an
2104
+ * absent file already failed there — and exists only so the type narrows without
2105
+ * a non-null assertion.
2106
+ * @param file - the file the store returned, or `undefined` for an empty store.
2107
+ * @param label - the store location named in the diagnostic.
2108
+ * @returns the file.
2109
+ */
2110
+ function requireCopilotFile(file, label) {
2111
+ requireGitHubToken(file, label);
2112
+ if (file === void 0) throw new AgentSdkError(`no GitHub Copilot credentials at ${label}`, MISSING_CREDENTIAL_CODE);
2113
+ return file;
2114
+ }
2115
+ /** Build the two-slot secret registry. */
2116
+ function createCopilotSecrets() {
2117
+ let github = "";
2118
+ let api = "";
2119
+ return {
2120
+ remember(kind, value) {
2121
+ if (value.length === 0) return;
2122
+ if (kind === "github") github = value;
2123
+ else api = value;
2124
+ },
2125
+ redact(text) {
2126
+ let result = text;
2127
+ for (const secret of [github, api]) {
2128
+ if (secret.length === 0) continue;
2129
+ result = result.split(secret).join(REDACTED);
2130
+ }
2131
+ return result;
2132
+ }
2133
+ };
2134
+ }
2135
+ /**
2136
+ * The fetch the provider dispatches through: identical to the injected one,
2137
+ * except that an error body is redacted — and, for the one case the endpoint is
2138
+ * known to be unhelpful about, explained — before anything retains it.
2139
+ *
2140
+ * Why here and not in an error mapper: `provider-http` puts the raw error body
2141
+ * into the failure's `cause`, and by the time a mapper sees it the text is
2142
+ * already retained. Redacting at the transport is the only point that runs BEFORE
2143
+ * that, and an endpoint echoing the `Authorization` header back in an error body
2144
+ * is something that has actually happened (Requirement 13.7).
2145
+ *
2146
+ * What is deliberately NOT touched:
2147
+ *
2148
+ * - **Successful responses.** The body is a live SSE stream and must reach the
2149
+ * pipeline unread and unwrapped.
2150
+ * - **Redirects, in every shape.** Rebuilding a `Response` loses `type`,
2151
+ * `redirected` and `url` — the three signals the transport's redirect guard
2152
+ * reads — so anything that is not a 4xx/5xx passes through untouched and the
2153
+ * guard still sees the original (Requirement 7.8).
2154
+ * @param options - read for the injected fetch and the error-body bound.
2155
+ * @param secrets - the live token values to redact.
2156
+ * @returns a fetch implementation to hand to the runtime provider.
2157
+ */
2158
+ function copilotProviderFetch(options, secrets) {
2159
+ const inner = options.fetch ?? globalThis.fetch;
2160
+ const maxBytes = options.maxErrorBodyBytes ?? DEFAULT_MAX_ERROR_BODY_BYTES;
2161
+ return async (...args) => {
2162
+ const response = await inner(...args);
2163
+ if (response.status < 400 || response.type === "opaqueredirect" || response.redirected) return response;
2164
+ let raw;
2165
+ try {
2166
+ raw = await readErrorBody(response, maxBytes);
2167
+ } catch {
2168
+ return response;
2169
+ }
2170
+ const redacted = secrets.redact(raw);
2171
+ const body = isMissingEditorHeaderFailure(response.status, redacted) ? editorHeaderDiagnostic(redacted) : redacted;
2172
+ const headers = new Headers(response.headers);
2173
+ headers.delete("content-length");
2174
+ return new Response(body, {
2175
+ status: response.status,
2176
+ statusText: response.statusText,
2177
+ headers
2178
+ });
2179
+ };
2180
+ }
2181
+ /**
2182
+ * Read an error body up to a byte bound, marking a truncation rather than hiding it.
2183
+ * @param response - the non-success response.
2184
+ * @param maxBytes - the configured bound (Requirement 13.6).
2185
+ * @returns the decoded text, truncated with a note when it hit the bound.
2186
+ */
2187
+ async function readErrorBody(response, maxBytes) {
2188
+ if (response.body === null) return "";
2189
+ const reader = response.body.getReader();
2190
+ const decoder = new TextDecoder();
2191
+ let bytes = 0;
2192
+ let text = "";
2193
+ try {
2194
+ while (true) {
2195
+ const next = await reader.read();
2196
+ if (next.done) return text + decoder.decode();
2197
+ if (next.value === void 0) continue;
2198
+ const remaining = maxBytes - bytes;
2199
+ if (remaining <= 0 || next.value.byteLength > remaining) {
2200
+ const kept = remaining <= 0 ? void 0 : next.value.subarray(0, remaining);
2201
+ const partial = kept === void 0 ? "" : decoder.decode(kept, { stream: true });
2202
+ await reader.cancel().catch(() => void 0);
2203
+ return `${text}${partial}${decoder.decode()}\n[error body truncated at ${maxBytes} bytes]`;
2204
+ }
2205
+ bytes += next.value.byteLength;
2206
+ text += decoder.decode(next.value, { stream: true });
2207
+ }
2208
+ } finally {
2209
+ reader.releaseLock();
2210
+ }
2211
+ }
2212
+ /**
2213
+ * Whether a failure looks like the endpoint refusing a request for a missing
2214
+ * editor header.
2215
+ *
2216
+ * Matched BROADLY on purpose. The endpoint's wording is not a contract — it is
2217
+ * one sentence that can be rephrased at any time — so this looks for the header
2218
+ * names in any plausible spelling, or for the word "editor" beside a complaint
2219
+ * about a header. It is also bounded to status 400: a 400 that says nothing about
2220
+ * editors keeps `REQUEST_INVALID` from the shared mapping rather than being
2221
+ * relabelled into a Copilot-specific failure it is not (Requirement 2.5).
2222
+ * @param status - the response status.
2223
+ * @param detail - the provider's error text, joined by the shared parser.
2224
+ * @returns true when the missing-header diagnosis is warranted.
2225
+ */
2226
+ function isMissingEditorHeaderFailure(status, detail) {
2227
+ if (status !== 400) return false;
2228
+ if (/editor[\s_-]*(?:plugin[\s_-]*)?version/i.test(detail)) return true;
2229
+ return /\beditor\b/i.test(detail) && /(missing|required|absent|invalid|unsupported|unrecogni[sz]ed|header)/i.test(detail);
2230
+ }
2231
+ /**
2232
+ * Wrap the endpoint's 400 in a body that names both headers and how to set them.
2233
+ *
2234
+ * The endpoint's own text is kept beside it rather than replaced: it is the
2235
+ * evidence, and the shared classifier reads it too.
2236
+ * @param endpointText - the endpoint's error body, already redacted.
2237
+ * @returns a JSON error body carrying the SDK-authored diagnosis.
2238
+ */
2239
+ function editorHeaderDiagnostic(endpointText) {
2240
+ return JSON.stringify({ error: {
2241
+ code: COPILOT_ERROR_CODES.EDITOR_HEADERS_MISSING,
2242
+ message: `the Copilot endpoint rejected this request for a missing or unaccepted editor header. Both \`Editor-Version\` and \`Editor-Plugin-Version\` are mandatory; configure them with the \`editorHeaders\` option (\`editorVersion\`, \`editorPluginVersion\`), whose defaults are the exported COPILOT_EDITOR_VERSION and COPILOT_EDITOR_PLUGIN_VERSION constants. The endpoint said: ${endpointText}`
2243
+ } });
2244
+ }
2245
+ /**
2246
+ * Forward every transport bound the caller set, and only those.
2247
+ * @param options - the caller's options.
2248
+ * @returns an object carrying the configured transport limits.
2249
+ */
2250
+ function transportLimits(options) {
2251
+ return {
2252
+ ...options.streamIdleTimeoutMs === void 0 ? {} : { streamIdleTimeoutMs: options.streamIdleTimeoutMs },
2253
+ ...options.requestTimeoutMs === void 0 ? {} : { requestTimeoutMs: options.requestTimeoutMs },
2254
+ ...options.maxRequestBytes === void 0 ? {} : { maxRequestBytes: options.maxRequestBytes },
2255
+ ...options.maxResponseBytes === void 0 ? {} : { maxResponseBytes: options.maxResponseBytes },
2256
+ ...options.maxResponseChunks === void 0 ? {} : { maxResponseChunks: options.maxResponseChunks },
2257
+ ...options.maxSseEvents === void 0 ? {} : { maxSseEvents: options.maxSseEvents },
2258
+ ...options.maxSseEventChars === void 0 ? {} : { maxSseEventChars: options.maxSseEventChars },
2259
+ ...options.maxErrorBodyBytes === void 0 ? {} : { maxErrorBodyBytes: options.maxErrorBodyBytes },
2260
+ ...options.requestLoggerTimeoutMs === void 0 ? {} : { requestLoggerTimeoutMs: options.requestLoggerTimeoutMs }
2261
+ };
2262
+ }
2263
+ /** A client-side correlation id; carries nothing about the account or the prompt. */
2264
+ function randomId() {
2265
+ return globalThis.crypto?.randomUUID?.() ?? `sdk-${Date.now().toString(36)}`;
2266
+ }
2267
+
2268
+ //#endregion
2269
+ //#region src/oauth.ts
2270
+ /** GitHub's OAuth issuer. */
2271
+ const DEFAULT_COPILOT_OAUTH_ISSUER = "https://github.com";
2272
+ /**
2273
+ * Default OAuth client id. Public, not a secret.
2274
+ *
2275
+ * This is the client id published in GitHub's own editor-plugin sources (the
2276
+ * value `copilot.vim` and the other Copilot editor integrations ship in the
2277
+ * clear), which is why it is on the allowlist that
2278
+ * `copilot_internal/v2/token` checks. Sending it means this SDK signs in AS that
2279
+ * editor client. See the module note for why that makes it a named option
2280
+ * instead of a hidden constant.
2281
+ *
2282
+ * ⚠ UNVERIFIED against a live account. Recorded 2026-09-10 from public editor
2283
+ * integration sources only; no sign-in against a real Copilot account has
2284
+ * confirmed THIS client id.
2285
+ *
2286
+ * The 2026-09-10 live run that confirmed the two editor headers did NOT confirm
2287
+ * this value, and could not: it was handed an existing `ghu_` user-to-server token
2288
+ * out of band, so it exercised the EXCHANGE (which answered 200 for that token)
2289
+ * while never running the device flow that would put this `client_id` on the wire.
2290
+ * What that run does establish is the shape of the claim still outstanding — the
2291
+ * exchange endpoint and the allowlist check are live and reachable, and the only
2292
+ * untested link is whether they accept a token minted by this particular app.
2293
+ *
2294
+ * TODO(copilot-identity): confirm on a real Copilot account, then replace this
2295
+ * warning with the confirmation date. To confirm: run the device flow against
2296
+ * `https://github.com/login/device/code` with this `client_id` and
2297
+ * `scope=read:user`, approve it on a Copilot-enabled account, then exchange the
2298
+ * resulting user token at `GET https://api.github.com/copilot_internal/v2/token`.
2299
+ * The client id is confirmed when that exchange returns a Copilot token rather
2300
+ * than 401/403. A non-allowlisted client id fails at the exchange, not at
2301
+ * sign-in, so the device flow succeeding on its own proves nothing — and equally,
2302
+ * an exchange that succeeds for a token this flow did not mint proves nothing
2303
+ * about this constant.
2304
+ */
2305
+ const COPILOT_OAUTH_CLIENT_ID = "Iv1.b507a08c87ecfe98";
2306
+ /** Requested scope; enough to exchange a token and read identity, no more. */
2307
+ const COPILOT_OAUTH_SCOPE = "read:user";
2308
+ /**
2309
+ * The absolute ceiling on one device login, INDEPENDENT of the server's
2310
+ * `expires_in`.
2311
+ *
2312
+ * `expires_in` is honoured when it is shorter — there is no point polling a code
2313
+ * the server has already retired. It is not honoured when it is longer: a server
2314
+ * that answers `expires_in: 86400` would otherwise hang a CLI for a day, and this
2315
+ * SDK is not the right place to hold that terminal hostage (Requirement 4.3).
2316
+ */
2317
+ const COPILOT_DEVICE_CODE_MAX_WAIT_MS = 9e5;
2318
+ /** Poll interval used when the device-code response states none. */
2319
+ const COPILOT_DEFAULT_POLL_INTERVAL_SECONDS = 5;
2320
+ /**
2321
+ * Seconds added on every `slow_down`, per RFC 8628 §3.5.
2322
+ *
2323
+ * The increment is what makes the wait STRICTLY increase even when the server
2324
+ * repeats `slow_down` without a new `interval`. Without it, a server that only
2325
+ * ever says "slow down" would be polled at exactly the rate it just objected to.
2326
+ */
2327
+ const COPILOT_SLOW_DOWN_INCREMENT_SECONDS = 5;
2328
+ /**
2329
+ * The warning shown beside the user code, worded exactly as the Codex device
2330
+ * prompt words it.
2331
+ *
2332
+ * A device code is a bearer of authorization that the user types into a page they
2333
+ * navigated to themselves. The one attack that works is getting somebody to type
2334
+ * an attacker's code, so the prompt has to say so; and it lives here rather than
2335
+ * in the CLI so every front end that renders a Copilot prompt renders the same
2336
+ * sentence.
2337
+ */
2338
+ const COPILOT_DEVICE_LOGIN_WARNING = "Only continue if YOU started this login. If someone sent you this code, stop.";
2339
+ /** GitHub's device-authorization leg. */
2340
+ const DEVICE_CODE_PATH = "/login/device/code";
2341
+ /** GitHub's device-token leg. */
2342
+ const DEVICE_TOKEN_PATH = "/login/oauth/access_token";
2343
+ /** The device-code grant type, spelled as RFC 8628 requires. */
2344
+ const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2345
+ /** The command that produces a credential, named when a login ends without one. */
2346
+ const COPILOT_LOGIN_COMMAND$1 = "npm run provider:copilot:login-device";
2347
+ const NEVER_ABORTED_SIGNAL = new AbortController().signal;
2348
+ const NULL_LOGGER = Object.freeze({
2349
+ child: () => NULL_LOGGER,
2350
+ trace: () => void 0,
2351
+ debug: () => void 0,
2352
+ info: () => void 0,
2353
+ warn: () => void 0,
2354
+ error: () => void 0,
2355
+ fatal: () => void 0
2356
+ });
2357
+ /** The real scheduler: `setTimeout`, `clearTimeout` and `Date.now`. */
2358
+ const DEFAULT_COPILOT_TIMER = Object.freeze({
2359
+ setTimeout: (handler, ms) => setTimeout(handler, ms),
2360
+ clearTimeout: (handle) => clearTimeout(handle),
2361
+ now: () => Date.now()
2362
+ });
2363
+ /**
2364
+ * Start a device authorization.
2365
+ *
2366
+ * `Accept: application/json` is set here as well as on the token leg. It is
2367
+ * load-bearing on the token leg (see {@link pollForCopilotToken}) and harmless
2368
+ * here, and setting it on both keeps the pair from drifting into "one of the two
2369
+ * legs parses JSON".
2370
+ * @param options - issuer, client id, scope, cancellation and read bounds.
2371
+ * @returns the code, the URL and the timings to show the user.
2372
+ * @throws CopilotDeviceLoginError with `reason: 'aborted'` when the caller's
2373
+ * signal aborts, or `reason: 'failed'` when the endpoint answers with anything
2374
+ * other than a usable device authorization.
2375
+ */
2376
+ async function requestCopilotDeviceCode(options = {}) {
2377
+ const pinned = issuerOf("oauthIssuer", options.oauthIssuer, DEFAULT_COPILOT_OAUTH_ISSUER, options);
2378
+ const body = await deviceJson({
2379
+ pinned,
2380
+ url: copilotUrl(pinned, DEVICE_CODE_PATH),
2381
+ operation: "device code",
2382
+ init: {
2383
+ method: "POST",
2384
+ headers: {
2385
+ accept: "application/json",
2386
+ "content-type": "application/json"
2387
+ },
2388
+ body: JSON.stringify({
2389
+ client_id: options.clientId ?? "Iv1.b507a08c87ecfe98",
2390
+ scope: options.scope ?? "read:user"
2391
+ })
2392
+ }
2393
+ }, "the device-code endpoint", options);
2394
+ if (!body.ok) throw deviceFailure(`the device-code endpoint failed (HTTP ${body.status})`, "failed", body.parseError);
2395
+ return Object.freeze({
2396
+ verificationUrl: verificationUrlOf(body.json, options),
2397
+ userCode: requireDeviceString(body.json, "user_code"),
2398
+ deviceCode: requireDeviceString(body.json, "device_code"),
2399
+ intervalSeconds: positiveSecondsOf(body.json.interval, 5),
2400
+ expiresInSeconds: positiveSecondsOf(body.json.expires_in, COPILOT_DEVICE_CODE_MAX_WAIT_MS / 1e3)
2401
+ });
2402
+ }
2403
+ async function runCopilotDeviceLogin(store, options = {}, progress = {}) {
2404
+ const captured = captureCopilotStore(store);
2405
+ const operation = {
2406
+ signal: options.signal ?? NEVER_ABORTED_SIGNAL,
2407
+ logger: NULL_LOGGER
2408
+ };
2409
+ const initial = await readStore(captured, operation);
2410
+ const code = await requestCopilotDeviceCode(options);
2411
+ notify(() => progress.onPrompt?.(code));
2412
+ const token = await pollForCopilotToken(code, options, progress);
2413
+ await commitStore(captured, {
2414
+ version: 1,
2415
+ github: {
2416
+ token: token.accessToken,
2417
+ ...token.tokenType === void 0 ? {} : { tokenType: token.tokenType },
2418
+ ...token.scope === void 0 ? {} : { scope: token.scope }
2419
+ },
2420
+ ...token.account === void 0 ? {} : { account: token.account },
2421
+ clientId: options.clientId ?? "Iv1.b507a08c87ecfe98",
2422
+ obtainedAt: new Date(timerOf(options).now()).toISOString()
2423
+ }, initial.revision, operation);
2424
+ return Object.freeze({
2425
+ location: captured.label,
2426
+ login: token.account?.login,
2427
+ accountId: token.account?.id,
2428
+ scope: token.scope
2429
+ });
2430
+ }
2431
+ /**
2432
+ * Poll the token leg until the user approves, the server refuses, or a bound
2433
+ * passes.
2434
+ *
2435
+ * Two things make this loop different from the Codex one, and both are easy to
2436
+ * get wrong:
2437
+ *
2438
+ * - **`Accept: application/json` is mandatory.** Without it GitHub's token
2439
+ * endpoint answers FORM-ENCODED, so a JSON parser meets
2440
+ * `error=authorization_pending&interval=10` and throws — which turns the "not
2441
+ * approved yet" branch into a hard-failure branch, and the flow can then never
2442
+ * succeed at all.
2443
+ * - **The error channel is HTTP 200 with `error` in the body.** Codex surfaces
2444
+ * "pending" as 403/404; GitHub surfaces it as a 200. So classification reads the
2445
+ * BODY FIRST and the status second. A status-first reader treats every pending
2446
+ * poll as a success and then fails looking for `access_token`.
2447
+ * @param code - the pending authorization.
2448
+ * @param options - issuer, client id, bounds and the injectable timer.
2449
+ * @param progress - poll notifications.
2450
+ * @returns the access token and whatever the endpoint disclosed beside it.
2451
+ */
2452
+ async function pollForCopilotToken(code, options, progress) {
2453
+ const pinned = issuerOf("oauthIssuer", options.oauthIssuer, DEFAULT_COPILOT_OAUTH_ISSUER, options);
2454
+ const url = copilotUrl(pinned, DEVICE_TOKEN_PATH);
2455
+ const timer = timerOf(options);
2456
+ const startedAt = timer.now();
2457
+ const deadlineAt = startedAt + Math.min(COPILOT_DEVICE_CODE_MAX_WAIT_MS, code.expiresInSeconds * 1e3);
2458
+ let intervalSeconds = code.intervalSeconds;
2459
+ while (true) {
2460
+ throwIfAborted(options.signal);
2461
+ if (timer.now() >= deadlineAt) throw deviceTimeout(startedAt, timer.now());
2462
+ notify(() => progress.onPoll?.(timer.now() - startedAt, intervalSeconds));
2463
+ const body = await deviceJson({
2464
+ pinned,
2465
+ url,
2466
+ operation: "device token",
2467
+ init: {
2468
+ method: "POST",
2469
+ headers: {
2470
+ accept: "application/json",
2471
+ "content-type": "application/json"
2472
+ },
2473
+ body: JSON.stringify({
2474
+ client_id: options.clientId ?? "Iv1.b507a08c87ecfe98",
2475
+ device_code: code.deviceCode,
2476
+ grant_type: DEVICE_GRANT_TYPE
2477
+ })
2478
+ }
2479
+ }, "the device-token endpoint", options);
2480
+ const error = typeof body.json.error === "string" ? body.json.error : void 0;
2481
+ if (error === "authorization_pending" || error === "slow_down") {
2482
+ intervalSeconds = nextIntervalSeconds(intervalSeconds, body.json.interval, error);
2483
+ const remaining = deadlineAt - timer.now();
2484
+ if (remaining <= 0) throw deviceTimeout(startedAt, timer.now());
2485
+ await sleep(Math.min(intervalSeconds * 1e3, remaining), options.signal, timer);
2486
+ continue;
2487
+ }
2488
+ if (error === "access_denied") throw deviceFailure(`the device login was denied on GitHub; run \`${COPILOT_LOGIN_COMMAND$1}\` again if you did mean to approve it`, "denied");
2489
+ if (error === "expired_token") throw deviceFailure(`the device code expired before it was approved; run \`${COPILOT_LOGIN_COMMAND$1}\` again to request a new code`, "expired");
2490
+ if (error !== void 0) throw deviceFailure(`the device-token endpoint refused the request (${error}, HTTP ${body.status})`, "failed");
2491
+ if (!body.ok) throw deviceFailure(`the device-token endpoint failed (HTTP ${body.status})`, "failed", body.parseError);
2492
+ return Object.freeze({
2493
+ accessToken: requireDeviceString(body.json, "access_token"),
2494
+ tokenType: optionalString(body.json.token_type),
2495
+ scope: optionalString(body.json.scope),
2496
+ account: accountIdentityOf(body.json)
2497
+ });
2498
+ }
2499
+ }
2500
+ /**
2501
+ * The effective wait after a `slow_down`, which must STRICTLY increase.
2502
+ *
2503
+ * `max(current, server-requested, current + 5)` — the third term is what keeps
2504
+ * the sequence increasing when the server sends no new `interval`, and taking the
2505
+ * max of all three keeps it from ever decreasing when the server sends a smaller
2506
+ * one. `authorization_pending` may carry a new interval too; there it is honoured
2507
+ * without the increment, so an ordinary pending poll does not back off forever.
2508
+ */
2509
+ function nextIntervalSeconds(current, requested, error) {
2510
+ const server = positiveSecondsOf(requested, 0);
2511
+ return error === "slow_down" ? Math.max(current, server, current + 5) : Math.max(current, server);
2512
+ }
2513
+ /**
2514
+ * Wait `ms`, losing the race to `signal` the instant it aborts.
2515
+ *
2516
+ * The timer is injected rather than closed over, so a test can drive the poll
2517
+ * loop through fifteen virtual minutes in a millisecond. Aborting rejects instead
2518
+ * of resolving early, because a caller who pressed Ctrl-C wants the flow to END,
2519
+ * not to take one more turn round the loop.
2520
+ */
2521
+ function sleep(ms, signal, timer) {
2522
+ if (signal?.aborted === true) return Promise.reject(deviceAborted());
2523
+ return new Promise((resolve, reject) => {
2524
+ const onAbort = () => {
2525
+ timer.clearTimeout(handle);
2526
+ reject(deviceAborted());
2527
+ };
2528
+ const handle = timer.setTimeout(() => {
2529
+ signal?.removeEventListener("abort", onAbort);
2530
+ resolve();
2531
+ }, ms);
2532
+ signal?.addEventListener("abort", onAbort, { once: true });
2533
+ });
2534
+ }
2535
+ /**
2536
+ * Dispatch one OAuth leg and read its body within the configured bounds.
2537
+ *
2538
+ * A body that is not a JSON object yields an EMPTY object plus `parseError`
2539
+ * rather than throwing: the status still has to be classified, and on the token
2540
+ * leg an unreadable body is one of the shapes a misconfigured `Accept` header
2541
+ * produces. Callers therefore always get to the body-first branch, and reach a
2542
+ * hard failure only after it finds no `error`.
2543
+ */
2544
+ async function deviceJson(request, what, options) {
2545
+ let response;
2546
+ try {
2547
+ response = await copilotFetch(request, options);
2548
+ } catch (error) {
2549
+ throwIfAborted(options.signal);
2550
+ throw error instanceof CopilotDeviceLoginError ? error : deviceFailure(`${what} could not be reached`, "failed", error);
2551
+ }
2552
+ let raw;
2553
+ try {
2554
+ raw = await readCopilotResponseText(response, options);
2555
+ } catch (error) {
2556
+ throwIfAborted(options.signal);
2557
+ throw deviceFailure(`${what} returned a response beyond the configured limits`, "failed", error);
2558
+ }
2559
+ try {
2560
+ const parsed = JSON.parse(raw);
2561
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new TypeError(`${what} returned JSON that is not an object`);
2562
+ return {
2563
+ ok: response.ok,
2564
+ status: response.status,
2565
+ json: parsed,
2566
+ parseError: void 0
2567
+ };
2568
+ } catch (error) {
2569
+ return {
2570
+ ok: response.ok,
2571
+ status: response.status,
2572
+ json: {},
2573
+ parseError: error
2574
+ };
2575
+ }
2576
+ }
2577
+ /**
2578
+ * Read the verification URL the user is told to open.
2579
+ *
2580
+ * Only the SCHEME is constrained, not the origin. This SDK never fetches this
2581
+ * URL — it prints it — and GitHub Enterprise deployments legitimately answer with
2582
+ * a host other than the issuer, so an origin pin here would reject working
2583
+ * installations to guard a request that is never made. The scheme check remains
2584
+ * because a `javascript:` or `data:` URL handed to a browser opener is a real
2585
+ * problem, and {@link COPILOT_DEVICE_LOGIN_WARNING} covers the rest.
2586
+ */
2587
+ function verificationUrlOf(body, options) {
2588
+ const raw = requireDeviceString(body, "verification_uri");
2589
+ let url;
2590
+ try {
2591
+ url = new URL(raw);
2592
+ } catch (error) {
2593
+ throw deviceFailure("the device-code endpoint returned an unusable verification URL", "failed", error);
2594
+ }
2595
+ if (url.protocol !== "https:" && !(options.allowInsecureIssuer === true && url.protocol === "http:")) throw deviceFailure("the device-code verification URL must use https", "failed");
2596
+ return url.href;
2597
+ }
2598
+ /** Identity fields, present only when the endpoint disclosed them (Property 16). */
2599
+ function accountIdentityOf(body) {
2600
+ const login = optionalString(body.login);
2601
+ const name = optionalString(body.name);
2602
+ const id = typeof body.id === "number" && Number.isFinite(body.id) ? body.id : void 0;
2603
+ if (login === void 0 && name === void 0 && id === void 0) return void 0;
2604
+ return Object.freeze({
2605
+ ...login === void 0 ? {} : { login },
2606
+ ...name === void 0 ? {} : { name },
2607
+ ...id === void 0 ? {} : { id }
2608
+ });
2609
+ }
2610
+ function optionalString(value) {
2611
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2612
+ }
2613
+ function requireDeviceString(body, key) {
2614
+ const value = body[key];
2615
+ if (typeof value !== "string" || value.length === 0) throw deviceFailure(`the device flow response omitted "${key}"`, "failed");
2616
+ return value;
2617
+ }
2618
+ /**
2619
+ * Read a seconds value that the endpoint may send as a number, as a numeric
2620
+ * string, or not at all.
2621
+ *
2622
+ * GitHub has been observed sending `interval` as a string, so both forms are
2623
+ * accepted; anything unparsable falls back rather than failing the login, because
2624
+ * a bad hint about pacing is not a reason to refuse a working authorization.
2625
+ */
2626
+ function positiveSecondsOf(value, fallbackSeconds) {
2627
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : NaN;
2628
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackSeconds;
2629
+ }
2630
+ function timerOf(options) {
2631
+ return options.timer ?? DEFAULT_COPILOT_TIMER;
2632
+ }
2633
+ /** Run a progress observer; observers do not own authentication. */
2634
+ function notify(report) {
2635
+ try {
2636
+ report();
2637
+ } catch {}
2638
+ }
2639
+ function throwIfAborted(signal) {
2640
+ if (signal?.aborted === true) throw deviceAborted();
2641
+ }
2642
+ function deviceAborted() {
2643
+ return deviceFailure("the device login was cancelled", "aborted");
2644
+ }
2645
+ function deviceTimeout(startedAt, now) {
2646
+ return deviceFailure(`the device login was not approved within ${Math.round((now - startedAt) / 1e3)}s (bound: ${COPILOT_DEVICE_CODE_MAX_WAIT_MS / 6e4} minutes); run \`${COPILOT_LOGIN_COMMAND$1}\` again`, "timeout");
2647
+ }
2648
+ function deviceFailure(message, reason, cause) {
2649
+ return new CopilotDeviceLoginError(credentialFailure(message, cause), reason);
2650
+ }
2651
+ async function readStore(captured, operation) {
2652
+ if (captured.kind === "versioned") {
2653
+ const record = await captured.store.read(operation);
2654
+ return record === void 0 ? {
2655
+ file: void 0,
2656
+ revision: null
2657
+ } : {
2658
+ file: record.value,
2659
+ revision: record.revision
2660
+ };
2661
+ }
2662
+ return {
2663
+ file: await captured.store.read(),
2664
+ revision: null
2665
+ };
2666
+ }
2667
+ async function commitStore(captured, file, expectedRevision, operation) {
2668
+ if (captured.kind === "versioned") {
2669
+ await captured.store.commit({
2670
+ value: file,
2671
+ expectedRevision
2672
+ }, operation);
2673
+ return;
2674
+ }
2675
+ await captured.store.write(file);
2676
+ }
2677
+
2678
+ //#endregion
2679
+ export { COPILOT_BASE_URL, COPILOT_CATALOG_PATH, COPILOT_DEFAULT_CATALOG_TIMEOUT_MS, COPILOT_DEFAULT_DIALECT, COPILOT_DEFAULT_MAX_CATALOG_BYTES, COPILOT_DEFAULT_MAX_CATALOG_CHUNKS, COPILOT_DEFAULT_MAX_CATALOG_MODELS, COPILOT_DEFAULT_POLL_INTERVAL_SECONDS, COPILOT_DEVICE_CODE_MAX_WAIT_MS, COPILOT_DEVICE_LOGIN_WARNING, COPILOT_DISPLAY_NAME, COPILOT_DUAL_PROTOCOL_ID, COPILOT_EDITOR_PLUGIN_VERSION, COPILOT_EDITOR_VERSION, COPILOT_ERROR_CODES, COPILOT_LOGIN_COMMAND, COPILOT_OAUTH_CLIENT_ID, COPILOT_OAUTH_SCOPE, COPILOT_PROVIDER_ID, COPILOT_RESPONSES_MODEL_PREFIXES, COPILOT_ROUTE_ID, COPILOT_SLOW_DOWN_INCREMENT_SECONDS, COPILOT_TOKEN_EXCHANGE_MARGIN_MS, COPILOT_TOKEN_EXCHANGE_PATH, CopilotDeviceLoginError, CopilotTokenExchangeError, DEFAULT_COPILOT_OAUTH_ISSUER, DEFAULT_COPILOT_TIMER, DEFAULT_GITHUB_API_BASE_URL, copilotAdapter, copilotCatalogCacheOptions, copilotDualProtocol, copilotPlugin, createCopilotEndpointRouter, createCopilotTokenCache, credentialFailure, discoverCopilotModels, exchangeCopilotToken, memoryCopilotAuthStore, memoryCopilotCredentialStore, partitionCopilotCatalog, requestCopilotDeviceCode, requireGitHubToken, resolveCopilotCatalogLimits, runCopilotDeviceLogin, shouldExchange, toChatCompletionsDialect, toResponsesDialect };
2680
+ //# sourceMappingURL=index.js.map