dsh-plugin-subscriptions 0.4.2 → 0.5.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.
Files changed (44) hide show
  1. package/README.md +19 -6
  2. package/README.zh.md +18 -6
  3. package/lib/auth/device-flow.d.ts +64 -0
  4. package/lib/auth/device-flow.js +176 -0
  5. package/lib/auth/oauth-flow.js +1 -1
  6. package/lib/auth/rpc.d.ts +21 -2
  7. package/lib/auth/rpc.js +23 -3
  8. package/lib/auth/store.d.ts +20 -2
  9. package/lib/auth/store.js +45 -9
  10. package/lib/client/ImageGallery.d.ts +54 -0
  11. package/lib/client/ImageGallery.js +112 -0
  12. package/lib/client/ImageGenerateToolview.d.ts +1 -1
  13. package/lib/client/ImageGenerateToolview.js +2 -2
  14. package/lib/client/SpeedSelect.d.ts +48 -0
  15. package/lib/client/SpeedSelect.js +173 -0
  16. package/lib/client/SubscriptionsSection.d.ts +10 -1
  17. package/lib/client/SubscriptionsSection.js +48 -5
  18. package/lib/client/index.d.ts +1 -0
  19. package/lib/client/index.js +42 -0
  20. package/lib/client/locales.d.ts +22 -0
  21. package/lib/client/locales.js +22 -0
  22. package/lib/client.js +679 -77
  23. package/lib/client.js.map +1 -1
  24. package/lib/index.d.ts +3 -2
  25. package/lib/index.js +1868 -183
  26. package/lib/providers/catalog-store.js +19 -0
  27. package/lib/providers/claude.d.ts +20 -1
  28. package/lib/providers/claude.js +58 -31
  29. package/lib/providers/codex.d.ts +27 -0
  30. package/lib/providers/codex.js +117 -27
  31. package/lib/providers/common.d.ts +34 -1
  32. package/lib/providers/common.js +48 -1
  33. package/lib/providers/copilot.d.ts +315 -0
  34. package/lib/providers/copilot.js +786 -0
  35. package/lib/providers/grok.d.ts +7 -2
  36. package/lib/providers/grok.js +46 -18
  37. package/lib/tools/image-generate.js +3 -10
  38. package/lib/translate/anthropic.d.ts +47 -6
  39. package/lib/translate/anthropic.js +135 -20
  40. package/lib/translate/chat-completions.d.ts +120 -0
  41. package/lib/translate/chat-completions.js +363 -0
  42. package/lib/translate/responses.d.ts +49 -5
  43. package/lib/translate/responses.js +40 -7
  44. package/package.json +6 -3
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, createUserMessage, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
2
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
3
3
  import { createServer } from "node:http";
4
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
@@ -155,7 +155,7 @@ var OAuthFlowManager = class {
155
155
  if (this.attempts.has(provider)) throw new Error(`a ${provider} login attempt is already in progress`);
156
156
  const input = {
157
157
  redirectUri: "",
158
- state: randomToken(16),
158
+ state: randomToken(32),
159
159
  pkce: createPkce(),
160
160
  nonce: randomHex(8)
161
161
  };
@@ -250,6 +250,164 @@ var OAuthFlowManager = class {
250
250
  }
251
251
  };
252
252
 
253
+ //#endregion
254
+ //#region src/auth/device-flow.ts
255
+ /**
256
+ * GitHub OAuth device-authorization flow (RFC 8628) for providers that cannot
257
+ * use the loopback redirect engine: no redirect URI, no PKCE, no client
258
+ * secret. The user opens a verification URL and types a short code while the
259
+ * plugin polls the token endpoint until GitHub releases the access token.
260
+ * The management model (one attempt per provider, `isBusy`/`pending`/`cancel`)
261
+ * mirrors {@link OAuthFlowManager} so the auth controller can treat both
262
+ * engines uniformly.
263
+ */
264
+ /** Default poll interval when the device-code response omits one. */
265
+ const DEFAULT_INTERVAL_SEC = 5;
266
+ /** Default device-code lifetime when the response omits one (GitHub: 15 minutes). */
267
+ const DEFAULT_EXPIRES_IN_SEC = 900;
268
+ /** Sleep for `ms`, rejecting early when the signal aborts. */
269
+ function sleep$1(ms, signal) {
270
+ return new Promise((resolve, reject) => {
271
+ if (signal.aborted) {
272
+ reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
273
+ return;
274
+ }
275
+ const timer = setTimeout(() => {
276
+ signal.removeEventListener("abort", onAbort);
277
+ resolve();
278
+ }, ms);
279
+ timer.unref();
280
+ const onAbort = () => {
281
+ clearTimeout(timer);
282
+ reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
283
+ };
284
+ signal.addEventListener("abort", onAbort, { once: true });
285
+ });
286
+ }
287
+ /**
288
+ * Own the set of in-flight device-flow attempts, keyed by provider. One
289
+ * attempt per provider at a time; an attempt removes itself when it settles.
290
+ */
291
+ var DeviceFlowManager = class {
292
+ attempts = /* @__PURE__ */ new Map();
293
+ /**
294
+ * Whether a device-flow attempt is running for one provider.
295
+ * @param provider - the provider route.
296
+ * @returns true while an attempt is polling.
297
+ */
298
+ isBusy(provider) {
299
+ return this.attempts.has(provider);
300
+ }
301
+ /**
302
+ * The pending attempt for one provider, when any.
303
+ * @param provider - the provider route.
304
+ * @returns the in-flight attempt, or `undefined`.
305
+ */
306
+ pending(provider) {
307
+ return this.attempts.get(provider);
308
+ }
309
+ /**
310
+ * Start a device-flow attempt: request a device code, then poll the token
311
+ * endpoint in the background of `waitToken`.
312
+ * @param provider - the provider route (one attempt at a time).
313
+ * @param spec - static flow facts for this provider.
314
+ * @returns the live attempt; its `waitToken()` settles the login.
315
+ * @throws when an attempt is already running or the device-code request fails.
316
+ */
317
+ async start(provider, spec) {
318
+ if (this.attempts.has(provider)) throw new Error(`a ${provider} login attempt is already in progress`);
319
+ const fetchFn = spec.fetchFn ?? fetch;
320
+ const response = await fetchFn(spec.deviceCodeUrl, {
321
+ method: "POST",
322
+ headers: {
323
+ "accept": "application/json",
324
+ "content-type": "application/x-www-form-urlencoded"
325
+ },
326
+ body: new URLSearchParams({
327
+ client_id: spec.clientId,
328
+ scope: spec.scope
329
+ }).toString()
330
+ });
331
+ if (!response.ok) throw new Error(`${provider} device-code request failed (HTTP ${String(response.status)})`);
332
+ const wire = await response.json();
333
+ if (typeof wire.device_code !== "string" || wire.device_code.length === 0 || typeof wire.user_code !== "string" || wire.user_code.length === 0 || typeof wire.verification_uri !== "string" || wire.verification_uri.length === 0) throw new Error(`${provider} device-code response is missing device_code/user_code/verification_uri`);
334
+ const intervalSec = typeof wire.interval === "number" && wire.interval > 0 ? wire.interval : DEFAULT_INTERVAL_SEC;
335
+ const expiresInSec = typeof wire.expires_in === "number" && wire.expires_in > 0 ? wire.expires_in : DEFAULT_EXPIRES_IN_SEC;
336
+ const controller = new AbortController();
337
+ let resolveToken;
338
+ let rejectToken;
339
+ const tokenPromise = new Promise((resolve, reject) => {
340
+ resolveToken = resolve;
341
+ rejectToken = reject;
342
+ });
343
+ tokenPromise.catch(() => void 0);
344
+ const settle = (error, token) => {
345
+ if (this.attempts.get(provider) !== attempt) return;
346
+ this.attempts.delete(provider);
347
+ if (error !== void 0) rejectToken(error);
348
+ else if (token !== void 0) resolveToken(token);
349
+ };
350
+ const poll = async () => {
351
+ let intervalMs = intervalSec * 1e3;
352
+ const deadline = Date.now() + expiresInSec * 1e3;
353
+ while (true) {
354
+ await sleep$1(intervalMs, controller.signal);
355
+ if (Date.now() >= deadline) {
356
+ settle(/* @__PURE__ */ new Error(`login timed out after ${String(Math.round(expiresInSec))}s`));
357
+ return;
358
+ }
359
+ const pollResponse = await fetchFn(spec.tokenUrl, {
360
+ method: "POST",
361
+ headers: {
362
+ "accept": "application/json",
363
+ "content-type": "application/x-www-form-urlencoded"
364
+ },
365
+ body: new URLSearchParams({
366
+ client_id: spec.clientId,
367
+ device_code: wire.device_code,
368
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
369
+ }).toString(),
370
+ signal: controller.signal
371
+ });
372
+ const result = await pollResponse.json();
373
+ if (typeof result.access_token === "string" && result.access_token.length > 0) {
374
+ settle(void 0, result.access_token);
375
+ return;
376
+ }
377
+ switch (result.error) {
378
+ case "authorization_pending": break;
379
+ case "slow_down":
380
+ intervalMs += 5e3;
381
+ break;
382
+ case "access_denied":
383
+ settle(/* @__PURE__ */ new Error("login declined on the GitHub authorization page"));
384
+ return;
385
+ case "expired_token":
386
+ settle(/* @__PURE__ */ new Error("the device code expired before authorization completed"));
387
+ return;
388
+ default:
389
+ settle(/* @__PURE__ */ new Error(`${provider} device-flow polling failed: ${result.error_description ?? result.error ?? `HTTP ${String(pollResponse.status)}`}`));
390
+ return;
391
+ }
392
+ }
393
+ };
394
+ const attempt = {
395
+ verificationUrl: wire.verification_uri,
396
+ userCode: wire.user_code,
397
+ waitToken: () => tokenPromise,
398
+ cancel: () => {
399
+ controller.abort(/* @__PURE__ */ new Error("login cancelled"));
400
+ settle(/* @__PURE__ */ new Error("login cancelled"));
401
+ }
402
+ };
403
+ this.attempts.set(provider, attempt);
404
+ poll().catch((error) => {
405
+ settle(error instanceof Error ? error : new Error(String(error)));
406
+ });
407
+ return attempt;
408
+ }
409
+ };
410
+
253
411
  //#endregion
254
412
  //#region src/auth/claude-code-creds.ts
255
413
  const PRIMARY_SERVICE = "Claude Code-credentials";
@@ -437,7 +595,8 @@ async function refreshClaudeSynced(session, doRefresh) {
437
595
  const PROVIDER_IDS = [
438
596
  "codex",
439
597
  "claude",
440
- "grok"
598
+ "grok",
599
+ "copilot"
441
600
  ];
442
601
  /**
443
602
  * Absolute path of the auth store file.
@@ -513,6 +672,34 @@ async function writeStore(store, path) {
513
672
  }
514
673
  }
515
674
  /**
675
+ * One write chain per store path. Every mutation is a read-modify-write of a
676
+ * single JSON file, and the plugin has several independent writers — a login,
677
+ * a logout, and one token refresh per provider adapter, each on its own
678
+ * schedule. Overlapping them unserialized costs whichever provider read the
679
+ * store first its entry.
680
+ *
681
+ * A chain is dropped once nothing is queued behind it, so the map holds an
682
+ * entry only while writes are in flight.
683
+ */
684
+ const writeChains = /* @__PURE__ */ new Map();
685
+ /**
686
+ * Run one read-modify-write of a store path after every write already queued
687
+ * for it. Callers join the chain synchronously, so call order is write order.
688
+ * @param path - the store file being mutated.
689
+ * @param action - the read-modify-write to run.
690
+ * @returns whatever `action` returns.
691
+ */
692
+ async function serialize(path, action) {
693
+ const next = (writeChains.get(path) ?? Promise.resolve()).then(action, action);
694
+ const tail = next.then(() => void 0, () => void 0);
695
+ writeChains.set(path, tail);
696
+ try {
697
+ return await next;
698
+ } finally {
699
+ if (writeChains.get(path) === tail) writeChains.delete(path);
700
+ }
701
+ }
702
+ /**
516
703
  * Read one provider's session.
517
704
  * @param provider - the provider route.
518
705
  * @param path - store file path; defaults to {@link authFilePath}.
@@ -528,9 +715,11 @@ async function getSession(provider, path = authFilePath()) {
528
715
  * @param path - store file path; defaults to {@link authFilePath}.
529
716
  */
530
717
  async function saveSession(provider, session, path = authFilePath()) {
531
- const store = await loadStore(path);
532
- store[provider] = session;
533
- await writeStore(store, path);
718
+ return serialize(path, async () => {
719
+ const store = await loadStore(path);
720
+ store[provider] = session;
721
+ await writeStore(store, path);
722
+ });
534
723
  }
535
724
  /**
536
725
  * Delete one provider's session (logout).
@@ -538,10 +727,12 @@ async function saveSession(provider, session, path = authFilePath()) {
538
727
  * @param path - store file path; defaults to {@link authFilePath}.
539
728
  */
540
729
  async function deleteSession(provider, path = authFilePath()) {
541
- const store = await loadStore(path);
542
- if (store[provider] === void 0) return;
543
- delete store[provider];
544
- await writeStore(store, path);
730
+ return serialize(path, async () => {
731
+ const store = await loadStore(path);
732
+ if (store[provider] === void 0) return;
733
+ delete store[provider];
734
+ await writeStore(store, path);
735
+ });
545
736
  }
546
737
 
547
738
  //#endregion
@@ -595,6 +786,12 @@ function readString(payload, field) {
595
786
  if (typeof value !== "string" || value.length === 0) throw new BadRequest(`payload.${field} must be a non-empty string`);
596
787
  return value;
597
788
  }
789
+ /** Validate the `setSpeed` endpoint's tier. */
790
+ function readSpeedTier(payload) {
791
+ const tier = payload.tier;
792
+ if (tier !== "standard" && tier !== "fast") throw new BadRequest("payload.tier must be \"standard\" or \"fast\"");
793
+ return tier;
794
+ }
598
795
  /** Validate the `image` endpoint's payload into a full attachment reference. */
599
796
  function readImageRef(payload) {
600
797
  if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
@@ -633,7 +830,12 @@ function readVideoName(payload) {
633
830
  if (typeof name$1 !== "string" || !VIDEO_NAME_PATTERN.test(name$1)) throw new BadRequest("payload.name must be a bare .mp4 file name");
634
831
  return name$1;
635
832
  }
636
- async function dispatch(controller, endpoint, payload, signal) {
833
+ /** Validate the session id both speed endpoints carry. */
834
+ function readSessionId(payload) {
835
+ if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
836
+ return readString(payload, "sessionId");
837
+ }
838
+ async function dispatch(controller, speed, endpoint, payload, signal) {
637
839
  switch (endpoint) {
638
840
  case "status": {
639
841
  const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
@@ -654,6 +856,10 @@ async function dispatch(controller, endpoint, payload, signal) {
654
856
  case "usage": return ok(await controller.usage(readProvider(payload), signal));
655
857
  case "image": return ok(await controller.readImage(readImageRef(payload), signal));
656
858
  case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
859
+ case "speed": return ok(await speed.speed(readSessionId(payload)));
860
+ case "setSpeed":
861
+ await speed.setSpeed(readSessionId(payload), readSpeedTier(payload));
862
+ return ok({ ok: true });
657
863
  default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
658
864
  }
659
865
  }
@@ -661,13 +867,14 @@ async function dispatch(controller, endpoint, payload, signal) {
661
867
  * Register the `/subscriptions-auth` RPC channel when a host connection exists.
662
868
  * @param ctx - the plugin context (headless profiles have no `connection`).
663
869
  * @param controller - the auth operations backing the endpoints.
870
+ * @param speed - the per-session speed-tier state backing the Speed toggle.
664
871
  */
665
- function registerAuthRpc(ctx, controller) {
872
+ function registerAuthRpc(ctx, controller, speed) {
666
873
  ctx.inject(["connection"], (ctx$1) => {
667
874
  const connection = ctx$1.get("connection");
668
875
  ctx$1.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
669
876
  try {
670
- return await dispatch(controller, endpoint, payload, signal);
877
+ return await dispatch(controller, speed, endpoint, payload, signal);
671
878
  } catch (error) {
672
879
  return failure(error);
673
880
  }
@@ -691,6 +898,7 @@ function validateModels(models, label) {
691
898
  if (model.contextWindow !== void 0 && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) throw new Error(`${label}: catalog model "${model.id}" contextWindow must be a positive integer`);
692
899
  if (model.maxTokens !== void 0 && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) throw new Error(`${label}: catalog model "${model.id}" maxTokens must be a positive integer`);
693
900
  if (model.inputModalities !== void 0 && (model.inputModalities.length === 0 || model.inputModalities.some((modality) => modality !== "text" && modality !== "image"))) throw new Error(`${label}: catalog model "${model.id}" inputModalities must be a non-empty list of "text"/"image"`);
901
+ if (model.wire !== void 0 && model.wire !== "chat-completions" && model.wire !== "responses") throw new Error(`${label}: catalog model "${model.id}" wire must be "chat-completions" or "responses"`);
694
902
  if (seen.has(model.id)) throw new Error(`${label}: duplicate catalog model "${model.id}"`);
695
903
  seen.add(model.id);
696
904
  return {
@@ -698,7 +906,8 @@ function validateModels(models, label) {
698
906
  ...model.name === void 0 ? {} : { name: model.name },
699
907
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
700
908
  ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
701
- ...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
909
+ ...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] },
910
+ ...model.wire === void 0 ? {} : { wire: model.wire }
702
911
  };
703
912
  });
704
913
  }
@@ -885,7 +1094,7 @@ const DISCOVERY_TTL_MS = 5 * 6e4;
885
1094
  * while a stale entry refreshes in the background, and only awaits the fetch
886
1095
  * when nothing is known yet. An optional {@link CatalogPersistence} seeds the
887
1096
  * last-known state across restarts and receives every successful fetch. A 401
888
- * during a fetch must call {@link invalidate}.
1097
+ * that still fails after a forced token refresh must call {@link invalidate}.
889
1098
  */
890
1099
  var ModelCatalogCache = class {
891
1100
  entry;
@@ -906,6 +1115,14 @@ var ModelCatalogCache = class {
906
1115
  if (this.entry === void 0 || Date.now() - this.entry.at >= this.ttlMs) return void 0;
907
1116
  return this.entry.models;
908
1117
  }
1118
+ /**
1119
+ * The last successfully fetched catalog, ignoring TTL. Used to carry
1120
+ * capability metadata forward when a later fetch cannot re-enrich.
1121
+ * @returns the last-known models, or `undefined` when nothing has been stored.
1122
+ */
1123
+ lastKnown() {
1124
+ return this.entry?.models;
1125
+ }
909
1126
  /** Load the persisted snapshot once; a fetch or invalidate that landed first wins. */
910
1127
  ensureSeeded() {
911
1128
  if (this.persistence === void 0) return Promise.resolve();
@@ -970,6 +1187,34 @@ var ModelCatalogCache = class {
970
1187
  this.persistence?.clear().catch(() => void 0);
971
1188
  }
972
1189
  };
1190
+ /** Whether discovery failed because the stored login is gone. */
1191
+ function isMissingOrInvalidCredential(error) {
1192
+ return error instanceof LlmError && (error.code === "MISSING_CREDENTIAL" || error.code === "INVALID_CREDENTIAL");
1193
+ }
1194
+ /** Whether discovery failed because the access token was rejected. */
1195
+ function isDiscoveryAuthFailure(error) {
1196
+ return error instanceof OAuthEndpointError && error.status === 401 || error instanceof LlmError && error.code === "AUTH";
1197
+ }
1198
+ /**
1199
+ * Run a catalog fetch, retrying once after a forced token refresh when the
1200
+ * first attempt is a 401/AUTH. Only {@link ModelCatalogCache.invalidate}s
1201
+ * when the retry is also an auth failure, so a refresh race cannot erase
1202
+ * last-known capability metadata.
1203
+ */
1204
+ async function discoverOrRetryAuth(session, catalog, run) {
1205
+ try {
1206
+ return await run();
1207
+ } catch (error) {
1208
+ if (isMissingOrInvalidCredential(error) || !isDiscoveryAuthFailure(error)) throw error;
1209
+ try {
1210
+ await session(true);
1211
+ return await run();
1212
+ } catch (retryError) {
1213
+ if (!isMissingOrInvalidCredential(retryError) && isDiscoveryAuthFailure(retryError)) catalog.invalidate();
1214
+ throw retryError;
1215
+ }
1216
+ }
1217
+ }
973
1218
 
974
1219
  //#endregion
975
1220
  //#region src/providers/catalog-store.ts
@@ -1013,6 +1258,14 @@ function sanitizeModel(value) {
1013
1258
  if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
1014
1259
  const thinkingType = raw.thinkingType;
1015
1260
  if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
1261
+ const fastTier = raw.fastTier;
1262
+ if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
1263
+ const copilotWire = raw.copilotWire;
1264
+ if (copilotWire !== void 0 && copilotWire !== "chat-completions" && copilotWire !== "responses") return;
1265
+ const copilotResponses = raw.copilotResponses;
1266
+ if (copilotResponses !== void 0 && typeof copilotResponses !== "boolean") return void 0;
1267
+ const inputModalities = raw.inputModalities;
1268
+ if (inputModalities !== void 0 && (!Array.isArray(inputModalities) || inputModalities.length === 0 || inputModalities.some((modality) => modality !== "text" && modality !== "image"))) return void 0;
1016
1269
  return {
1017
1270
  id: raw.id,
1018
1271
  name: raw.name,
@@ -1020,7 +1273,11 @@ function sanitizeModel(value) {
1020
1273
  ...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
1021
1274
  ...raw.priority === void 0 ? {} : { priority: raw.priority },
1022
1275
  ...reasoning === void 0 ? {} : { reasoning },
1023
- ...thinkingType === void 0 ? {} : { thinkingType }
1276
+ ...thinkingType === void 0 ? {} : { thinkingType },
1277
+ ...fastTier === void 0 ? {} : { fastTier },
1278
+ ...copilotWire === void 0 ? {} : { copilotWire },
1279
+ ...copilotResponses === void 0 ? {} : { copilotResponses },
1280
+ ...inputModalities === void 0 ? {} : { inputModalities: [...inputModalities] }
1024
1281
  };
1025
1282
  }
1026
1283
  /**
@@ -1202,22 +1459,31 @@ async function* parseSse(stream, onActivity) {
1202
1459
  //#endregion
1203
1460
  //#region src/translate/responses.ts
1204
1461
  /** Flatten a tool result's content to plain text for `function_call_output`. */
1205
- function toolResultText$1(block) {
1462
+ function toolResultText$2(block) {
1206
1463
  return block.content.map((part) => part.type === "text" ? part.text : "").join("");
1207
1464
  }
1208
1465
  /**
1209
1466
  * Convert harness messages into Responses `instructions` + `input` items.
1210
1467
  * System-role messages become `instructions`; an explicit `system` argument
1211
- * wins over them when both exist. Reasoning blocks are not replayed (v1).
1212
- * Images must arrive pre-resolved ({@link TranslatableMessage}); an unresolved
1213
- * ImageBlock is skipped because its bytes are unreachable here.
1468
+ * wins over them when both exist. Reasoning blocks are never replayed in
1469
+ * their text form: a Responses model continuing past a tool call needs its
1470
+ * reasoning back as the provider's completed reasoning items (id, summary,
1471
+ * and the ENCRYPTED payload), so `reasoningFor` may resolve per-call
1472
+ * captured items, replayed ahead of the matching function_call item. Images
1473
+ * must arrive pre-resolved
1474
+ * ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
1475
+ * its bytes are unreachable here.
1214
1476
  * @param messages - ordered conversation messages with resolved images.
1215
1477
  * @param system - explicit system prompt, which takes precedence.
1478
+ * @param reasoningFor - resolves one tool call id to the COMPLETED reasoning
1479
+ * items captured for it (id, summary, status, encrypted payload), replayed
1480
+ * ahead of the matching function_call item, when the adapter kept them.
1216
1481
  * @returns request fields ready to merge into the request body.
1217
1482
  */
1218
- function toResponsesInput(messages, system) {
1483
+ function toResponsesInput(messages, system, reasoningFor) {
1219
1484
  const input = [];
1220
1485
  const systemTexts = [];
1486
+ let lastReplay;
1221
1487
  for (const message of messages) {
1222
1488
  if (message.role === "system") {
1223
1489
  for (const block of message.content) if (block.type === "text") systemTexts.push(block.text);
@@ -1241,8 +1507,19 @@ function toResponsesInput(messages, system) {
1241
1507
  text: block.text
1242
1508
  });
1243
1509
  break;
1244
- case "tool-call":
1510
+ case "tool-call": {
1245
1511
  flushMessage();
1512
+ const encrypted = reasoningFor?.(String(block.id));
1513
+ if (encrypted !== void 0 && encrypted !== lastReplay) {
1514
+ for (const item of encrypted) input.push({
1515
+ type: "reasoning",
1516
+ ...item.id === void 0 ? {} : { id: item.id },
1517
+ ...item.summary === void 0 ? {} : { summary: item.summary },
1518
+ ...item.status === void 0 ? {} : { status: item.status },
1519
+ encrypted_content: item.encrypted_content
1520
+ });
1521
+ lastReplay = encrypted;
1522
+ }
1246
1523
  input.push({
1247
1524
  type: "function_call",
1248
1525
  call_id: String(block.id),
@@ -1250,12 +1527,13 @@ function toResponsesInput(messages, system) {
1250
1527
  arguments: block.arguments
1251
1528
  });
1252
1529
  break;
1530
+ }
1253
1531
  case "tool-result":
1254
1532
  flushMessage();
1255
1533
  input.push({
1256
1534
  type: "function_call_output",
1257
1535
  call_id: String(block.toolCallId),
1258
- output: toolResultText$1(block)
1536
+ output: toolResultText$2(block)
1259
1537
  });
1260
1538
  break;
1261
1539
  case "image":
@@ -1317,7 +1595,7 @@ function responsesFailure(code, message) {
1317
1595
  return new LlmError(text, "SERVER");
1318
1596
  }
1319
1597
  /** Assemble the final ContentBlock for one open block. */
1320
- function closeBlock$1(block) {
1598
+ function closeBlock$2(block) {
1321
1599
  switch (block.kind) {
1322
1600
  case "text": return {
1323
1601
  type: "text",
@@ -1379,7 +1657,7 @@ var ResponsesStreamTranslator = class {
1379
1657
  chunks.push({
1380
1658
  type: "block-end",
1381
1659
  index: block.index,
1382
- block: closeBlock$1(block)
1660
+ block: closeBlock$2(block)
1383
1661
  });
1384
1662
  }
1385
1663
  /** Close every still-open block for one output item (prefix match on the key). */
@@ -1396,7 +1674,7 @@ var ResponsesStreamTranslator = class {
1396
1674
  chunks.push({
1397
1675
  type: "block-end",
1398
1676
  index: block.index,
1399
- block: closeBlock$1(block)
1677
+ block: closeBlock$2(block)
1400
1678
  });
1401
1679
  return;
1402
1680
  }
@@ -1521,9 +1799,12 @@ var ResponsesStreamTranslator = class {
1521
1799
  * Consume a Responses SSE byte stream and yield harness StreamChunks.
1522
1800
  * @param stream - raw response body.
1523
1801
  * @param onActivity - transport-activity callback for the idle watchdog.
1802
+ * @param transform - optional per-event rewrite applied before translation
1803
+ * (Copilot's gateway mints a fresh item id per event; the adapter rewrites
1804
+ * them into stable per-item keys).
1524
1805
  * @returns the chunk stream; throws when the stream ends before `response.completed`.
1525
1806
  */
1526
- async function* streamResponses(stream, onActivity) {
1807
+ async function* streamResponses(stream, onActivity, transform) {
1527
1808
  const translator = new ResponsesStreamTranslator();
1528
1809
  for await (const sseEvent of parseSse(stream, onActivity)) {
1529
1810
  let event;
@@ -1532,6 +1813,7 @@ async function* streamResponses(stream, onActivity) {
1532
1813
  } catch {
1533
1814
  throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, "MALFORMED_RESPONSE");
1534
1815
  }
1816
+ if (transform !== void 0) event = transform(event);
1535
1817
  yield* translator.push(event);
1536
1818
  if (translator.terminated) return;
1537
1819
  }
@@ -1584,6 +1866,14 @@ const CODEX_EFFORTS = [
1584
1866
  const CODEX_DEFAULT_EFFORT = ReasoningEffortId("high");
1585
1867
  /** Every gpt-5.x codex model accepts image input. */
1586
1868
  const CODEX_MODALITIES = ["text", "image"];
1869
+ /**
1870
+ * Fast tier (the codex CLI's "fast mode"): the Responses `service_tier` wire
1871
+ * value for priority processing, mirroring codex-rs
1872
+ * `ServiceTier::Fast.request_value()`. The legacy catalog spelling is the
1873
+ * `additional_speed_tiers` entry "fast".
1874
+ */
1875
+ const CODEX_FAST_SERVICE_TIER = "priority";
1876
+ const CODEX_FAST_SPEED_TIER = "fast";
1587
1877
  /** Static codex flow facts for the OAuth flow engine. */
1588
1878
  const codexFlow = {
1589
1879
  callbackPath: CODEX_CALLBACK_PATH,
@@ -1795,6 +2085,14 @@ function effortName(effort) {
1795
2085
  return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
1796
2086
  }
1797
2087
  /**
2088
+ * Whether a catalog entry advertises the fast tier. Mirrors codex-rs
2089
+ * `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
2090
+ * wire value, or the legacy `additional_speed_tiers` "fast" entry.
2091
+ */
2092
+ function supportsFastTier(entry) {
2093
+ return (entry.service_tiers ?? []).some((tier) => tier.id === CODEX_FAST_SERVICE_TIER) || (entry.additional_speed_tiers ?? []).includes(CODEX_FAST_SPEED_TIER);
2094
+ }
2095
+ /**
1798
2096
  * Fetch the live codex model catalog with the session's auth headers.
1799
2097
  * @param session - the stored session (used as-is; never refreshed here).
1800
2098
  * @param fetchFn - fetch implementation (injectable for tests).
@@ -1821,7 +2119,7 @@ async function fetchCodexModels(session, fetchFn = fetch) {
1821
2119
  ...level.description === void 0 ? {} : { description: level.description }
1822
2120
  }));
1823
2121
  const defaultEffort = typeof entry.default_reasoning_level === "string" && entry.default_reasoning_level.length > 0 && efforts.some((effort) => effort.id === ReasoningEffortId(entry.default_reasoning_level)) ? ReasoningEffortId(entry.default_reasoning_level) : void 0;
1824
- discovered.push({
2122
+ const model = {
1825
2123
  id: entry.slug,
1826
2124
  name: typeof entry.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : entry.slug,
1827
2125
  ...typeof entry.description === "string" && entry.description.length > 0 ? { description: entry.description } : {},
@@ -1830,13 +2128,84 @@ async function fetchCodexModels(session, fetchFn = fetch) {
1830
2128
  ...efforts.length > 0 ? { reasoning: {
1831
2129
  efforts,
1832
2130
  ...defaultEffort === void 0 ? {} : { defaultEffort }
1833
- } } : {}
1834
- });
2131
+ } } : {},
2132
+ ...supportsFastTier(entry) ? { fastTier: true } : {}
2133
+ };
2134
+ discovered.push(model);
1835
2135
  }
1836
2136
  discovered.sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) - (b.priority ?? Number.MAX_SAFE_INTEGER));
1837
2137
  if (discovered.length === 0) throw new Error(`codex models endpoint returned an empty catalog (client_version ${CODEX_CLIENT_VERSION})`);
1838
2138
  return discovered;
1839
2139
  }
2140
+ const CODEX_CALL_ID_MAX_LENGTH = 64;
2141
+ const CODEX_CALL_ID_PREFIX = "call_";
2142
+ /**
2143
+ * Bound tool-call ids at the Codex wire boundary without changing the shared
2144
+ * Responses translation used by Grok. Short ids stay verbatim. Oversized ids
2145
+ * become deterministic hashes, and every id already present in this request
2146
+ * is reserved first so a generated id cannot collide with a legitimate short
2147
+ * one (or another oversized id).
2148
+ */
2149
+ function normalizeCodexCallIds(input) {
2150
+ const mapping = /* @__PURE__ */ new Map();
2151
+ const used = /* @__PURE__ */ new Set();
2152
+ const callId = (item) => (item.type === "function_call" || item.type === "function_call_output") && typeof item.call_id === "string" ? item.call_id : void 0;
2153
+ for (const item of input) {
2154
+ const id = callId(item);
2155
+ if (id !== void 0 && id.length <= CODEX_CALL_ID_MAX_LENGTH) {
2156
+ mapping.set(id, id);
2157
+ used.add(id);
2158
+ }
2159
+ }
2160
+ for (const item of input) {
2161
+ const id = callId(item);
2162
+ if (id === void 0 || mapping.has(id)) continue;
2163
+ let attempt = 0;
2164
+ let normalized;
2165
+ do {
2166
+ const hash = createHash("sha256");
2167
+ if (attempt > 0) hash.update(String(attempt)).update("\0");
2168
+ normalized = `${CODEX_CALL_ID_PREFIX}${hash.update(id).digest("hex").slice(0, CODEX_CALL_ID_MAX_LENGTH - 5)}`;
2169
+ attempt += 1;
2170
+ } while (used.has(normalized));
2171
+ mapping.set(id, normalized);
2172
+ used.add(normalized);
2173
+ }
2174
+ return input.map((item) => {
2175
+ const id = callId(item);
2176
+ if (id === void 0) return item;
2177
+ const normalized = mapping.get(id) ?? id;
2178
+ return normalized === id ? item : {
2179
+ ...item,
2180
+ call_id: normalized
2181
+ };
2182
+ });
2183
+ }
2184
+ /**
2185
+ * The Responses request body for one generation. A fast-tier request (the
2186
+ * composer Speed toggle, the codex CLI's fast mode) carries
2187
+ * `service_tier: priority`; the tier field is omitted entirely otherwise,
2188
+ * matching the CLI (it never sends an explicit standard tier).
2189
+ */
2190
+ function codexRequestBody(options, resolved, fast) {
2191
+ return {
2192
+ model: options.model,
2193
+ instructions: resolved.instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
2194
+ input: normalizeCodexCallIds(resolved.input),
2195
+ ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toResponsesTools(options.tools) } : {},
2196
+ tool_choice: "auto",
2197
+ parallel_tool_calls: true,
2198
+ ...options.reasoningEffort !== void 0 ? { reasoning: {
2199
+ effort: String(options.reasoningEffort),
2200
+ summary: "auto"
2201
+ } } : {},
2202
+ store: false,
2203
+ stream: true,
2204
+ include: ["reasoning.encrypted_content"],
2205
+ ...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {},
2206
+ ...fast ? { service_tier: CODEX_FAST_SERVICE_TIER } : {}
2207
+ };
2208
+ }
1840
2209
  /** Codex wire adapter: one instance serves the `codex` provider route. */
1841
2210
  var CodexAdapter = class extends LlmAdapter {
1842
2211
  catalog;
@@ -1867,7 +2236,7 @@ var CodexAdapter = class extends LlmAdapter {
1867
2236
  if (await this.options.tokens.peek() === void 0) return [];
1868
2237
  if (!this.options.discovery) return this.staticModels(provider);
1869
2238
  try {
1870
- return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
2239
+ return (await discoverOrRetryAuth((force) => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog()))).map((model) => ({
1871
2240
  provider,
1872
2241
  id: model.id,
1873
2242
  name: model.name,
@@ -1875,8 +2244,7 @@ var CodexAdapter = class extends LlmAdapter {
1875
2244
  inputModalities: CODEX_MODALITIES
1876
2245
  }));
1877
2246
  } catch (error) {
1878
- if (error instanceof LlmError && (error.code === "MISSING_CREDENTIAL" || error.code === "INVALID_CREDENTIAL")) return [];
1879
- if (error instanceof OAuthEndpointError && error.status === 401) this.catalog.invalidate();
2247
+ if (isMissingOrInvalidCredential(error)) return [];
1880
2248
  this.options.onWarn?.(`codex model discovery failed; using the built-in catalog (${errorChain(error)})`);
1881
2249
  return this.staticModels(provider);
1882
2250
  }
@@ -1892,6 +2260,16 @@ var CodexAdapter = class extends LlmAdapter {
1892
2260
  if (!this.options.discovery) return void 0;
1893
2261
  return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
1894
2262
  }
2263
+ /** Whether the discovered catalog advertises a fast tier for this model. */
2264
+ async supportsFastTier(model) {
2265
+ return (await this.discovered(model))?.fastTier === true;
2266
+ }
2267
+ /** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
2268
+ async fastCapableModels() {
2269
+ if (!this.options.discovery) return [];
2270
+ if (await this.options.tokens.peek() === void 0) return [];
2271
+ return (await this.catalog.resolve(() => this.fetchCatalog()) ?? []).filter((model) => model.fastTier === true).map((model) => model.id);
2272
+ }
1895
2273
  async resolveModel(provider, model) {
1896
2274
  const discovered = await this.discovered(model);
1897
2275
  const configured = this.options.models.find((entry) => entry.id === model);
@@ -1930,23 +2308,9 @@ var CodexAdapter = class extends LlmAdapter {
1930
2308
  }
1931
2309
  }
1932
2310
  async request(options, session, signal) {
1933
- const { instructions, input } = toResponsesInput(await resolveImages(options.messages, this.options.resolveAttachments?.(), signal), options.system);
1934
- const body = {
1935
- model: options.model,
1936
- instructions: instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
1937
- input,
1938
- ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toResponsesTools(options.tools) } : {},
1939
- tool_choice: "auto",
1940
- parallel_tool_calls: true,
1941
- ...options.reasoningEffort !== void 0 ? { reasoning: {
1942
- effort: String(options.reasoningEffort),
1943
- summary: "auto"
1944
- } } : {},
1945
- store: false,
1946
- stream: true,
1947
- include: ["reasoning.encrypted_content"],
1948
- ...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {}
1949
- };
2311
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
2312
+ const fast = this.options.speedFor !== void 0 && await this.options.speedFor(options.sessionId, options.model);
2313
+ const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
1950
2314
  return fetch(CODEX_API_URL, {
1951
2315
  method: "POST",
1952
2316
  headers: {
@@ -1972,8 +2336,27 @@ var CodexAdapter = class extends LlmAdapter {
1972
2336
  * system entry on every request.
1973
2337
  */
1974
2338
  const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
2339
+ /** Tags wrapping a mid-conversation system message where it sits in the history. */
2340
+ const SYSTEM_REMINDER_OPEN = "<system-reminder>";
2341
+ const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
2342
+ /**
2343
+ * How far apart consecutive message breakpoints sit, in content blocks.
2344
+ *
2345
+ * A breakpoint looks back at most 20 blocks for an entry an earlier request
2346
+ * wrote, so marks must stay closer than that: one agentic turn can append a
2347
+ * dozen tool_use/tool_result blocks at once, and a single trailing mark would
2348
+ * silently fall out of range and rebuild the whole prefix.
2349
+ */
2350
+ const CACHE_BLOCK_STRIDE = 15;
2351
+ /**
2352
+ * Message breakpoints per request. Anthropic allows four in total and the
2353
+ * last `system` block takes the fourth, so three are left for the history —
2354
+ * enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
2355
+ * blocks before a read is lost.
2356
+ */
2357
+ const MESSAGE_CACHE_BREAKPOINTS = 3;
1975
2358
  /** Flatten a tool result's content to plain text for `tool_result`. */
1976
- function toolResultText(block) {
2359
+ function toolResultText$1(block) {
1977
2360
  return block.content.map((part) => part.type === "text" ? part.text : "").join("");
1978
2361
  }
1979
2362
  /** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
@@ -1987,11 +2370,49 @@ function parseToolInput(raw) {
1987
2370
  }
1988
2371
  }
1989
2372
  /**
2373
+ * Move a user message's `tool_result` blocks into one contiguous run at the
2374
+ * front, preserving the relative order of both groups.
2375
+ *
2376
+ * Anthropic answers every `tool_use` against the blocks that *lead* the next
2377
+ * message, so a block of any other kind before or between the results reads
2378
+ * as a call left unanswered and the request is rejected. The harness merges
2379
+ * everything queued for one user turn into a single message, and a parallel
2380
+ * tool batch arrives as one result message per call, so any context spliced
2381
+ * mid-batch lands between two results. Restoring the run here keeps that
2382
+ * independent of delivery order. Order *among* the results does not matter.
2383
+ * @param message - one assembled user message, reordered in place.
2384
+ */
2385
+ function leadWithToolResults(message) {
2386
+ const firstOther = message.content.findIndex((block) => block.type !== "tool_result");
2387
+ if (firstOther === -1) return;
2388
+ if (!message.content.slice(firstOther).some((block) => block.type === "tool_result")) return;
2389
+ message.content = [...message.content.filter((block) => block.type === "tool_result"), ...message.content.filter((block) => block.type !== "tool_result")];
2390
+ }
2391
+ /**
2392
+ * Index of the first non-system message; `messages.length` when every message
2393
+ * is a system one.
2394
+ *
2395
+ * A system message before the conversation starts is the operator's opening
2396
+ * instruction and belongs in the `system` slot. One that arrives later is
2397
+ * mid-conversation context, and hoisting it into `system` would move bytes in
2398
+ * front of the whole history — invalidating every cached turn behind it — so
2399
+ * it stays where it is, as a reminder block in `messages`.
2400
+ * @param messages - ordered conversation messages.
2401
+ * @returns the boundary index separating the two.
2402
+ */
2403
+ function conversationStart(messages) {
2404
+ const index = messages.findIndex((message) => message.role !== "system");
2405
+ return index === -1 ? messages.length : index;
2406
+ }
2407
+ /**
1990
2408
  * Convert harness messages into Anthropic messages. Consecutive same-role
1991
2409
  * messages merge into one message with multiple content blocks; tool results
1992
- * arrive as user messages with `tool_result` blocks; system-role messages are
1993
- * handled by {@link toAnthropicSystem} and skipped here. Reasoning blocks are
1994
- * not replayed (v1). Images must arrive pre-resolved
2410
+ * arrive as user messages with `tool_result` blocks, which a merged user
2411
+ * message keeps in one leading run ({@link leadWithToolResults}); system-role
2412
+ * messages before the conversation starts are handled by
2413
+ * {@link toAnthropicSystem} and skipped here, while a later one rides in
2414
+ * place as a user-role `<system-reminder>` block.
2415
+ * Reasoning blocks are not replayed (v1). Images must arrive pre-resolved
1995
2416
  * ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
1996
2417
  * its bytes are unreachable here.
1997
2418
  * @param messages - ordered conversation messages with resolved images.
@@ -1999,30 +2420,34 @@ function parseToolInput(raw) {
1999
2420
  */
2000
2421
  function toAnthropicMessages(messages) {
2001
2422
  const out = [];
2002
- for (const message of messages) {
2003
- if (message.role === "system") continue;
2004
- const role = message.role;
2423
+ const start = conversationStart(messages);
2424
+ for (const [index, message] of messages.entries()) {
2425
+ if (message.role === "system" && index < start) continue;
2426
+ const role = message.role === "system" ? "user" : message.role;
2005
2427
  const blocks = [];
2006
2428
  for (const block of message.content) switch (block.type) {
2007
2429
  case "text":
2008
2430
  blocks.push({
2009
2431
  type: "text",
2010
- text: block.text
2432
+ text: message.role === "system" ? `${SYSTEM_REMINDER_OPEN}${block.text}${SYSTEM_REMINDER_CLOSE}` : block.text
2011
2433
  });
2012
2434
  break;
2013
2435
  case "tool-call":
2014
- blocks.push({
2436
+ blocks.push(role === "assistant" ? {
2015
2437
  type: "tool_use",
2016
2438
  id: String(block.id),
2017
2439
  name: block.name,
2018
2440
  input: parseToolInput(block.arguments)
2441
+ } : {
2442
+ type: "text",
2443
+ text: `[tool call ${block.name}: ${block.arguments}]`
2019
2444
  });
2020
2445
  break;
2021
2446
  case "tool-result":
2022
2447
  blocks.push({
2023
2448
  type: "tool_result",
2024
2449
  tool_use_id: String(block.toolCallId),
2025
- content: toolResultText(block),
2450
+ content: toolResultText$1(block),
2026
2451
  ...block.isError === true ? { is_error: true } : {}
2027
2452
  });
2028
2453
  break;
@@ -2046,13 +2471,34 @@ function toAnthropicMessages(messages) {
2046
2471
  content: blocks
2047
2472
  });
2048
2473
  }
2474
+ for (const message of out) if (message.role === "user") leadWithToolResults(message);
2049
2475
  return out;
2050
2476
  }
2051
2477
  /**
2478
+ * Mark the conversation's cache breakpoints in place: the last content block,
2479
+ * then one every {@link CACHE_BLOCK_STRIDE} blocks backwards, {@link
2480
+ * MESSAGE_CACHE_BREAKPOINTS} in total.
2481
+ *
2482
+ * The history is append-only, so the block one request marks last is
2483
+ * byte-identical in the next — that entry is what the next request reads.
2484
+ * Marks are counted across the flattened block sequence, not per message,
2485
+ * because the lookback window Anthropic walks counts blocks the same way.
2486
+ * @param messages - assembled Anthropic messages, marked in place.
2487
+ */
2488
+ function markMessageCache(messages) {
2489
+ const blocks = messages.flatMap((message) => message.content);
2490
+ for (let mark = 0; mark < MESSAGE_CACHE_BREAKPOINTS; mark++) {
2491
+ const at = blocks.length - 1 - mark * CACHE_BLOCK_STRIDE;
2492
+ if (at < 0) return;
2493
+ blocks[at].cache_control = { type: "ephemeral" };
2494
+ }
2495
+ }
2496
+ /**
2052
2497
  * Build the Anthropic `system` array: the mandatory Claude Code identity
2053
2498
  * block, then the explicit system prompt, then any system-role messages.
2054
2499
  * @param system - explicit system prompt, when set.
2055
- * @param messages - conversation messages; their system-role text is appended.
2500
+ * @param messages - conversation messages; the system-role text preceding the
2501
+ * conversation is appended, and a later one is left to {@link toAnthropicMessages}.
2056
2502
  * @returns the system content blocks.
2057
2503
  */
2058
2504
  function toAnthropicSystem(system, messages) {
@@ -2064,29 +2510,34 @@ function toAnthropicSystem(system, messages) {
2064
2510
  type: "text",
2065
2511
  text: system
2066
2512
  });
2067
- for (const message of messages ?? []) {
2068
- if (message.role !== "system") continue;
2069
- for (const block of message.content) if (block.type === "text") blocks.push({
2070
- type: "text",
2071
- text: block.text
2072
- });
2073
- }
2513
+ const history = messages ?? [];
2514
+ for (const message of history.slice(0, conversationStart(history))) for (const block of message.content) if (block.type === "text") blocks.push({
2515
+ type: "text",
2516
+ text: block.text
2517
+ });
2518
+ blocks[blocks.length - 1].cache_control = { type: "ephemeral" };
2074
2519
  return blocks;
2075
2520
  }
2076
2521
  /**
2077
- * Map harness tool schemas to Anthropic tools.
2522
+ * Map harness tool schemas to Anthropic tools, in name order.
2523
+ *
2524
+ * `tools` renders at position 0 of the cached prefix, so any reordering
2525
+ * invalidates every cache entry behind it — `system` and the whole
2526
+ * conversation included. Registration order belongs to the caller and plugin
2527
+ * load order can differ between processes, so the wire order is fixed here
2528
+ * instead. Anthropic selects a tool by name; the array order carries nothing.
2078
2529
  * @param tools - tool schemas from the request.
2079
- * @returns Anthropic `tools` array entries.
2530
+ * @returns Anthropic `tools` array entries, ordered by tool name.
2080
2531
  */
2081
2532
  function toAnthropicTools(tools) {
2082
- return tools.map((tool) => ({
2533
+ return [...tools].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0).map((tool) => ({
2083
2534
  name: tool.name,
2084
2535
  description: tool.description,
2085
2536
  input_schema: tool.parameters
2086
2537
  }));
2087
2538
  }
2088
2539
  /** Assemble the final ContentBlock for one open block. */
2089
- function closeBlock(block) {
2540
+ function closeBlock$1(block) {
2090
2541
  switch (block.kind) {
2091
2542
  case "text": return {
2092
2543
  type: "text",
@@ -2255,7 +2706,7 @@ var AnthropicStreamTranslator = class {
2255
2706
  chunks.push({
2256
2707
  type: "block-end",
2257
2708
  index: block.index,
2258
- block: closeBlock(block)
2709
+ block: closeBlock$1(block)
2259
2710
  });
2260
2711
  return chunks;
2261
2712
  }
@@ -2282,7 +2733,7 @@ var AnthropicStreamTranslator = class {
2282
2733
  chunks.push({
2283
2734
  type: "block-end",
2284
2735
  index: block.index,
2285
- block: closeBlock(block)
2736
+ block: closeBlock$1(block)
2286
2737
  });
2287
2738
  }
2288
2739
  this.emitUsage(chunks);
@@ -2330,11 +2781,13 @@ async function* streamAnthropic(stream, onActivity) {
2330
2781
  //#endregion
2331
2782
  //#region src/providers/claude.ts
2332
2783
  const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
2784
+ const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
2333
2785
  const CLAUDE_TOKEN_URL = "https://claude.ai/v1/oauth/token";
2334
2786
  const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages?beta=true";
2335
2787
  const CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
2336
2788
  const CLAUDE_MODELS_URL = "https://api.anthropic.com/v1/models?beta=true";
2337
2789
  const CLAUDE_SCOPE = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
2790
+ const CLAUDE_CALLBACK_PATH = "/callback";
2338
2791
  const CLAUDE_CONTEXT_WINDOW = 2e5;
2339
2792
  const CLAUDE_DEFAULT_MAX_TOKENS = 32e3;
2340
2793
  /** Refresh when the access token has less than this much life left. */
@@ -2355,7 +2808,11 @@ function detectClaudeVersion() {
2355
2808
  } catch {}
2356
2809
  return CLAUDE_CLI_FALLBACK_VERSION;
2357
2810
  }
2358
- const CLAUDE_CLI_USER_AGENT = `claude-cli/${detectClaudeVersion()} (external, cli)`;
2811
+ let claudeCliUserAgent;
2812
+ function getClaudeCliUserAgent() {
2813
+ if (claudeCliUserAgent === void 0) claudeCliUserAgent = `claude-cli/${detectClaudeVersion()} (external, cli)`;
2814
+ return claudeCliUserAgent;
2815
+ }
2359
2816
  const CLAUDE_BETA_FALLBACK = [
2360
2817
  "claude-code-20250219",
2361
2818
  "oauth-2025-04-20",
@@ -2366,6 +2823,26 @@ const CLAUDE_BETA_FALLBACK = [
2366
2823
  "files-api-2025-04-14"
2367
2824
  ].join(",");
2368
2825
  const CLAUDE_BETA_FLAGS = CLAUDE_BETA_FALLBACK;
2826
+ /** Static claude flow facts for the OAuth flow engine. */
2827
+ const claudeFlow = {
2828
+ callbackPath: CLAUDE_CALLBACK_PATH,
2829
+ listen: {
2830
+ host: "localhost",
2831
+ ports: [0]
2832
+ },
2833
+ buildAuthorizeUrl({ redirectUri, state, pkce }) {
2834
+ return `${CLAUDE_AUTHORIZE_URL}?${new URLSearchParams({
2835
+ code: "true",
2836
+ client_id: CLAUDE_CLIENT_ID,
2837
+ response_type: "code",
2838
+ redirect_uri: redirectUri,
2839
+ scope: CLAUDE_SCOPE,
2840
+ code_challenge: pkce.challenge,
2841
+ code_challenge_method: "S256",
2842
+ state
2843
+ }).toString()}`;
2844
+ }
2845
+ };
2369
2846
  /** Best-effort account profile; login must not fail when this does. */
2370
2847
  async function fetchClaudeProfile(accessToken) {
2371
2848
  try {
@@ -2508,7 +2985,7 @@ async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
2508
2985
  headers: {
2509
2986
  "authorization": `Bearer ${session.accessToken}`,
2510
2987
  "anthropic-beta": "oauth-2025-04-20",
2511
- "user-agent": CLAUDE_CLI_USER_AGENT,
2988
+ "user-agent": getClaudeCliUserAgent(),
2512
2989
  "accept": "application/json"
2513
2990
  },
2514
2991
  ...signal === void 0 ? {} : { signal }
@@ -2560,7 +3037,7 @@ async function fetchClaudeModels(session, fetchFn = fetch) {
2560
3037
  const response = await fetchFn(CLAUDE_MODELS_URL, { headers: {
2561
3038
  "authorization": `Bearer ${session.accessToken}`,
2562
3039
  "anthropic-version": "2023-06-01",
2563
- "user-agent": CLAUDE_CLI_USER_AGENT,
3040
+ "user-agent": getClaudeCliUserAgent(),
2564
3041
  "anthropic-dangerous-direct-browser-access": "true",
2565
3042
  "accept": "application/json"
2566
3043
  } });
@@ -2590,6 +3067,36 @@ const CLAUDE_RETRY_MAX_DELAY_MS = 6e4;
2590
3067
  const CLAUDE_RETRY_JITTER_RATIO = .2;
2591
3068
  /** The Claude 4.5 family accepts image input. */
2592
3069
  const CLAUDE_MODALITIES = ["text", "image"];
3070
+ /**
3071
+ * Assemble the Anthropic request body.
3072
+ *
3073
+ * Extracted from the adapter so the wire shape — cache breakpoints above all —
3074
+ * is testable without a network round trip. The message array is marked before
3075
+ * it is placed so the breakpoints land on the blocks the body ships: one on the
3076
+ * last `system` block (covering `tools` + `system`, which render ahead of it)
3077
+ * and up to three across the history, Anthropic's four-slot maximum.
3078
+ * @param options - the generate request.
3079
+ * @param messages - conversation messages with images already resolved.
3080
+ * @param maxTokens - the resolved output cap.
3081
+ * @param thinking - the thinking parameter, when the model takes one.
3082
+ * @param effort - the reasoning effort, when the model advertises efforts.
3083
+ * @returns the JSON body to POST.
3084
+ */
3085
+ function claudeRequestBody(options, messages, maxTokens, thinking, effort) {
3086
+ const anthropicMessages = toAnthropicMessages(messages);
3087
+ markMessageCache(anthropicMessages);
3088
+ return {
3089
+ model: options.model,
3090
+ max_tokens: maxTokens,
3091
+ system: toAnthropicSystem(options.system, messages),
3092
+ messages: anthropicMessages,
3093
+ ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toAnthropicTools(options.tools) } : {},
3094
+ ...thinking === void 0 ? {} : { thinking },
3095
+ ...effort === void 0 ? {} : { output_config: { effort } },
3096
+ stream: true,
3097
+ ...options.sessionId !== void 0 ? { metadata: { user_id: String(options.sessionId) } } : {}
3098
+ };
3099
+ }
2593
3100
  /** Claude wire adapter: one instance serves the `claude` provider route. */
2594
3101
  var ClaudeAdapter = class extends LlmAdapter {
2595
3102
  catalog;
@@ -2635,15 +3142,14 @@ var ClaudeAdapter = class extends LlmAdapter {
2635
3142
  if (await this.options.tokens.peek() === void 0) return [];
2636
3143
  if (!this.options.discovery) return this.staticModels(provider);
2637
3144
  try {
2638
- return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
3145
+ return (await discoverOrRetryAuth((force) => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog()))).map((model) => ({
2639
3146
  provider,
2640
3147
  id: model.id,
2641
3148
  name: model.name,
2642
3149
  inputModalities: CLAUDE_MODALITIES
2643
3150
  }));
2644
3151
  } catch (error) {
2645
- if (error instanceof LlmError && (error.code === "MISSING_CREDENTIAL" || error.code === "INVALID_CREDENTIAL")) return [];
2646
- if (error instanceof LlmError && error.code === "AUTH") this.catalog.invalidate();
3152
+ if (isMissingOrInvalidCredential(error)) return [];
2647
3153
  this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
2648
3154
  return this.staticModels(provider);
2649
3155
  }
@@ -2708,26 +3214,14 @@ var ClaudeAdapter = class extends LlmAdapter {
2708
3214
  const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
2709
3215
  const maxTokens = options.maxTokens ?? this.options.models.find((entry) => entry.id === options.model)?.maxTokens ?? CLAUDE_DEFAULT_MAX_TOKENS;
2710
3216
  const disc = await this.discovered(options.model);
2711
- const thinking = this.thinkingParam(disc?.thinkingType, maxTokens);
2712
- const effort = options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? { output_config: { effort: String(options.reasoningEffort) } } : {};
2713
- const body = {
2714
- model: options.model,
2715
- max_tokens: maxTokens,
2716
- system: toAnthropicSystem(options.system, messages),
2717
- messages: toAnthropicMessages(messages),
2718
- ...options.tools !== void 0 && options.tools.length > 0 ? { tools: toAnthropicTools(options.tools) } : {},
2719
- ...thinking === void 0 ? {} : { thinking },
2720
- ...effort,
2721
- stream: true,
2722
- ...options.sessionId !== void 0 ? { metadata: { user_id: String(options.sessionId) } } : {}
2723
- };
3217
+ const body = claudeRequestBody(options, messages, maxTokens, this.thinkingParam(disc?.thinkingType, maxTokens), options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? String(options.reasoningEffort) : void 0);
2724
3218
  return fetch(CLAUDE_API_URL, {
2725
3219
  method: "POST",
2726
3220
  headers: {
2727
3221
  "authorization": `Bearer ${session.accessToken}`,
2728
3222
  "anthropic-version": "2023-06-01",
2729
3223
  "anthropic-beta": CLAUDE_BETA_FLAGS,
2730
- "user-agent": CLAUDE_CLI_USER_AGENT,
3224
+ "user-agent": getClaudeCliUserAgent(),
2731
3225
  "x-app": "cli",
2732
3226
  "anthropic-dangerous-direct-browser-access": "true",
2733
3227
  "accept": "text/event-stream",
@@ -3047,23 +3541,42 @@ function isChatModel(id) {
3047
3541
  return !/imagine|image-|video|embed/i.test(id);
3048
3542
  }
3049
3543
  /**
3544
+ * CLI-contributed fields carried forward from a previously discovered model.
3545
+ * @param prior - the last-known entry for this id, if any.
3546
+ * @returns enrichment to apply when the live CLI catalog cannot contribute.
3547
+ */
3548
+ function grokPriorMeta(prior) {
3549
+ if (prior === void 0) return {};
3550
+ return {
3551
+ ...prior.name.length > 0 ? { name: prior.name } : {},
3552
+ ...prior.description === void 0 ? {} : { description: prior.description },
3553
+ ...prior.contextWindow === void 0 ? {} : { contextWindow: prior.contextWindow },
3554
+ ...prior.reasoning === void 0 ? {} : { reasoning: prior.reasoning }
3555
+ };
3556
+ }
3557
+ /**
3050
3558
  * Fetch the live grok model list, enriched with the CLI catalog's per-model
3051
3559
  * metadata (display name, context window, reasoning efforts). The api.x.ai
3052
3560
  * list stays authoritative for which models exist; the CLI catalog is
3053
3561
  * enrichment only, so its failure degrades to a plain list instead of taking
3054
- * discovery down models it does not cover simply expose no efforts.
3562
+ * discovery down. When enrichment is missing, last-known capability metadata
3563
+ * is carried forward so a transient CLI outage cannot strip efforts a
3564
+ * session already selected.
3055
3565
  * @param session - the stored session (used as-is; never refreshed here).
3056
3566
  * @param fetchFn - fetch implementation (injectable for tests).
3057
3567
  * @param onWarn - warning sink for a failed CLI catalog fetch.
3568
+ * @param previous - last-known catalog used to keep enrichment when the CLI
3569
+ * catalog is down or omits a model.
3058
3570
  * @returns discovered chat models in endpoint order.
3059
3571
  */
3060
- async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
3572
+ async function fetchGrokModels(session, fetchFn = fetch, onWarn, previous) {
3573
+ const previousById = previous === void 0 || previous.length === 0 ? void 0 : new Map(previous.map((model) => [model.id, model]));
3061
3574
  const [response, cliCatalog] = await Promise.all([fetchFn(GROK_MODELS_URL, { headers: {
3062
3575
  "authorization": `Bearer ${session.accessToken}`,
3063
3576
  "accept": "application/json",
3064
3577
  ...attributionHeaders()
3065
3578
  } }), fetchGrokCliCatalog(session, fetchFn).catch((error) => {
3066
- onWarn?.(`grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`);
3579
+ onWarn?.(previousById === void 0 ? `grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})` : `grok CLI catalog fetch failed; keeping last-known reasoning efforts (${errorChain(error)})`);
3067
3580
  })]);
3068
3581
  if (!response.ok) throw await oauthEndpointError(response, "grok models");
3069
3582
  const payload = await response.json();
@@ -3074,10 +3587,11 @@ async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
3074
3587
  if (typeof entry.id !== "string" || entry.id.length === 0 || seen.has(entry.id)) continue;
3075
3588
  if (!isChatModel(entry.id)) continue;
3076
3589
  seen.add(entry.id);
3590
+ const cli = cliCatalog?.get(entry.id);
3077
3591
  discovered.push({
3078
3592
  id: entry.id,
3079
3593
  name: entry.id,
3080
- ...cliCatalog?.get(entry.id)
3594
+ ...cli ?? grokPriorMeta(previousById?.get(entry.id))
3081
3595
  });
3082
3596
  }
3083
3597
  if (discovered.length === 0) throw new Error("grok models endpoint returned an empty catalog");
@@ -3093,7 +3607,16 @@ var GrokAdapter = class extends LlmAdapter {
3093
3607
  }
3094
3608
  /** Discovery fetcher: resolves the session through the refresh-aware path. */
3095
3609
  async fetchCatalog() {
3096
- return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn);
3610
+ return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn, this.catalog.lastKnown());
3611
+ }
3612
+ listed(provider, discovered) {
3613
+ return discovered.map((model) => ({
3614
+ provider,
3615
+ id: model.id,
3616
+ name: model.name,
3617
+ ...model.description === void 0 ? {} : { description: model.description },
3618
+ inputModalities: grokModalities(model.id)
3619
+ }));
3097
3620
  }
3098
3621
  providerInfo(provider) {
3099
3622
  return {
@@ -3113,16 +3636,9 @@ var GrokAdapter = class extends LlmAdapter {
3113
3636
  if (await this.options.tokens.peek() === void 0) return [];
3114
3637
  if (!this.options.discovery) return this.staticModels(provider);
3115
3638
  try {
3116
- return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
3117
- provider,
3118
- id: model.id,
3119
- name: model.name,
3120
- ...model.description === void 0 ? {} : { description: model.description },
3121
- inputModalities: grokModalities(model.id)
3122
- }));
3639
+ return this.listed(provider, await discoverOrRetryAuth((force) => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog())));
3123
3640
  } catch (error) {
3124
- if (error instanceof LlmError && (error.code === "MISSING_CREDENTIAL" || error.code === "INVALID_CREDENTIAL")) return [];
3125
- if (error instanceof OAuthEndpointError && error.status === 401) this.catalog.invalidate();
3641
+ if (isMissingOrInvalidCredential(error)) return [];
3126
3642
  this.options.onWarn?.(`grok model discovery failed; using the built-in catalog (${errorChain(error)})`);
3127
3643
  return this.staticModels(provider);
3128
3644
  }
@@ -3202,56 +3718,1074 @@ var GrokAdapter = class extends LlmAdapter {
3202
3718
  };
3203
3719
 
3204
3720
  //#endregion
3205
- //#region src/tools/x-search.ts
3206
- /** Endpoint the search request is posted to. */
3207
- const X_SEARCH_URL = "https://api.x.ai/v1/responses";
3208
- /** Grok model the search runs on (a catalog model of the grok provider). */
3209
- const X_SEARCH_MODEL = "grok-4";
3210
- /** xAI caps each handle filter list at ten entries. */
3211
- const MAX_HANDLES = 10;
3212
- /**
3213
- * Validate and assemble the request facts from tool arguments. Throws plain
3214
- * Errors for argument problems the schema DSL cannot express (non-empty
3215
- * query, handle caps, mutually exclusive filters).
3216
- */
3217
- function buildXSearchRequest(args) {
3218
- const query = args.query.trim();
3219
- if (query.length === 0) throw new Error("x_search: query must be a non-empty string");
3220
- const allowed = normalizeHandles(args.allowed_x_handles, "allowed_x_handles");
3221
- const excluded = normalizeHandles(args.excluded_x_handles, "excluded_x_handles");
3222
- if (allowed.length > 0 && excluded.length > 0) throw new Error("x_search: allowed_x_handles and excluded_x_handles cannot be used together");
3223
- const tool = { type: "x_search" };
3224
- if (allowed.length > 0) tool.allowed_x_handles = allowed;
3225
- if (excluded.length > 0) tool.excluded_x_handles = excluded;
3226
- if (args.from_date !== void 0 && args.from_date.trim().length > 0) tool.from_date = args.from_date.trim();
3227
- if (args.to_date !== void 0 && args.to_date.trim().length > 0) tool.to_date = args.to_date.trim();
3228
- if (args.enable_image_understanding === true) tool.enable_image_understanding = true;
3229
- if (args.enable_video_understanding === true) tool.enable_video_understanding = true;
3230
- return {
3231
- query,
3232
- tool
3233
- };
3234
- }
3235
- /** Strip `@` prefixes, drop blanks, and enforce the provider's handle cap. */
3236
- function normalizeHandles(value, field) {
3237
- if (value === void 0) return [];
3238
- const handles = value.map((handle) => handle.trim().replace(/^@+/, "")).filter((handle) => handle.length > 0);
3239
- if (handles.length > MAX_HANDLES) throw new Error(`x_search: ${field} supports at most ${MAX_HANDLES} handles`);
3240
- return handles;
3241
- }
3242
- function isRecord$1(value) {
3243
- return typeof value === "object" && value !== null && !Array.isArray(value);
3721
+ //#region src/translate/chat-completions.ts
3722
+ /** Flatten a tool result's content to plain text for a `tool` message. */
3723
+ function toolResultText(block) {
3724
+ return block.content.map((part) => part.type === "text" ? part.text : "").join("");
3244
3725
  }
3245
3726
  /**
3246
- * Extract the answer text and citation URLs from a Responses payload: the
3247
- * `output_text` shortcut or message output parts for the answer, and both
3248
- * top-level `citations` and inline `url_citation` annotations for sources.
3727
+ * Convert harness messages into chat completions `messages`. System-role
3728
+ * messages become one leading `system` message; an explicit `system` argument
3729
+ * wins over them when both exist. Reasoning blocks are not replayed (matching
3730
+ * the Responses translator). Images must arrive pre-resolved; an unresolved
3731
+ * ImageBlock is skipped because its bytes are unreachable here. A user message
3732
+ * carrying only text collapses to a plain string body (some endpoints still
3733
+ * reject content-part arrays); tool results become separate `tool` messages.
3734
+ * @param messages - ordered conversation messages with resolved images.
3735
+ * @param system - explicit system prompt, which takes precedence.
3736
+ * @returns the wire `messages` array.
3249
3737
  */
3250
- function parseXSearchResponse(payload) {
3251
- const body = isRecord$1(payload) ? payload : {};
3252
- let answer = typeof body.output_text === "string" ? body.output_text.trim() : "";
3253
- const citations = [];
3254
- const push = (url) => {
3738
+ function toChatMessages(messages, system) {
3739
+ const out = [];
3740
+ const systemTexts = [];
3741
+ for (const message of messages) {
3742
+ if (message.role === "system") {
3743
+ for (const block of message.content) if (block.type === "text") systemTexts.push(block.text);
3744
+ continue;
3745
+ }
3746
+ if (message.role === "user") {
3747
+ let texts$1 = [];
3748
+ let parts = [];
3749
+ const flushUser = () => {
3750
+ if (parts.length > 0) {
3751
+ if (texts$1.length > 0) parts.unshift({
3752
+ type: "text",
3753
+ text: texts$1.join("\n")
3754
+ });
3755
+ out.push({
3756
+ role: "user",
3757
+ content: parts
3758
+ });
3759
+ } else if (texts$1.length > 0) out.push({
3760
+ role: "user",
3761
+ content: texts$1.join("\n")
3762
+ });
3763
+ texts$1 = [];
3764
+ parts = [];
3765
+ };
3766
+ for (const block of message.content) switch (block.type) {
3767
+ case "text":
3768
+ texts$1.push(block.text);
3769
+ break;
3770
+ case "image":
3771
+ if ("dataBase64" in block) parts.push({
3772
+ type: "image_url",
3773
+ image_url: { url: `data:${block.mediaType};base64,${block.dataBase64}` }
3774
+ });
3775
+ break;
3776
+ case "tool-result":
3777
+ flushUser();
3778
+ out.push({
3779
+ role: "tool",
3780
+ tool_call_id: String(block.toolCallId),
3781
+ content: toolResultText(block)
3782
+ });
3783
+ break;
3784
+ default: break;
3785
+ }
3786
+ flushUser();
3787
+ continue;
3788
+ }
3789
+ const texts = [];
3790
+ const toolCalls = [];
3791
+ for (const block of message.content) switch (block.type) {
3792
+ case "text":
3793
+ texts.push(block.text);
3794
+ break;
3795
+ case "tool-call":
3796
+ toolCalls.push({
3797
+ id: String(block.id),
3798
+ type: "function",
3799
+ function: {
3800
+ name: block.name,
3801
+ arguments: block.arguments
3802
+ }
3803
+ });
3804
+ break;
3805
+ default: break;
3806
+ }
3807
+ if (texts.length === 0 && toolCalls.length === 0) continue;
3808
+ out.push({
3809
+ role: "assistant",
3810
+ content: texts.join("\n"),
3811
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
3812
+ });
3813
+ }
3814
+ const systemText = system ?? (systemTexts.length > 0 ? systemTexts.join("\n\n") : void 0);
3815
+ if (systemText !== void 0) out.unshift({
3816
+ role: "system",
3817
+ content: systemText
3818
+ });
3819
+ return out;
3820
+ }
3821
+ /**
3822
+ * Map harness tool schemas to chat completions function tools.
3823
+ * @param tools - tool schemas from the request.
3824
+ * @returns the wire `tools` array.
3825
+ */
3826
+ function toChatTools(tools) {
3827
+ return tools.map((tool) => ({
3828
+ type: "function",
3829
+ function: {
3830
+ name: tool.name,
3831
+ description: tool.description,
3832
+ parameters: tool.parameters
3833
+ }
3834
+ }));
3835
+ }
3836
+ /**
3837
+ * Map chat completions usage to disjoint harness counts (cached input is
3838
+ * subtracted out of `inputTokens` and reported as `cacheReadTokens`).
3839
+ * @param usage - wire usage from the terminal chunk.
3840
+ * @returns harness token usage.
3841
+ */
3842
+ function mapChatCompletionsUsage(usage) {
3843
+ const cached = usage.prompt_tokens_details?.cached_tokens;
3844
+ const reasoning = usage.completion_tokens_details?.reasoning_tokens;
3845
+ return {
3846
+ inputTokens: usage.prompt_tokens - (cached ?? 0),
3847
+ outputTokens: usage.completion_tokens,
3848
+ ...cached !== void 0 ? { cacheReadTokens: cached } : {},
3849
+ ...reasoning !== void 0 ? { reasoningTokens: reasoning } : {}
3850
+ };
3851
+ }
3852
+ /** Assemble the final ContentBlock for one open block. */
3853
+ function closeBlock(block) {
3854
+ switch (block.kind) {
3855
+ case "text": return {
3856
+ type: "text",
3857
+ text: block.text
3858
+ };
3859
+ case "reasoning": return {
3860
+ type: "reasoning",
3861
+ text: block.text
3862
+ };
3863
+ case "tool-call": return {
3864
+ type: "tool-call",
3865
+ id: CallId(block.callId),
3866
+ name: block.name ?? "",
3867
+ arguments: block.text
3868
+ };
3869
+ }
3870
+ }
3871
+ /**
3872
+ * Push-model chat completions SSE translator: feed each parsed chunk object
3873
+ * to {@link push} and collect the emitted harness StreamChunks. The terminal
3874
+ * `finish_reason` chunk closes every block but only ARMS the finish chunk —
3875
+ * usage must precede the terminal finish, and where usage lives differs by
3876
+ * upstream: OpenAI-style streams send a trailing usage-only chunk
3877
+ * (stream_options.include_usage), while Copilot's Gemini models attach a
3878
+ * (zero) usage object to EVERY chunk and fold the real usage into the
3879
+ * finish chunk itself. A chunk therefore never early-returns on `usage`
3880
+ * alone: its deltas are always processed, and the terminal pair is drained
3881
+ * when the finish is armed and usage arrived (or when a usage-only chunk
3882
+ * follows an armed finish). `flush()` emits whatever remains when the
3883
+ * stream's `[DONE]` (or EOF) arrives.
3884
+ */
3885
+ var ChatCompletionsStreamTranslator = class {
3886
+ /** Text/reasoning blocks keyed by kind; tool calls keyed by their wire index. */
3887
+ blocks = /* @__PURE__ */ new Map();
3888
+ order = [];
3889
+ nextIndex = 0;
3890
+ sawToolCall = false;
3891
+ pendingUsage;
3892
+ armedFinish;
3893
+ /** Set once the terminal finish chunk was emitted. */
3894
+ terminated = false;
3895
+ open(key, kind, chunks, callId = "", name$1) {
3896
+ const block = {
3897
+ index: this.nextIndex++,
3898
+ kind,
3899
+ text: "",
3900
+ callId,
3901
+ ...name$1 === void 0 ? {} : { name: name$1 }
3902
+ };
3903
+ this.blocks.set(key, block);
3904
+ this.order.push(block);
3905
+ chunks.push({
3906
+ type: "block-start",
3907
+ index: block.index,
3908
+ blockType: kind
3909
+ });
3910
+ return block;
3911
+ }
3912
+ close(key, chunks) {
3913
+ const block = this.blocks.get(key);
3914
+ if (block === void 0) return;
3915
+ this.blocks.delete(key);
3916
+ chunks.push({
3917
+ type: "block-end",
3918
+ index: block.index,
3919
+ block: closeBlock(block)
3920
+ });
3921
+ }
3922
+ closeAll(chunks) {
3923
+ for (const key of [...this.blocks.keys()]) this.close(key, chunks);
3924
+ }
3925
+ /** Build the terminal finish chunk for one wire finish reason. */
3926
+ finishChunk(finishReason) {
3927
+ if (this.order.length === 0) return {
3928
+ type: "finish",
3929
+ reason: {
3930
+ kind: "error",
3931
+ failure: {
3932
+ message: "model returned a completed response with no content",
3933
+ code: EMPTY_RESPONSE_CODE
3934
+ }
3935
+ }
3936
+ };
3937
+ switch (finishReason) {
3938
+ case "tool_calls": return {
3939
+ type: "finish",
3940
+ reason: { kind: "tool-calls" }
3941
+ };
3942
+ case "length": return {
3943
+ type: "finish",
3944
+ reason: { kind: "max-tokens" }
3945
+ };
3946
+ case "content_filter": return {
3947
+ type: "finish",
3948
+ reason: {
3949
+ kind: "error",
3950
+ failure: {
3951
+ message: "the response was blocked by the provider content filter",
3952
+ code: "CONTENT_FILTER"
3953
+ }
3954
+ }
3955
+ };
3956
+ default: return {
3957
+ type: "finish",
3958
+ reason: { kind: this.sawToolCall ? "tool-calls" : "stop" }
3959
+ };
3960
+ }
3961
+ }
3962
+ /** Usage, then the armed finish: the only order the harness accepts. */
3963
+ drainTerminal(chunks) {
3964
+ if (this.pendingUsage !== void 0) {
3965
+ chunks.push({
3966
+ type: "usage",
3967
+ usage: mapChatCompletionsUsage(this.pendingUsage)
3968
+ });
3969
+ this.pendingUsage = void 0;
3970
+ }
3971
+ if (this.armedFinish !== void 0) {
3972
+ chunks.push(this.armedFinish);
3973
+ this.armedFinish = void 0;
3974
+ this.terminated = true;
3975
+ }
3976
+ }
3977
+ /**
3978
+ * Process one parsed chat-completion chunk.
3979
+ * @param event - the parsed chunk object.
3980
+ * @returns the StreamChunks this event produced (possibly none).
3981
+ */
3982
+ push(event) {
3983
+ if (this.terminated) return [];
3984
+ const chunks = [];
3985
+ const usage = event.usage;
3986
+ const hasUsage = usage !== void 0 && usage !== null;
3987
+ if (hasUsage) this.pendingUsage = usage;
3988
+ const choice = event.choices?.[0];
3989
+ const delta = choice?.delta;
3990
+ if (delta !== void 0) {
3991
+ if (typeof delta.content === "string" && delta.content.length > 0) {
3992
+ const block = this.blocks.get("content") ?? this.open("content", "text", chunks);
3993
+ block.text += delta.content;
3994
+ chunks.push({
3995
+ type: "text-delta",
3996
+ index: block.index,
3997
+ text: delta.content
3998
+ });
3999
+ }
4000
+ const reasoning = typeof delta.reasoning_content === "string" ? delta.reasoning_content : typeof delta.reasoning_text === "string" ? delta.reasoning_text : void 0;
4001
+ if (reasoning !== void 0 && reasoning.length > 0) {
4002
+ const block = this.blocks.get("reasoning") ?? this.open("reasoning", "reasoning", chunks);
4003
+ block.text += reasoning;
4004
+ chunks.push({
4005
+ type: "reasoning-delta",
4006
+ index: block.index,
4007
+ text: reasoning
4008
+ });
4009
+ }
4010
+ for (const call of delta.tool_calls ?? []) {
4011
+ const key = `call:${String(call.index ?? 0)}`;
4012
+ let block = this.blocks.get(key);
4013
+ if (block === void 0) {
4014
+ this.sawToolCall = true;
4015
+ block = this.open(key, "tool-call", chunks, call.id ?? "", call.function?.name);
4016
+ chunks.push({
4017
+ type: "tool-call-delta",
4018
+ index: block.index,
4019
+ id: CallId(block.callId),
4020
+ ...block.name === void 0 ? {} : { name: block.name },
4021
+ argumentsDelta: ""
4022
+ });
4023
+ }
4024
+ if (call.function?.arguments !== void 0 && call.function.arguments.length > 0) {
4025
+ block.text += call.function.arguments;
4026
+ chunks.push({
4027
+ type: "tool-call-delta",
4028
+ index: block.index,
4029
+ id: CallId(block.callId),
4030
+ argumentsDelta: call.function.arguments
4031
+ });
4032
+ }
4033
+ }
4034
+ }
4035
+ if (choice?.finish_reason !== void 0 && choice.finish_reason !== null) {
4036
+ this.closeAll(chunks);
4037
+ if (this.armedFinish === void 0) this.armedFinish = this.finishChunk(choice.finish_reason);
4038
+ }
4039
+ if (hasUsage && (this.armedFinish !== void 0 || choice === void 0)) this.drainTerminal(chunks);
4040
+ return chunks;
4041
+ }
4042
+ /**
4043
+ * Emit whatever the stream left pending (`[DONE]` or EOF without a final
4044
+ * usage chunk). Safe to call repeatedly.
4045
+ * @returns the remaining terminal chunks.
4046
+ */
4047
+ flush() {
4048
+ const chunks = [];
4049
+ this.drainTerminal(chunks);
4050
+ return chunks;
4051
+ }
4052
+ };
4053
+ /**
4054
+ * Consume a chat completions SSE byte stream and yield harness StreamChunks.
4055
+ * @param stream - raw response body.
4056
+ * @param onActivity - transport-activity callback for the idle watchdog.
4057
+ * @returns the chunk stream; throws when the stream ends before any finish chunk.
4058
+ */
4059
+ async function* streamChatCompletions(stream, onActivity) {
4060
+ const translator = new ChatCompletionsStreamTranslator();
4061
+ for await (const sseEvent of parseSse(stream, onActivity)) {
4062
+ if (sseEvent.data === "[DONE]") {
4063
+ yield* translator.flush();
4064
+ return;
4065
+ }
4066
+ let event;
4067
+ try {
4068
+ event = JSON.parse(sseEvent.data);
4069
+ } catch {
4070
+ throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, "MALFORMED_RESPONSE");
4071
+ }
4072
+ yield* translator.push(event);
4073
+ if (translator.terminated) return;
4074
+ }
4075
+ yield* translator.flush();
4076
+ if (!translator.terminated) throw new LlmError("chat completions SSE stream ended before a finish chunk", "STREAM_CLOSED");
4077
+ }
4078
+
4079
+ //#endregion
4080
+ //#region src/providers/copilot.ts
4081
+ /**
4082
+ * Client id of the VS Code Copilot Chat GitHub App (pi-mono and
4083
+ * copilot2api-go use the same value): the app is pre-authorized for the
4084
+ * Copilot internal token exchange, a self-registered OAuth App is not.
4085
+ */
4086
+ const COPILOT_CLIENT_ID = "Iv1.b507a08c87ecfe98";
4087
+ const COPILOT_DEVICE_CODE_URL = "https://github.com/login/device/code";
4088
+ const COPILOT_DEVICE_TOKEN_URL = "https://github.com/login/oauth/access_token";
4089
+ const COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
4090
+ const GITHUB_USER_URL = "https://api.github.com/user";
4091
+ const COPILOT_API_URL = "https://api.githubcopilot.com/chat/completions";
4092
+ /** Responses endpoint for models whose catalog entry only lists `/responses`. */
4093
+ const COPILOT_RESPONSES_URL = "https://api.githubcopilot.com/responses";
4094
+ const COPILOT_MODELS_URL = "https://api.githubcopilot.com/models";
4095
+ const COPILOT_SCOPE = "read:user";
4096
+ const COPILOT_CONTEXT_WINDOW = 128e3;
4097
+ const COPILOT_DEFAULT_MAX_TOKENS = 16e3;
4098
+ /** Refresh when the Copilot API token has less than this much life left. */
4099
+ const COPILOT_PREEMPT_MS = 5 * 6e4;
4100
+ /**
4101
+ * The VS Code update feed answers a JSON array of version strings, latest
4102
+ * stable first. The Copilot API rejects requests whose Editor-Version is too
4103
+ * old with `401 IDE token expired`, so the version is resolved live (cached
4104
+ * for a day) instead of hardcoded — a stale hardcode bricks every request.
4105
+ */
4106
+ const VSCODE_RELEASES_URL = "https://update.code.visualstudio.com/api/releases/stable";
4107
+ /** Last-known-good VS Code version when the feed is unreachable. */
4108
+ const FALLBACK_VSCODE_VERSION = "1.107.0";
4109
+ const VSCODE_VERSION_TTL_MS = 24 * 36e5;
4110
+ let vscodeVersionCache;
4111
+ let vscodeVersionInflight;
4112
+ /**
4113
+ * Resolve the VS Code version presented as Editor-Version: the latest stable
4114
+ * from the update feed, cached for a day, falling back to a pinned version
4115
+ * when the feed fails. Concurrent resolves coalesce behind one fetch.
4116
+ * @param fetchFn - fetch implementation (injectable for tests).
4117
+ * @param forceRefresh - bypass the cache (a 401 `IDE token expired` retry).
4118
+ * @returns a `major.minor.patch` version string.
4119
+ */
4120
+ async function latestVsCodeVersion(fetchFn = fetch, forceRefresh = false) {
4121
+ if (!forceRefresh && vscodeVersionCache !== void 0 && Date.now() - vscodeVersionCache.at < VSCODE_VERSION_TTL_MS) return vscodeVersionCache.version;
4122
+ vscodeVersionInflight ??= (async () => {
4123
+ try {
4124
+ const response = await fetchFn(VSCODE_RELEASES_URL, { headers: { accept: "application/json" } });
4125
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
4126
+ const releases = await response.json();
4127
+ const version = Array.isArray(releases) ? releases.find((entry) => typeof entry === "string" && /^\d+\.\d+\.\d+$/.test(entry)) : void 0;
4128
+ if (version === void 0) throw new Error("no version string in the feed");
4129
+ vscodeVersionCache = {
4130
+ version,
4131
+ at: Date.now()
4132
+ };
4133
+ return version;
4134
+ } catch {
4135
+ return vscodeVersionCache?.version ?? FALLBACK_VSCODE_VERSION;
4136
+ }
4137
+ })().finally(() => {
4138
+ vscodeVersionInflight = void 0;
4139
+ });
4140
+ return vscodeVersionInflight;
4141
+ }
4142
+ /**
4143
+ * The device-flow facts for the auth controller's DeviceFlowManager.
4144
+ * @returns the flow spec for one attempt.
4145
+ */
4146
+ function copilotDeviceFlow() {
4147
+ return {
4148
+ clientId: COPILOT_CLIENT_ID,
4149
+ scope: COPILOT_SCOPE,
4150
+ deviceCodeUrl: COPILOT_DEVICE_CODE_URL,
4151
+ tokenUrl: COPILOT_DEVICE_TOKEN_URL
4152
+ };
4153
+ }
4154
+ /**
4155
+ * Header set presenting requests as the VS Code Copilot Chat extension; the
4156
+ * Copilot API rejects traffic without an editor identity.
4157
+ * @param hasVision - whether the request carries image input.
4158
+ * @param vscodeVersion - Editor-Version value from {@link latestVsCodeVersion}.
4159
+ * @returns headers to merge into Copilot API requests.
4160
+ */
4161
+ function copilotHeaders(hasVision = false, vscodeVersion = FALLBACK_VSCODE_VERSION) {
4162
+ return {
4163
+ "user-agent": "GitHubCopilotChat/0.35.0",
4164
+ "editor-version": `vscode/${vscodeVersion}`,
4165
+ "editor-plugin-version": "copilot-chat/0.35.0",
4166
+ "copilot-integration-id": "vscode-chat",
4167
+ "openai-intent": "conversation-edits",
4168
+ "x-github-api-version": "2026-06-01",
4169
+ ...hasVision ? { "copilot-vision-request": "true" } : {}
4170
+ };
4171
+ }
4172
+ /**
4173
+ * Exchange a long-lived GitHub OAuth token for a short-lived Copilot API
4174
+ * token. A 401/403 means the GitHub token is revoked or the account lost its
4175
+ * Copilot subscription — permanent, re-login required.
4176
+ * @param githubToken - the GitHub OAuth token from the device flow.
4177
+ * @param fetchFn - fetch implementation (injectable for tests).
4178
+ * @returns the Copilot API token and its expiry.
4179
+ */
4180
+ async function exchangeCopilotToken(githubToken, fetchFn = fetch) {
4181
+ const response = await fetchFn(COPILOT_TOKEN_URL, { headers: {
4182
+ "authorization": `Bearer ${githubToken}`,
4183
+ "accept": "application/json",
4184
+ ...copilotHeaders(false, await latestVsCodeVersion(fetchFn))
4185
+ } });
4186
+ if (!response.ok) throw await oauthEndpointError(response, "copilot");
4187
+ const wire = await response.json();
4188
+ if (typeof wire.token !== "string" || wire.token.length === 0) throw new Error("copilot token endpoint returned no token");
4189
+ return {
4190
+ accessToken: wire.token,
4191
+ expiresAt: typeof wire.expires_at === "number" && wire.expires_at > 0 ? wire.expires_at * 1e3 : Date.now() + 25 * 6e4
4192
+ };
4193
+ }
4194
+ /**
4195
+ * Complete a device-flow login: exchange the GitHub token for a Copilot API
4196
+ * token and read the GitHub login name for the status display.
4197
+ * @param githubToken - the GitHub OAuth token the device flow released.
4198
+ * @param fetchFn - fetch implementation (injectable for tests).
4199
+ * @returns the session to store.
4200
+ */
4201
+ async function completeCopilotLogin(githubToken, fetchFn = fetch) {
4202
+ const pair = await exchangeCopilotToken(githubToken, fetchFn);
4203
+ let account;
4204
+ try {
4205
+ const response = await fetchFn(GITHUB_USER_URL, { headers: {
4206
+ "authorization": `Bearer ${githubToken}`,
4207
+ "accept": "application/json",
4208
+ "user-agent": "GitHubCopilotChat/0.35.0"
4209
+ } });
4210
+ if (response.ok) {
4211
+ const profile = await response.json();
4212
+ if (typeof profile.login === "string" && profile.login.length > 0) account = profile.login;
4213
+ }
4214
+ } catch {}
4215
+ return {
4216
+ accessToken: pair.accessToken,
4217
+ refreshToken: githubToken,
4218
+ expiresAt: pair.expiresAt,
4219
+ ...account === void 0 ? {} : { account }
4220
+ };
4221
+ }
4222
+ /**
4223
+ * Refresh a copilot session: re-exchange the long-lived GitHub token for a
4224
+ * fresh Copilot API token.
4225
+ * @param session - the stored session.
4226
+ * @param fetchFn - fetch implementation (injectable for tests).
4227
+ * @returns the fresh session to store.
4228
+ */
4229
+ async function refreshCopilot(session, fetchFn = fetch) {
4230
+ const pair = await exchangeCopilotToken(session.refreshToken, fetchFn);
4231
+ return {
4232
+ accessToken: pair.accessToken,
4233
+ refreshToken: session.refreshToken,
4234
+ expiresAt: pair.expiresAt,
4235
+ ...session.account === void 0 ? {} : { account: session.account }
4236
+ };
4237
+ }
4238
+ /**
4239
+ * Whether a copilot refresh failure means the login is permanently gone.
4240
+ * @param error - the thrown refresh error.
4241
+ * @returns true when re-login is the only fix (GitHub token revoked or the subscription lost).
4242
+ */
4243
+ function isCopilotPermanentRefreshError(error) {
4244
+ return error instanceof OAuthEndpointError && (error.status === 401 || error.status === 403);
4245
+ }
4246
+ /** Display name for one Copilot wire reasoning-effort value. */
4247
+ function copilotEffortName(effort) {
4248
+ return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
4249
+ }
4250
+ /**
4251
+ * Map a catalog entry's `supports.reasoning_effort` array into selectable
4252
+ * efforts. The endpoint discloses no default effort, so none is claimed
4253
+ * (absence preserves the provider's own default). Duplicates and non-string
4254
+ * entries are dropped: the harness rejects duplicate effort ids outright.
4255
+ */
4256
+ function copilotReasoning(entry) {
4257
+ const wire = entry.capabilities?.supports?.reasoning_effort;
4258
+ if (!Array.isArray(wire)) return void 0;
4259
+ const seen = /* @__PURE__ */ new Set();
4260
+ const efforts = [];
4261
+ for (const value of wire) {
4262
+ if (typeof value !== "string" || value.length === 0 || seen.has(value)) continue;
4263
+ seen.add(value);
4264
+ efforts.push({
4265
+ id: ReasoningEffortId(value),
4266
+ name: copilotEffortName(value)
4267
+ });
4268
+ }
4269
+ return efforts.length > 0 ? { efforts } : void 0;
4270
+ }
4271
+ /**
4272
+ * Fetch the live Copilot model list. Models hidden from the picker or
4273
+ * disabled by policy are excluded, as are models able to speak neither
4274
+ * protocol this adapter knows: an entry listing `/chat/completions` speaks
4275
+ * the chat wire, one listing only `/responses` (the newer GPT families,
4276
+ * e.g. gpt-5.6) speaks the Responses wire, and the choice is recorded on the
4277
+ * discovered entry so requests pick the matching endpoint; an entry listing
4278
+ * BOTH endpoints additionally records `/responses` availability, which
4279
+ * {@link copilotRequestWire} uses to reroute tools+effort requests. Vision
4280
+ * support from the catalog becomes the model's input modalities, and a
4281
+ * non-empty `supports.reasoning_effort` array becomes the model's selectable
4282
+ * reasoning efforts (the endpoint discloses no default, so none is claimed).
4283
+ * @param session - the stored session (used as-is; never refreshed here).
4284
+ * @param fetchFn - fetch implementation (injectable for tests).
4285
+ * @returns discovered chat models in endpoint order.
4286
+ */
4287
+ async function fetchCopilotModels(session, fetchFn = fetch) {
4288
+ const response = await fetchFn(COPILOT_MODELS_URL, { headers: {
4289
+ "authorization": `Bearer ${session.accessToken}`,
4290
+ "accept": "application/json",
4291
+ ...copilotHeaders(false, await latestVsCodeVersion(fetchFn))
4292
+ } });
4293
+ if (!response.ok) throw await oauthEndpointError(response, "copilot models");
4294
+ const payload = await response.json();
4295
+ if (!Array.isArray(payload.data)) throw new Error("copilot models endpoint returned no data array");
4296
+ const seen = /* @__PURE__ */ new Set();
4297
+ const discovered = [];
4298
+ for (const entry of payload.data) {
4299
+ if (typeof entry.id !== "string" || entry.id.length === 0 || seen.has(entry.id)) continue;
4300
+ if (entry.model_picker_enabled !== true || entry.policy?.state === "disabled") continue;
4301
+ let wire;
4302
+ let responsesSupported = false;
4303
+ if (Array.isArray(entry.supported_endpoints)) {
4304
+ responsesSupported = entry.supported_endpoints.includes("/responses");
4305
+ if (entry.supported_endpoints.includes("/chat/completions")) wire = "chat-completions";
4306
+ else if (responsesSupported) wire = "responses";
4307
+ else continue;
4308
+ }
4309
+ seen.add(entry.id);
4310
+ const reasoning = copilotReasoning(entry);
4311
+ discovered.push({
4312
+ id: entry.id,
4313
+ name: typeof entry.name === "string" && entry.name.length > 0 ? entry.name : entry.id,
4314
+ ...typeof entry.capabilities?.limits?.max_context_window_tokens === "number" && entry.capabilities.limits.max_context_window_tokens > 0 ? { contextWindow: entry.capabilities.limits.max_context_window_tokens } : {},
4315
+ inputModalities: entry.capabilities?.supports?.vision === true ? ["text", "image"] : ["text"],
4316
+ ...reasoning === void 0 ? {} : { reasoning },
4317
+ ...wire === void 0 ? {} : { copilotWire: wire },
4318
+ ...responsesSupported ? { copilotResponses: true } : {}
4319
+ });
4320
+ }
4321
+ if (discovered.length === 0) throw new Error("copilot models endpoint returned an empty catalog");
4322
+ return discovered;
4323
+ }
4324
+ /**
4325
+ * The wire protocol for one model: the discovered catalog entry's recorded
4326
+ * choice, defaulting to chat completions for unknown models (static-catalog
4327
+ * and no-discovery configurations, and models listing both endpoints).
4328
+ * @param entry - the discovered catalog entry, when known.
4329
+ * @returns the protocol the request for this model must speak.
4330
+ */
4331
+ function copilotWireFor(entry) {
4332
+ return entry?.copilotWire === "responses" ? "responses" : "chat-completions";
4333
+ }
4334
+ /**
4335
+ * The upstream protocol for ONE REQUEST: the model's default wire, except
4336
+ * that a dual-protocol model defaulting to chat completions must reroute to
4337
+ * Responses when the request combines function tools with a reasoning effort
4338
+ * — Copilot rejects exactly that combination on /chat/completions with
4339
+ * HTTP 400 invalid_request_body ("Function tools with reasoning_effort are
4340
+ * not supported … use /v1/responses or set reasoning_effort to 'none'",
4341
+ * observed on gpt-5.4) while /responses serves it. Effort 'none' stays on
4342
+ * the chat wire (the API allows the combination there), and models not
4343
+ * listing /responses never reroute.
4344
+ * @param entry - the discovered catalog entry, when known.
4345
+ * @param options - the harness generate options (tools + effort only).
4346
+ * @returns the protocol the request for this model must speak.
4347
+ */
4348
+ function copilotRequestWire(entry, options) {
4349
+ const wire = copilotWireFor(entry);
4350
+ if (wire !== "chat-completions") return wire;
4351
+ if (entry?.copilotResponses !== true) return wire;
4352
+ if (options.tools === void 0 || options.tools.length === 0) return wire;
4353
+ if (options.reasoningEffort === void 0 || options.reasoningEffort === "none") return wire;
4354
+ return "responses";
4355
+ }
4356
+ /**
4357
+ * The chat completions request body for one generation. The output cap rides
4358
+ * `max_completion_tokens` — the newer OpenAI-family models on Copilot reject
4359
+ * the legacy `max_tokens` parameter outright (HTTP 400 "Unsupported
4360
+ * parameter"), and the rest of the catalog accepts the new spelling.
4361
+ * @param options - the harness generate options.
4362
+ * @param messages - translated wire messages (images pre-resolved).
4363
+ * @returns the JSON body.
4364
+ */
4365
+ function copilotChatRequestBody(options, messages) {
4366
+ return {
4367
+ model: options.model,
4368
+ messages,
4369
+ ...options.tools !== void 0 && options.tools.length > 0 ? {
4370
+ tools: toChatTools(options.tools),
4371
+ tool_choice: "auto"
4372
+ } : {},
4373
+ ...options.maxTokens !== void 0 ? { max_completion_tokens: options.maxTokens } : {},
4374
+ ...options.reasoningEffort !== void 0 ? { reasoning_effort: String(options.reasoningEffort) } : {},
4375
+ stream: true,
4376
+ stream_options: { include_usage: true }
4377
+ };
4378
+ }
4379
+ /**
4380
+ * The Responses request body for one generation (the wire the `/responses`-
4381
+ * only model families speak). Usage arrives on `response.completed`.
4382
+ * @param options - the harness generate options.
4383
+ * @param resolved - translated instructions + input (images pre-resolved).
4384
+ * @returns the JSON body.
4385
+ */
4386
+ function copilotResponsesRequestBody(options, resolved) {
4387
+ return {
4388
+ model: options.model,
4389
+ ...resolved.instructions !== void 0 ? { instructions: resolved.instructions } : {},
4390
+ input: resolved.input,
4391
+ ...options.tools !== void 0 && options.tools.length > 0 ? {
4392
+ tools: toResponsesTools(options.tools),
4393
+ tool_choice: "auto"
4394
+ } : {},
4395
+ ...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
4396
+ ...options.reasoningEffort !== void 0 ? { reasoning: { effort: String(options.reasoningEffort) } } : {},
4397
+ include: ["reasoning.encrypted_content"],
4398
+ stream: true
4399
+ };
4400
+ }
4401
+ /**
4402
+ * The replayable form of one completed reasoning item: the COMPLETE item as
4403
+ * the gateway delivered it on `response.output_item.done` — its ORIGINAL id
4404
+ * (captured before the stable-key rewrite), summary parts, status, and the
4405
+ * encrypted payload. A reasoning item's `id` and `summary` are not optional
4406
+ * in the Responses input schema, so an item missing its id or its blob is
4407
+ * not replayable and degrades to the no-replay path instead of risking an
4408
+ * invalid input item.
4409
+ */
4410
+ function completedReasoningItem(item) {
4411
+ if (typeof item.encrypted_content !== "string" || item.encrypted_content.length === 0) return void 0;
4412
+ if (typeof item.id !== "string" || item.id.length === 0) return void 0;
4413
+ return {
4414
+ type: "reasoning",
4415
+ id: item.id,
4416
+ ...Array.isArray(item.summary) ? { summary: item.summary } : {},
4417
+ ...typeof item.status === "string" && item.status.length > 0 ? { status: item.status } : {},
4418
+ encrypted_content: item.encrypted_content
4419
+ };
4420
+ }
4421
+ /**
4422
+ * Rewrite Copilot's Responses-gateway item ids into stable per-item keys.
4423
+ * Unlike chatgpt.com's Responses backend, the Copilot gateway mints a FRESH
4424
+ * opaque `item.id`/`item_id` on every event of one response (the `added`,
4425
+ * each delta, and the `done` all differ), which defeats id-keyed block
4426
+ * assembly in the shared translator: text fragments would each open their
4427
+ * own block, `done` would synthesize duplicates, and a function call whose
4428
+ * arguments arrive whole only on `done` (the deltas carry empty strings)
4429
+ * would close empty. The stable key derives from the event's `output_index`
4430
+ * — the item's position in the response's output array, which survives the
4431
+ * gateway's per-event id churn even when two items' events interleave on
4432
+ * the wire (parallel tool calls do exactly that). Events without an
4433
+ * `output_index` fall back to the key of the last `output_item.added`, which
4434
+ * is only correct while one item's events stay contiguous — the pre-
4435
+ * interleaving behavior, kept for gateways that omit the field; with no
4436
+ * `added` seen yet they key to `copilot-item-0` as before. Function-call
4437
+ * identity additionally rides the gateway-stable `call_id`.
4438
+ */
4439
+ var CopilotResponsesItemNormalizer = class {
4440
+ adds = 0;
4441
+ lastKey = "copilot-item-0";
4442
+ /** Call ids and completed reasoning items collected for the open response. */
4443
+ capturedCallIds = [];
4444
+ capturedReasoning = [];
4445
+ /**
4446
+ * @param onCaptured - fired at each `response.completed` that produced BOTH
4447
+ * function calls and completed reasoning items, receiving the response's
4448
+ * call ids and replayable reasoning items so the adapter can replay them
4449
+ * on the next request.
4450
+ */
4451
+ constructor(onCaptured) {
4452
+ this.onCaptured = onCaptured;
4453
+ }
4454
+ /**
4455
+ * [2026-08-23]-[a single arrival-order ordinal mis-buckets every event after
4456
+ * a second item's `added`, mangling interleaved parallel tool calls;
4457
+ * output_index is the only correlator the gateway keeps stable]-[changes
4458
+ * keys only for streams that carry output_index; no-index streams keep the
4459
+ * old last-added-key behavior byte for byte]
4460
+ */
4461
+ keyFor(event) {
4462
+ return event.output_index !== void 0 ? `copilot-item-${String(event.output_index)}` : this.lastKey;
4463
+ }
4464
+ /**
4465
+ * Rewrite one parsed Responses event.
4466
+ * @param event - the event as parsed off the wire.
4467
+ * @returns the event with a stable item key.
4468
+ */
4469
+ push(event) {
4470
+ if (event.type === "response.output_item.added") {
4471
+ this.adds += 1;
4472
+ const key = event.output_index !== void 0 ? `copilot-item-${String(event.output_index)}` : `copilot-item-${String(this.adds)}`;
4473
+ this.lastKey = key;
4474
+ const item = event.item;
4475
+ if (item?.type === "function_call" && typeof item.call_id === "string" && item.call_id.length > 0) this.capturedCallIds.push(item.call_id);
4476
+ return item === void 0 ? event : {
4477
+ ...event,
4478
+ item: {
4479
+ ...item,
4480
+ id: key
4481
+ }
4482
+ };
4483
+ }
4484
+ if (event.type === "response.output_item.done") {
4485
+ const item = event.item;
4486
+ if (item?.type === "reasoning") {
4487
+ const captured = completedReasoningItem(item);
4488
+ if (captured !== void 0) this.capturedReasoning.push(captured);
4489
+ }
4490
+ return item === void 0 ? event : {
4491
+ ...event,
4492
+ item: {
4493
+ ...item,
4494
+ id: this.keyFor(event)
4495
+ }
4496
+ };
4497
+ }
4498
+ if (event.type === "response.completed") {
4499
+ if (this.capturedCallIds.length > 0 && this.capturedReasoning.length > 0) this.onCaptured?.(this.capturedCallIds, this.capturedReasoning);
4500
+ this.capturedCallIds = [];
4501
+ this.capturedReasoning = [];
4502
+ return event;
4503
+ }
4504
+ if (event.item_id === void 0) return event;
4505
+ return {
4506
+ ...event,
4507
+ item_id: this.keyFor(event)
4508
+ };
4509
+ }
4510
+ };
4511
+ /** Copilot wire adapter: one instance serves the `copilot` provider route. */
4512
+ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
4513
+ catalog;
4514
+ /**
4515
+ * [2026-08-23]-[a reasoning model continuing a tool chain must get its
4516
+ * reasoning back or it restarts from scratch every tool round trip; the
4517
+ * items live in ADAPTER memory because dsh-llm's reasoning ContentBlock is
4518
+ * a closed shape that cannot carry them through the harness]-[entries are
4519
+ * namespaced per ACCOUNT × CONVERSATION × MODEL, idle out via a sliding
4520
+ * TTL, and the whole store is dropped on auth transitions, so replay
4521
+ * degrades to the old behavior instead of leaking across contexts]
4522
+ */
4523
+ replayByScope = /* @__PURE__ */ new Map();
4524
+ /** Call-id entries kept per scope; see {@link captureReasoning}. */
4525
+ static REPLAY_CALL_LIMIT = 64;
4526
+ /** Conversation scopes kept at once; bounds memory when many sessions interleave. */
4527
+ static REPLAY_SCOPE_LIMIT = 32;
4528
+ /** How long a captured entry stays replayable; tool round trips take minutes, not hours. */
4529
+ static REPLAY_TTL_MS = 30 * 6e4;
4530
+ constructor(options) {
4531
+ super();
4532
+ this.options = options;
4533
+ this.catalog = new ModelCatalogCache(options.catalogStore);
4534
+ }
4535
+ /** Discovery fetcher: resolves the session through the refresh-aware path. */
4536
+ async fetchCatalog() {
4537
+ return fetchCopilotModels(await this.options.tokens.session(), this.options.fetchFn);
4538
+ }
4539
+ providerInfo(provider) {
4540
+ return {
4541
+ id: provider,
4542
+ name: "GitHub Copilot"
4543
+ };
4544
+ }
4545
+ staticModels(provider) {
4546
+ return this.options.models.map((model) => ({
4547
+ provider,
4548
+ id: model.id,
4549
+ name: model.name ?? model.id,
4550
+ inputModalities: model.inputModalities ?? ["text"]
4551
+ }));
4552
+ }
4553
+ async listModels(provider) {
4554
+ if (await this.options.tokens.peek() === void 0) return [];
4555
+ if (!this.options.discovery) return this.staticModels(provider);
4556
+ try {
4557
+ return (await discoverOrRetryAuth((force) => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog()))).map((model) => ({
4558
+ provider,
4559
+ id: model.id,
4560
+ name: model.name,
4561
+ ...model.description === void 0 ? {} : { description: model.description },
4562
+ ...model.inputModalities === void 0 ? {} : { inputModalities: model.inputModalities }
4563
+ }));
4564
+ } catch (error) {
4565
+ if (isMissingOrInvalidCredential(error)) return [];
4566
+ this.options.onWarn?.(`copilot model discovery failed; using the built-in catalog (${errorChain(error)})`);
4567
+ return this.staticModels(provider);
4568
+ }
4569
+ }
4570
+ /**
4571
+ * The discovered entry for one model. Resolved through the cache's
4572
+ * stale-while-revalidate path: capability metadata must stay stable across
4573
+ * a long conversation — a mid-turn refetch must neither block nor fail the
4574
+ * call before provider I/O.
4575
+ */
4576
+ async discovered(model) {
4577
+ if (!this.options.discovery) return void 0;
4578
+ return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
4579
+ }
4580
+ /**
4581
+ * [2026-08-23]-[a manually configured responses-only model combined with
4582
+ * `discovery:false` left discovered() undefined, so copilotRequestWire
4583
+ * silently defaulted to /chat/completions and the request 404/400'd at the
4584
+ * gateway; an explicit config wire must win over catalog inference]-[config
4585
+ * `models[].wire` now routes the request even without discovery]
4586
+ */
4587
+ configuredWireEntry(model) {
4588
+ const configured = this.options.models.find((entry) => entry.id === model);
4589
+ return configured?.wire === void 0 ? void 0 : {
4590
+ id: configured.id,
4591
+ name: configured.name ?? configured.id,
4592
+ copilotWire: configured.wire
4593
+ };
4594
+ }
4595
+ /**
4596
+ * The replay scope isolating one ACCOUNT × CONVERSATION × MODEL. The
4597
+ * account identity is the session's long-lived GitHub token (stable across
4598
+ * Copilot-token refreshes, different per GitHub login); the conversation is
4599
+ * the loop-stamped `sessionId`, falling back to the first message's id
4600
+ * when a hand-built request carries no session stamp; the model separates
4601
+ * wire families. A call id captured in one scope is invisible to every
4602
+ * other scope, so reused ids cannot leak reasoning across accounts,
4603
+ * conversations, or models.
4604
+ */
4605
+ replayScope(tokenKey, options) {
4606
+ return `${tokenKey}\u0000${options.sessionId !== void 0 ? `session:${String(options.sessionId)}` : options.messages[0] !== void 0 ? `anchor:${String(options.messages[0].id)}` : "conversation:none"}\u0000${options.model}`;
4607
+ }
4608
+ /**
4609
+ * Store one response's completed reasoning items behind every call id it
4610
+ * produced, inside one replay scope. Retention: a CONSUMED entry is kept —
4611
+ * every later round of the same conversation replays ALL its earlier
4612
+ * function_calls — until it idles out of the TTL (see {@link replayFor})
4613
+ * or the per-scope entry cap evicts it oldest-first. All calls of one
4614
+ * response share ONE entry object: toResponsesInput dedupes replays by
4615
+ * array reference, so parallel calls replay the items once instead of once
4616
+ * per call.
4617
+ */
4618
+ captureReasoning(scope, callIds, items) {
4619
+ let entries = this.replayByScope.get(scope);
4620
+ if (entries === void 0) {
4621
+ entries = /* @__PURE__ */ new Map();
4622
+ this.replayByScope.set(scope, entries);
4623
+ } else {
4624
+ this.replayByScope.delete(scope);
4625
+ this.replayByScope.set(scope, entries);
4626
+ }
4627
+ const now = Date.now();
4628
+ for (const [callId, entry$1] of entries) if (now - entry$1.at >= CopilotAdapter.REPLAY_TTL_MS) entries.delete(callId);
4629
+ const entry = {
4630
+ items: [...items],
4631
+ at: now
4632
+ };
4633
+ for (const callId of callIds) entries.set(callId, entry);
4634
+ while (entries.size > CopilotAdapter.REPLAY_CALL_LIMIT) {
4635
+ const oldest = entries.keys().next().value;
4636
+ if (oldest === void 0) break;
4637
+ entries.delete(oldest);
4638
+ }
4639
+ while (this.replayByScope.size > CopilotAdapter.REPLAY_SCOPE_LIMIT) {
4640
+ const oldest = this.replayByScope.keys().next().value;
4641
+ if (oldest === void 0) break;
4642
+ this.replayByScope.delete(oldest);
4643
+ }
4644
+ }
4645
+ /**
4646
+ * The replay items for one call id in one scope, when still fresh. The TTL
4647
+ * bounds IDLE time, not total age: a hit refreshes the entry (and its
4648
+ * eviction recency), so an ongoing conversation keeps its chain alive
4649
+ * while a conversation that stopped asking forgets within the TTL. An
4650
+ * absent or aged-out entry answers `undefined` — the no-replay
4651
+ * degradation, never an error.
4652
+ */
4653
+ replayFor(scope, callId) {
4654
+ const entries = this.replayByScope.get(scope);
4655
+ const entry = entries?.get(callId);
4656
+ if (entries === void 0 || entry === void 0) return void 0;
4657
+ const now = Date.now();
4658
+ if (now - entry.at >= CopilotAdapter.REPLAY_TTL_MS) return void 0;
4659
+ entry.at = now;
4660
+ entries.delete(callId);
4661
+ entries.set(callId, entry);
4662
+ this.replayByScope.delete(scope);
4663
+ this.replayByScope.set(scope, entries);
4664
+ return entry.items;
4665
+ }
4666
+ /**
4667
+ * Drop every captured replay entry. Lookup correctness never depends on
4668
+ * the call — the scope already carries the account identity — but the host
4669
+ * wiring invokes this on every copilot auth transition (login, logout,
4670
+ * credential death) so a switched account's memory never holds the
4671
+ * previous account's encrypted reasoning at all; conversation teardown is
4672
+ * bounded by the TTL and the caps.
4673
+ */
4674
+ clearReplayState() {
4675
+ this.replayByScope.clear();
4676
+ }
4677
+ async resolveModel(provider, model) {
4678
+ const discovered = await this.discovered(model);
4679
+ const configured = this.options.models.find((entry) => entry.id === model);
4680
+ return {
4681
+ provider,
4682
+ id: model,
4683
+ name: discovered?.name ?? configured?.name ?? model,
4684
+ ...discovered?.description === void 0 ? {} : { description: discovered.description },
4685
+ inputModalities: discovered?.inputModalities ?? configured?.inputModalities ?? ["text"],
4686
+ context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? COPILOT_CONTEXT_WINDOW },
4687
+ defaultMaxTokens: configured?.maxTokens ?? COPILOT_DEFAULT_MAX_TOKENS,
4688
+ ...discovered?.reasoning === void 0 ? {} : { reasoning: discovered.reasoning }
4689
+ };
4690
+ }
4691
+ async *stream(options) {
4692
+ const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
4693
+ try {
4694
+ const wire = copilotRequestWire(this.configuredWireEntry(options.model) ?? await this.discovered(options.model), options);
4695
+ let session = await this.options.tokens.session();
4696
+ const scope = this.replayScope(session.refreshToken, options);
4697
+ let response = await this.request(options, session, watchdog.signal, wire, scope);
4698
+ if (response.status === 401) {
4699
+ await latestVsCodeVersion(this.options.fetchFn ?? fetch, true);
4700
+ session = await this.options.tokens.session(true);
4701
+ response = await this.request(options, session, watchdog.signal, wire, scope);
4702
+ }
4703
+ if (!response.ok) throw await httpLlmError(response, "copilot API");
4704
+ if (response.body === null) throw new LlmError("copilot API returned no response body", EMPTY_RESPONSE_CODE);
4705
+ const pulse = () => {
4706
+ watchdog.pulse();
4707
+ };
4708
+ if (wire === "responses") {
4709
+ const normalizer = new CopilotResponsesItemNormalizer((callIds, items) => {
4710
+ this.captureReasoning(scope, callIds, items);
4711
+ });
4712
+ yield* streamResponses(response.body, pulse, (event) => normalizer.push(event));
4713
+ } else yield* streamChatCompletions(response.body, pulse);
4714
+ } catch (error) {
4715
+ throw mapFetchFailure("copilot API", error, watchdog, options.signal);
4716
+ } finally {
4717
+ watchdog.stop();
4718
+ }
4719
+ }
4720
+ async request(options, session, signal, wire, replayScopeKey) {
4721
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
4722
+ const hasVision = messages.some((message) => message.content.some((block) => block.type === "image"));
4723
+ const body = wire === "responses" ? copilotResponsesRequestBody(options, toResponsesInput(messages, options.system, (callId) => this.replayFor(replayScopeKey, callId))) : copilotChatRequestBody(options, toChatMessages(messages, options.system));
4724
+ return fetch(wire === "responses" ? COPILOT_RESPONSES_URL : COPILOT_API_URL, {
4725
+ method: "POST",
4726
+ headers: {
4727
+ "authorization": `Bearer ${session.accessToken}`,
4728
+ "accept": "text/event-stream",
4729
+ "content-type": "application/json",
4730
+ ...copilotHeaders(hasVision, await latestVsCodeVersion(this.options.fetchFn ?? fetch))
4731
+ },
4732
+ body: JSON.stringify(body),
4733
+ signal
4734
+ });
4735
+ }
4736
+ };
4737
+
4738
+ //#endregion
4739
+ //#region src/tools/x-search.ts
4740
+ /** Endpoint the search request is posted to. */
4741
+ const X_SEARCH_URL = "https://api.x.ai/v1/responses";
4742
+ /** Grok model the search runs on (a catalog model of the grok provider). */
4743
+ const X_SEARCH_MODEL = "grok-4";
4744
+ /** xAI caps each handle filter list at ten entries. */
4745
+ const MAX_HANDLES = 10;
4746
+ /**
4747
+ * Validate and assemble the request facts from tool arguments. Throws plain
4748
+ * Errors for argument problems the schema DSL cannot express (non-empty
4749
+ * query, handle caps, mutually exclusive filters).
4750
+ */
4751
+ function buildXSearchRequest(args) {
4752
+ const query = args.query.trim();
4753
+ if (query.length === 0) throw new Error("x_search: query must be a non-empty string");
4754
+ const allowed = normalizeHandles(args.allowed_x_handles, "allowed_x_handles");
4755
+ const excluded = normalizeHandles(args.excluded_x_handles, "excluded_x_handles");
4756
+ if (allowed.length > 0 && excluded.length > 0) throw new Error("x_search: allowed_x_handles and excluded_x_handles cannot be used together");
4757
+ const tool = { type: "x_search" };
4758
+ if (allowed.length > 0) tool.allowed_x_handles = allowed;
4759
+ if (excluded.length > 0) tool.excluded_x_handles = excluded;
4760
+ if (args.from_date !== void 0 && args.from_date.trim().length > 0) tool.from_date = args.from_date.trim();
4761
+ if (args.to_date !== void 0 && args.to_date.trim().length > 0) tool.to_date = args.to_date.trim();
4762
+ if (args.enable_image_understanding === true) tool.enable_image_understanding = true;
4763
+ if (args.enable_video_understanding === true) tool.enable_video_understanding = true;
4764
+ return {
4765
+ query,
4766
+ tool
4767
+ };
4768
+ }
4769
+ /** Strip `@` prefixes, drop blanks, and enforce the provider's handle cap. */
4770
+ function normalizeHandles(value, field) {
4771
+ if (value === void 0) return [];
4772
+ const handles = value.map((handle) => handle.trim().replace(/^@+/, "")).filter((handle) => handle.length > 0);
4773
+ if (handles.length > MAX_HANDLES) throw new Error(`x_search: ${field} supports at most ${MAX_HANDLES} handles`);
4774
+ return handles;
4775
+ }
4776
+ function isRecord$1(value) {
4777
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4778
+ }
4779
+ /**
4780
+ * Extract the answer text and citation URLs from a Responses payload: the
4781
+ * `output_text` shortcut or message output parts for the answer, and both
4782
+ * top-level `citations` and inline `url_citation` annotations for sources.
4783
+ */
4784
+ function parseXSearchResponse(payload) {
4785
+ const body = isRecord$1(payload) ? payload : {};
4786
+ let answer = typeof body.output_text === "string" ? body.output_text.trim() : "";
4787
+ const citations = [];
4788
+ const push = (url) => {
3255
4789
  if (typeof url === "string" && url.length > 0 && !citations.includes(url)) citations.push(url);
3256
4790
  };
3257
4791
  if (Array.isArray(body.citations)) for (const citation of body.citations) push(citation);
@@ -3697,19 +5231,11 @@ function createImageGenerateTool(options) {
3697
5231
  });
3698
5232
  }
3699
5233
  const revisedPrompt = images.find((image) => image.revisedPrompt !== void 0)?.revisedPrompt;
3700
- const value = {
5234
+ return {
3701
5235
  paths,
3702
5236
  ...refs.length > 0 ? { images: refs } : {},
3703
5237
  ...revisedPrompt === void 0 ? {} : { revisedPrompt }
3704
5238
  };
3705
- if (exec.parent !== void 0 && refs.length > 0) exec.deferContext(createUserMessage({
3706
- content: imageGenerateContent(value),
3707
- source: {
3708
- kind: "plugin",
3709
- plugin: "dsh-plugin-subscriptions"
3710
- }
3711
- }));
3712
- return value;
3713
5239
  }
3714
5240
  });
3715
5241
  }
@@ -3966,26 +5492,30 @@ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
3966
5492
  const providerIdSchema = z.union([
3967
5493
  "codex",
3968
5494
  "claude",
3969
- "grok"
5495
+ "grok",
5496
+ "copilot"
3970
5497
  ]);
3971
5498
  const modelEntrySchema = z.object({
3972
5499
  id: z.string().required(),
3973
5500
  name: z.string(),
3974
5501
  contextWindow: z.number().step(1).min(1),
3975
5502
  maxTokens: z.number().step(1).min(1),
3976
- inputModalities: z.array(z.union(["text", "image"]))
5503
+ inputModalities: z.array(z.union(["text", "image"])),
5504
+ wire: z.union(["chat-completions", "responses"])
3977
5505
  });
3978
5506
  const Config = z.object({
3979
5507
  providers: z.array(providerIdSchema).default([
3980
5508
  "codex",
3981
5509
  "claude",
3982
- "grok"
5510
+ "grok",
5511
+ "copilot"
3983
5512
  ]),
3984
5513
  streamIdleTimeoutMs: z.number().min(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
3985
5514
  models: z.object({
3986
5515
  codex: z.array(modelEntrySchema),
3987
5516
  claude: z.array(modelEntrySchema),
3988
- grok: z.array(modelEntrySchema)
5517
+ grok: z.array(modelEntrySchema),
5518
+ copilot: z.array(modelEntrySchema)
3989
5519
  })
3990
5520
  });
3991
5521
  /** Built-in catalogs used when the config does not override a provider's models. */
@@ -4042,6 +5572,28 @@ const DEFAULT_MODELS = {
4042
5572
  id: "grok-code-fast-1",
4043
5573
  name: "Grok Code Fast 1"
4044
5574
  }
5575
+ ],
5576
+ copilot: [
5577
+ {
5578
+ id: "gpt-4.1",
5579
+ name: "GPT-4.1",
5580
+ inputModalities: ["text", "image"]
5581
+ },
5582
+ {
5583
+ id: "gpt-4o",
5584
+ name: "GPT-4o",
5585
+ inputModalities: ["text", "image"]
5586
+ },
5587
+ {
5588
+ id: "claude-sonnet-4.5",
5589
+ name: "Claude Sonnet 4.5",
5590
+ inputModalities: ["text", "image"]
5591
+ },
5592
+ {
5593
+ id: "gemini-2.5-pro",
5594
+ name: "Gemini 2.5 Pro",
5595
+ inputModalities: ["text", "image"]
5596
+ }
4045
5597
  ]
4046
5598
  };
4047
5599
  /** Validate and detach the model catalog for every provider. */
@@ -4053,7 +5605,8 @@ function resolveCatalog(models) {
4053
5605
  return {
4054
5606
  codex: resolve("codex"),
4055
5607
  claude: resolve("claude"),
4056
- grok: resolve("grok")
5608
+ grok: resolve("grok"),
5609
+ copilot: resolve("copilot")
4057
5610
  };
4058
5611
  }
4059
5612
  /** The display account of a stored session, for the status endpoint. */
@@ -4066,21 +5619,49 @@ function accountOf(provider, session) {
4066
5619
  }
4067
5620
  case "claude": return session.emailAddress;
4068
5621
  case "grok": return session.account;
5622
+ case "copilot": return session.account;
4069
5623
  }
4070
5624
  }
4071
5625
  /**
4072
5626
  * Auth operations behind the `/subscriptions-auth` RPC channel: start/complete
4073
5627
  * OAuth attempts in the background, feed pasted codes, cancel, log out, and
4074
5628
  * answer usage lookups.
5629
+ *
5630
+ * @internal Exported for tests only; not part of the plugin's public surface.
4075
5631
  */
4076
5632
  var SubscriptionsAuthController = class {
4077
5633
  /** Last login failure per provider, surfaced as `detail` until the next success. */
4078
5634
  lastError = /* @__PURE__ */ new Map();
4079
- constructor(flows, onAuthChanged, resolveAttachments, usageFetchers = {}) {
5635
+ /**
5636
+ * Device-flow logins whose poll already settled but whose token exchange +
5637
+ * persist is still running. Between those two moments the attempt is gone
5638
+ * from the flow manager (busy=false) while no session exists yet
5639
+ * (loggedIn=false) — counting this window as busy keeps the Settings page
5640
+ * polling until the card can show the real outcome.
5641
+ */
5642
+ finalizing = /* @__PURE__ */ new Set();
5643
+ /** In-flight OAuth completions, one per provider at most. */
5644
+ completions = /* @__PURE__ */ new Map();
5645
+ /**
5646
+ * Per-provider claim counter. Everything that takes ownership of a
5647
+ * provider's session — starting a login, importing Claude Code credentials,
5648
+ * cancelling, logging out — bumps it, and a session write carrying an older
5649
+ * number has been superseded and is dropped.
5650
+ *
5651
+ * The counter is what makes a late OAuth completion safe: an attempt leaves
5652
+ * `OAuthFlowManager`'s pending map the moment its callback delivers the
5653
+ * code, while the token exchange that follows can still run for seconds. For
5654
+ * that whole window `pending(provider)?.cancel()` is a no-op, so ownership
5655
+ * cannot be read off the flow manager.
5656
+ */
5657
+ claims = /* @__PURE__ */ new Map();
5658
+ constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials) {
4080
5659
  this.flows = flows;
5660
+ this.deviceFlows = deviceFlows;
4081
5661
  this.onAuthChanged = onAuthChanged;
4082
5662
  this.resolveAttachments = resolveAttachments;
4083
5663
  this.usageFetchers = usageFetchers;
5664
+ this.readClaudeCreds = readClaudeCreds;
4084
5665
  }
4085
5666
  usage(provider, signal) {
4086
5667
  const fetcher = this.usageFetchers[provider];
@@ -4108,7 +5689,7 @@ var SubscriptionsAuthController = class {
4108
5689
  const detail = this.lastError.get(provider);
4109
5690
  return {
4110
5691
  loggedIn: session !== void 0,
4111
- busy: this.flows.isBusy(provider),
5692
+ busy: this.flows.isBusy(provider) || this.deviceFlows.isBusy(provider) || this.finalizing.has(provider),
4112
5693
  ...session === void 0 ? {} : { expiresAt: session.expiresAt },
4113
5694
  ...account === void 0 ? {} : { account },
4114
5695
  ...detail === void 0 ? {} : { detail }
@@ -4116,30 +5697,73 @@ var SubscriptionsAuthController = class {
4116
5697
  }
4117
5698
  async login(provider) {
4118
5699
  if (provider === "claude") {
4119
- const session = readClaudeCodeCredentials();
4120
- if (session) {
4121
- await this.persist("claude", session);
5700
+ const imported = this.readClaudeCreds();
5701
+ if (imported !== void 0) {
5702
+ this.claim("claude");
5703
+ this.flows.pending("claude")?.cancel();
5704
+ await this.persist("claude", imported);
4122
5705
  this.lastError.delete("claude");
4123
5706
  this.onAuthChanged("claude");
4124
5707
  return { authorizeUrl: "" };
4125
5708
  }
4126
- throw new Error("Claude Code credentials not found. Run \"claude\" first to log in.");
5709
+ const attempt$1 = await this.flows.start("claude", claudeFlow);
5710
+ this.completions.set("claude", this.complete("claude", attempt$1, this.claim("claude")));
5711
+ return { authorizeUrl: attempt$1.authorizeUrl };
5712
+ }
5713
+ if (provider === "copilot") {
5714
+ const attempt$1 = await this.deviceFlows.start(provider, copilotDeviceFlow());
5715
+ this.finalizing.add(provider);
5716
+ this.completeDevice(provider, attempt$1);
5717
+ return {
5718
+ authorizeUrl: attempt$1.verificationUrl,
5719
+ userCode: attempt$1.userCode
5720
+ };
4127
5721
  }
4128
5722
  const spec = provider === "grok" ? await grokFlow() : codexFlow;
4129
5723
  const attempt = await this.flows.start(provider, spec);
4130
- this.complete(provider, attempt);
5724
+ this.completions.set(provider, this.complete(provider, attempt, this.claim(provider)));
4131
5725
  return { authorizeUrl: attempt.authorizeUrl };
4132
5726
  }
4133
- /** Drive one attempt to a stored session; records failures for the status endpoint. */
4134
- async complete(provider, attempt) {
5727
+ /**
5728
+ * Take ownership of a provider's session, superseding every older claim.
5729
+ * @param provider - the provider route.
5730
+ * @returns the claim number a later write checks itself against.
5731
+ */
5732
+ claim(provider) {
5733
+ const next = (this.claims.get(provider) ?? 0) + 1;
5734
+ this.claims.set(provider, next);
5735
+ return next;
5736
+ }
5737
+ /**
5738
+ * Drive one attempt to a stored session; records failures for the status
5739
+ * endpoint. The exchange runs unsupervised — the attempt is gone from the
5740
+ * flow manager as soon as its code arrives — so the result is stored only
5741
+ * while `claim` still owns the provider's session.
5742
+ */
5743
+ async complete(provider, attempt, claim) {
4135
5744
  try {
4136
5745
  const code = await attempt.waitCode();
4137
5746
  const session = await this.exchange(provider, code, attempt);
5747
+ if (this.claims.get(provider) !== claim) return;
5748
+ await this.persist(provider, session);
5749
+ this.lastError.delete(provider);
5750
+ this.onAuthChanged(provider);
5751
+ } catch (error) {
5752
+ if (this.claims.get(provider) !== claim) return;
5753
+ if (!(error instanceof Error && error.message === "login cancelled")) this.lastError.set(provider, errorChain(error));
5754
+ }
5755
+ }
5756
+ /** Drive one device-flow attempt to a stored session (the copilot path of {@link complete}). */
5757
+ async completeDevice(provider, attempt) {
5758
+ try {
5759
+ const session = await completeCopilotLogin(await attempt.waitToken());
4138
5760
  await this.persist(provider, session);
4139
5761
  this.lastError.delete(provider);
4140
5762
  this.onAuthChanged(provider);
4141
5763
  } catch (error) {
4142
5764
  if (!(error instanceof Error && error.message === "login cancelled")) this.lastError.set(provider, errorChain(error));
5765
+ } finally {
5766
+ this.finalizing.delete(provider);
4143
5767
  }
4144
5768
  }
4145
5769
  exchange(provider, code, attempt) {
@@ -4147,6 +5771,7 @@ var SubscriptionsAuthController = class {
4147
5771
  case "codex": return exchangeCodexCode(code, attempt.pkce.verifier, attempt.redirectUri);
4148
5772
  case "claude": return exchangeClaudeCode(code, attempt.pkce.verifier, attempt.redirectUri, attempt.state);
4149
5773
  case "grok": return exchangeGrokCode(code, attempt.pkce.verifier, attempt.redirectUri, attempt.pkce.challenge);
5774
+ case "copilot": return Promise.reject(/* @__PURE__ */ new Error("copilot uses the device flow; no authorization code to exchange"));
4150
5775
  }
4151
5776
  }
4152
5777
  persist(provider, session) {
@@ -4154,8 +5779,19 @@ var SubscriptionsAuthController = class {
4154
5779
  case "codex": return saveSession("codex", session);
4155
5780
  case "claude": return saveSession("claude", session);
4156
5781
  case "grok": return saveSession("grok", session);
5782
+ case "copilot": return saveSession("copilot", session);
4157
5783
  }
4158
5784
  }
5785
+ /**
5786
+ * Settle once no OAuth completion is running for a provider.
5787
+ *
5788
+ * @internal Exported for tests only: a login's token exchange outlives the
5789
+ * `login()` call that started it, and a test asserting on what it stored
5790
+ * would otherwise have to guess at a timeout.
5791
+ */
5792
+ async settled(provider) {
5793
+ await this.completions.get(provider);
5794
+ }
4159
5795
  manual(provider, input) {
4160
5796
  const attempt = this.flows.pending(provider);
4161
5797
  if (attempt === void 0) return Promise.reject(/* @__PURE__ */ new Error(`no ${provider} login attempt is in progress`));
@@ -4163,11 +5799,15 @@ var SubscriptionsAuthController = class {
4163
5799
  return Promise.resolve();
4164
5800
  }
4165
5801
  cancel(provider) {
5802
+ this.claim(provider);
4166
5803
  this.flows.pending(provider)?.cancel();
5804
+ this.deviceFlows.pending(provider)?.cancel();
4167
5805
  return Promise.resolve();
4168
5806
  }
4169
5807
  async logout(provider) {
5808
+ this.claim(provider);
4170
5809
  this.flows.pending(provider)?.cancel();
5810
+ this.deviceFlows.pending(provider)?.cancel();
4171
5811
  await deleteSession(provider);
4172
5812
  this.lastError.delete(provider);
4173
5813
  this.onAuthChanged(provider);
@@ -4180,18 +5820,23 @@ function apply(ctx, config) {
4180
5820
  const catalog = resolveCatalog(config.models);
4181
5821
  const overridden = new Set(PROVIDER_IDS.filter((provider) => (config.models?.[provider]?.length ?? 0) > 0));
4182
5822
  const flows = new OAuthFlowManager();
5823
+ const deviceFlows = new DeviceFlowManager();
4183
5824
  const onWarn = (message) => {
4184
5825
  ctx.logger.warn(`dsh-plugin-subscriptions: ${message}`);
4185
5826
  };
4186
5827
  const resolveAttachments = () => ctx.get("attachments");
4187
5828
  const handles = /* @__PURE__ */ new Map();
4188
5829
  const authChanged = (provider) => {
5830
+ if (provider === "copilot") copilotAdapter?.clearReplayState();
4189
5831
  handles.get(provider)?.replace([provider]);
4190
5832
  };
4191
5833
  let codexTokens;
4192
5834
  let claudeTokens;
4193
5835
  let grokTokens;
4194
5836
  const usageFetchers = {};
5837
+ const speedBySession = /* @__PURE__ */ new Map();
5838
+ let codexAdapter;
5839
+ let copilotAdapter;
4195
5840
  for (const provider of providers) switch (provider) {
4196
5841
  case "codex": {
4197
5842
  const tokens = new TokenManager({
@@ -4208,15 +5853,19 @@ function apply(ctx, config) {
4208
5853
  });
4209
5854
  codexTokens = tokens;
4210
5855
  usageFetchers.codex = async (signal) => fetchCodexUsage(await tokens.session(), fetch, signal);
4211
- handles.set("codex", ctx.llm.registerAdapter(["codex"], new CodexAdapter({
5856
+ let adapter;
5857
+ adapter = new CodexAdapter({
4212
5858
  models: catalog.codex,
4213
5859
  streamIdleTimeoutMs,
4214
5860
  tokens,
4215
5861
  discovery: !overridden.has("codex"),
4216
5862
  onWarn,
4217
5863
  resolveAttachments,
4218
- catalogStore: catalogStore("codex")
4219
- })));
5864
+ catalogStore: catalogStore("codex"),
5865
+ speedFor: (sessionId, model) => sessionId !== void 0 && speedBySession.get(sessionId) === "fast" && adapter.supportsFastTier(model)
5866
+ });
5867
+ codexAdapter = adapter;
5868
+ handles.set("codex", ctx.llm.registerAdapter(["codex"], adapter));
4220
5869
  break;
4221
5870
  }
4222
5871
  case "claude": {
@@ -4272,8 +5921,44 @@ function apply(ctx, config) {
4272
5921
  })));
4273
5922
  break;
4274
5923
  }
5924
+ case "copilot": {
5925
+ const tokens = new TokenManager({
5926
+ displayName: "GitHub Copilot",
5927
+ preemptMs: COPILOT_PREEMPT_MS,
5928
+ load: () => getSession("copilot"),
5929
+ save: (session) => saveSession("copilot", session),
5930
+ remove: () => deleteSession("copilot"),
5931
+ refresh: refreshCopilot,
5932
+ isPermanent: isCopilotPermanentRefreshError,
5933
+ onRemoved: () => {
5934
+ authChanged("copilot");
5935
+ }
5936
+ });
5937
+ copilotAdapter = new CopilotAdapter({
5938
+ models: catalog.copilot,
5939
+ streamIdleTimeoutMs,
5940
+ tokens,
5941
+ discovery: !overridden.has("copilot"),
5942
+ onWarn,
5943
+ resolveAttachments,
5944
+ catalogStore: catalogStore("copilot")
5945
+ });
5946
+ handles.set("copilot", ctx.llm.registerAdapter(["copilot"], copilotAdapter));
5947
+ break;
5948
+ }
4275
5949
  }
4276
- registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments, usageFetchers));
5950
+ registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers), {
5951
+ async speed(sessionId) {
5952
+ return {
5953
+ tier: speedBySession.get(sessionId) ?? "standard",
5954
+ fastModels: await codexAdapter?.fastCapableModels() ?? []
5955
+ };
5956
+ },
5957
+ async setSpeed(sessionId, tier) {
5958
+ if (tier === "standard") speedBySession.delete(sessionId);
5959
+ else speedBySession.set(sessionId, tier);
5960
+ }
5961
+ });
4277
5962
  if (claudeTokens !== void 0) {
4278
5963
  const syncTimer = setInterval(() => {
4279
5964
  claudeTokens?.session().catch(() => {});
@@ -4297,4 +5982,4 @@ function apply(ctx, config) {
4297
5982
  }
4298
5983
 
4299
5984
  //#endregion
4300
- export { Config, DEFAULT_STREAM_IDLE_TIMEOUT_MS, apply, inject, name };
5985
+ export { Config, DEFAULT_STREAM_IDLE_TIMEOUT_MS, SubscriptionsAuthController, apply, inject, name };