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.
- package/README.md +19 -6
- package/README.zh.md +18 -6
- package/lib/auth/device-flow.d.ts +64 -0
- package/lib/auth/device-flow.js +176 -0
- package/lib/auth/oauth-flow.js +1 -1
- package/lib/auth/rpc.d.ts +21 -2
- package/lib/auth/rpc.js +23 -3
- package/lib/auth/store.d.ts +20 -2
- package/lib/auth/store.js +45 -9
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/ImageGenerateToolview.js +2 -2
- package/lib/client/SpeedSelect.d.ts +48 -0
- package/lib/client/SpeedSelect.js +173 -0
- package/lib/client/SubscriptionsSection.d.ts +10 -1
- package/lib/client/SubscriptionsSection.js +48 -5
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +42 -0
- package/lib/client/locales.d.ts +22 -0
- package/lib/client/locales.js +22 -0
- package/lib/client.js +679 -77
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +1868 -183
- package/lib/providers/catalog-store.js +19 -0
- package/lib/providers/claude.d.ts +20 -1
- package/lib/providers/claude.js +58 -31
- package/lib/providers/codex.d.ts +27 -0
- package/lib/providers/codex.js +117 -27
- package/lib/providers/common.d.ts +34 -1
- package/lib/providers/common.js +48 -1
- package/lib/providers/copilot.d.ts +315 -0
- package/lib/providers/copilot.js +786 -0
- package/lib/providers/grok.d.ts +7 -2
- package/lib/providers/grok.js +46 -18
- package/lib/tools/image-generate.js +3 -10
- package/lib/translate/anthropic.d.ts +47 -6
- package/lib/translate/anthropic.js +135 -20
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +363 -0
- package/lib/translate/responses.d.ts +49 -5
- package/lib/translate/responses.js +40 -7
- 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,
|
|
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(
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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$
|
|
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
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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 (
|
|
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
|
|
1934
|
-
const
|
|
1935
|
-
|
|
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
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
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
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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;
|
|
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
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
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
|
-
|
|
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":
|
|
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":
|
|
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 (
|
|
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
|
|
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":
|
|
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
|
|
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
|
-
...
|
|
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()))
|
|
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 (
|
|
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/
|
|
3206
|
-
/**
|
|
3207
|
-
|
|
3208
|
-
|
|
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
|
-
*
|
|
3247
|
-
*
|
|
3248
|
-
*
|
|
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
|
|
3251
|
-
const
|
|
3252
|
-
|
|
3253
|
-
const
|
|
3254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4120
|
-
if (
|
|
4121
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
4134
|
-
|
|
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
|
-
|
|
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 };
|