@eddyskywalker/dsh-chatgpt-subscription 0.10.11 → 0.10.13
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/CHANGELOG.md +33 -0
- package/README.md +176 -42
- package/lib/client.js +133 -26
- package/lib/client.js.map +1 -1
- package/lib/index.js +1145 -561
- package/lib/types/client/claude/ClaudeSection.d.ts.map +1 -1
- package/lib/types/client/claude/locales.d.ts +17 -2
- package/lib/types/client/claude/locales.d.ts.map +1 -1
- package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
- package/lib/types/client/kimi-code/locales.d.ts +17 -2
- package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
- package/lib/types/compat.d.ts +35 -2
- package/lib/types/compat.d.ts.map +1 -1
- package/lib/types/host/adapter.d.ts +20 -1
- package/lib/types/host/adapter.d.ts.map +1 -1
- package/lib/types/host/claude/account-pool.d.ts +4 -21
- package/lib/types/host/claude/account-pool.d.ts.map +1 -1
- package/lib/types/host/claude/adapter.d.ts +11 -0
- package/lib/types/host/claude/adapter.d.ts.map +1 -1
- package/lib/types/host/claude/client.d.ts +7 -2
- package/lib/types/host/claude/client.d.ts.map +1 -1
- package/lib/types/host/claude/mapper.d.ts +13 -1
- package/lib/types/host/claude/mapper.d.ts.map +1 -1
- package/lib/types/host/claude/routes.d.ts.map +1 -1
- package/lib/types/host/claude/token-store.d.ts +9 -0
- package/lib/types/host/claude/token-store.d.ts.map +1 -1
- package/lib/types/host/claude/types.d.ts +35 -0
- package/lib/types/host/claude/types.d.ts.map +1 -1
- package/lib/types/host/codex-account-pool.d.ts.map +1 -1
- package/lib/types/host/codex-catalog.d.ts +54 -0
- package/lib/types/host/codex-catalog.d.ts.map +1 -0
- package/lib/types/host/codex-images.d.ts.map +1 -1
- package/lib/types/host/codex-search.d.ts.map +1 -1
- package/lib/types/host/common/account-pool.d.ts +93 -6
- package/lib/types/host/common/account-pool.d.ts.map +1 -1
- package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
- package/lib/types/host/kimi-code/mapper.d.ts +13 -7
- package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
- package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
- package/lib/types/host/kimi-code/token-store.d.ts +11 -1
- package/lib/types/host/kimi-code/token-store.d.ts.map +1 -1
- package/lib/types/host/kimi-code/types.d.ts +13 -0
- package/lib/types/host/kimi-code/types.d.ts.map +1 -1
- package/lib/types/host/minimax-code/account-pool.d.ts +2 -2
- package/lib/types/host/minimax-code/account-pool.d.ts.map +1 -1
- package/lib/types/host/model-catalog.d.ts +3 -2
- package/lib/types/host/model-catalog.d.ts.map +1 -1
- package/lib/types/host/oauth-service.d.ts.map +1 -1
- package/lib/types/host/reasoning-collapse-guard/index.d.ts +26 -11
- package/lib/types/host/reasoning-collapse-guard/index.d.ts.map +1 -1
- package/lib/types/host/responses-client.d.ts +24 -0
- package/lib/types/host/responses-client.d.ts.map +1 -1
- package/lib/types/host/responses-mapper.d.ts +5 -1
- package/lib/types/host/responses-mapper.d.ts.map +1 -1
- package/lib/types/host/wire-auth.d.ts +14 -1
- package/lib/types/host/wire-auth.d.ts.map +1 -1
- package/lib/types/host/workbuddy/account-pool.d.ts +8 -8
- package/lib/types/host/workbuddy/account-pool.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/claude-contracts.d.ts +19 -0
- package/lib/types/shared/claude-contracts.d.ts.map +1 -1
- package/lib/types/shared/kimi-code-contracts.d.ts +16 -0
- package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -2,13 +2,13 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import * as LlmModule from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { HarnessError, LlmAdapter, LlmError, LlmError as LlmError$1, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import fs, { chmod, lstat, mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
|
|
5
6
|
import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
7
|
import os, { homedir } from "node:os";
|
|
7
8
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
9
|
import { isDeepStrictEqual } from "node:util";
|
|
9
10
|
import { execSync, spawn } from "node:child_process";
|
|
10
|
-
import
|
|
11
|
-
import fs, { appendFileSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
11
|
+
import fs$1, { appendFileSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
12
12
|
import http, { createServer } from "node:http";
|
|
13
13
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
14
14
|
import { lookup } from "node:dns/promises";
|
|
@@ -60,7 +60,23 @@ const OAUTH_CALLBACK_HOST = "localhost";
|
|
|
60
60
|
const OAUTH_CALLBACK_PORT = 1455;
|
|
61
61
|
const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}/auth/callback`;
|
|
62
62
|
const OAUTH_SCOPE = "openid profile email offline_access";
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The single `originator` this line presents to the Codex backend on every
|
|
65
|
+
* request, and the one OAuth sends during sign-in.
|
|
66
|
+
*
|
|
67
|
+
* This used to be three different values - `opencode` for chat and sign-in,
|
|
68
|
+
* `pi` for the image and search endpoints - which was an archaeological record
|
|
69
|
+
* of when each endpoint was reverse-engineered, not a decision. The Codex
|
|
70
|
+
* backend keys behaviour off this value (`codex_cli_rs` is what the official
|
|
71
|
+
* CLI sends) and treats it as one client identity, so splitting it across three
|
|
72
|
+
* files meant three unrelated failure signatures for one account. Both spellings
|
|
73
|
+
* are known-good against the backend; `opencode` is kept because it is the value
|
|
74
|
+
* the OAuth flow has always presented, and changing the sign-in originator is a
|
|
75
|
+
* different risk from changing the request originator.
|
|
76
|
+
*/
|
|
77
|
+
const CODEX_ORIGINATOR = "opencode";
|
|
78
|
+
/** OAuth presents the same identity the request path does (see above). */
|
|
79
|
+
const OAUTH_ORIGINATOR = CODEX_ORIGINATOR;
|
|
64
80
|
const TOKEN_REFRESH_MARGIN_MS = 6e4;
|
|
65
81
|
const ROUTE_PREFIX$4 = "/api/dsh-chatgpt-subscription";
|
|
66
82
|
const PLUGIN_VERSION = "0.1.0-alpha.0";
|
|
@@ -70,9 +86,18 @@ const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
|
|
|
70
86
|
const CODEX_IMAGE_GENERATION_URL = `${CODEX_API_BASE}/images/generations`;
|
|
71
87
|
const CODEX_SEARCH_URL = `${CODEX_API_BASE}/alpha/search`;
|
|
72
88
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
89
|
+
const CODEX_MODELS_URL = `${CODEX_API_BASE}/models?client_version=0.99.0`;
|
|
90
|
+
/**
|
|
91
|
+
* Beta gate the Codex backend expects on every responses request.
|
|
92
|
+
*
|
|
93
|
+
* The subscription backend is served under a beta flag, not as stable API:
|
|
94
|
+
* without this header the endpoint is a different (and, on older builds,
|
|
95
|
+
* rejected) surface. Kept as a single constant so the value is reviewable in
|
|
96
|
+
* one place with the rest of the wire contract in `wire-auth.ts`.
|
|
97
|
+
*/
|
|
98
|
+
const CODEX_OPENAI_BETA = "responses=experimental";
|
|
73
99
|
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
74
100
|
const CODEX_RESET_CREDITS_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
|
|
75
|
-
const CODEX_ORIGINATOR = "opencode";
|
|
76
101
|
const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
|
|
77
102
|
const CODEX_IMAGE_MODEL = "gpt-image-2";
|
|
78
103
|
const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
|
|
@@ -227,6 +252,9 @@ function reasoningEffortsForModel(model) {
|
|
|
227
252
|
if (profile === "gpt-6") return GPT_6_REASONING_EFFORTS;
|
|
228
253
|
return profile === "gpt-5.6" ? GPT_56_REASONING_EFFORTS : STANDARD_REASONING_EFFORTS;
|
|
229
254
|
}
|
|
255
|
+
function isCodexReasoningEffort(value) {
|
|
256
|
+
return typeof value === "string" && GPT_56_REASONING_EFFORTS.some((effort) => effort === value);
|
|
257
|
+
}
|
|
230
258
|
function isCodexModelId(model) {
|
|
231
259
|
return typeof model === "string" && CODEX_MODEL_CATALOG.some((entry) => entry.id === model);
|
|
232
260
|
}
|
|
@@ -270,28 +298,40 @@ function resolveCodexFallbackModel(model) {
|
|
|
270
298
|
//#region src/host/model-catalog.ts
|
|
271
299
|
const PROVIDER_ID$6 = CODEX_CHATGPT_PROVIDER_ID;
|
|
272
300
|
const PROVIDER_NAME$6 = "Codex(ChatGPT 订阅)";
|
|
273
|
-
function listCodexModels(preferences) {
|
|
301
|
+
function listCodexModels(preferences, live) {
|
|
274
302
|
const status = preferences?.status();
|
|
275
303
|
if (status?.enabled === false) return [];
|
|
276
|
-
const
|
|
277
|
-
|
|
304
|
+
const source = live !== void 0 && live.length > 0 ? live.map((entry) => ({
|
|
305
|
+
id: entry.id,
|
|
306
|
+
name: entry.name,
|
|
307
|
+
inputModalities: entry.inputModalities
|
|
308
|
+
})) : CODEX_MODEL_CATALOG.map((entry) => ({
|
|
309
|
+
id: entry.id,
|
|
310
|
+
name: entry.name,
|
|
311
|
+
inputModalities: entry.inputModalities
|
|
312
|
+
}));
|
|
313
|
+
const visible = new Set(status?.visibleModelIds ?? source.map((entry) => entry.id));
|
|
314
|
+
return source.filter((entry) => visible.has(entry.id)).map((entry) => ({
|
|
278
315
|
provider: PROVIDER_ID$6,
|
|
279
316
|
id: entry.id,
|
|
280
317
|
name: entry.name,
|
|
281
318
|
inputModalities: [...entry.inputModalities]
|
|
282
319
|
}));
|
|
283
320
|
}
|
|
284
|
-
function resolveCodexModel(model, preferences) {
|
|
321
|
+
function resolveCodexModel(model, preferences, live) {
|
|
285
322
|
const entry = resolveCodexCatalogEntry(model);
|
|
323
|
+
const liveEntry = live?.find((candidate) => candidate.id === model);
|
|
286
324
|
const configuredContextWindow = (preferences?.status())?.contextWindowOverrides[model];
|
|
287
|
-
const
|
|
288
|
-
const
|
|
325
|
+
const listedEfforts = liveEntry?.reasoningEfforts?.filter(isCodexReasoningEffort);
|
|
326
|
+
const efforts = listedEfforts !== void 0 && listedEfforts.length > 0 ? listedEfforts : reasoningEffortsForModel(model);
|
|
327
|
+
const statedDefault = liveEntry?.defaultReasoningEffort ?? entry.defaultReasoningEffort;
|
|
328
|
+
const defaultEffort = isCodexReasoningEffort(statedDefault) && efforts.includes(statedDefault) ? statedDefault : efforts[0];
|
|
289
329
|
return {
|
|
290
330
|
provider: PROVIDER_ID$6,
|
|
291
331
|
id: model,
|
|
292
|
-
name: entry.id === model ? entry.name : model,
|
|
293
|
-
inputModalities: [...entry.inputModalities],
|
|
294
|
-
context: { contextWindow: configuredContextWindow ?? entry.contextWindow },
|
|
332
|
+
name: liveEntry?.name ?? (entry.id === model ? entry.name : model),
|
|
333
|
+
inputModalities: [...liveEntry?.inputModalities ?? entry.inputModalities],
|
|
334
|
+
context: { contextWindow: configuredContextWindow ?? liveEntry?.contextWindow ?? entry.contextWindow },
|
|
295
335
|
defaultMaxTokens: codexModelMaxTokens(model),
|
|
296
336
|
reasoning: {
|
|
297
337
|
efforts: efforts.map((effort) => ({
|
|
@@ -324,10 +364,12 @@ const RETRY_POLICY$5 = resolveRetryPolicy({
|
|
|
324
364
|
var CodexChatGptAdapter = class extends LlmAdapter {
|
|
325
365
|
client;
|
|
326
366
|
preferences;
|
|
327
|
-
|
|
367
|
+
loadCatalog;
|
|
368
|
+
constructor(client, preferences, loadCatalog) {
|
|
328
369
|
super();
|
|
329
370
|
this.client = client;
|
|
330
371
|
this.preferences = preferences;
|
|
372
|
+
this.loadCatalog = loadCatalog;
|
|
331
373
|
}
|
|
332
374
|
providerInfo(provider) {
|
|
333
375
|
return {
|
|
@@ -340,10 +382,21 @@ var CodexChatGptAdapter = class extends LlmAdapter {
|
|
|
340
382
|
}
|
|
341
383
|
imageRequestPricing(_provider, _model) {}
|
|
342
384
|
async listModels() {
|
|
343
|
-
return listCodexModels(this.preferences);
|
|
385
|
+
return listCodexModels(this.preferences, await this.catalog());
|
|
344
386
|
}
|
|
345
387
|
async resolveModel(_provider, model, signal) {
|
|
346
|
-
return resolveCodexModel(model, this.preferences);
|
|
388
|
+
return resolveCodexModel(model, this.preferences, await this.catalog(signal));
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The live listing, or undefined when this line has no way to fetch one.
|
|
392
|
+
*
|
|
393
|
+
* Never throws: a listing failure must not become a failed picker or a
|
|
394
|
+
* rejected model resolution. `loadCodexCatalog` already answers with the
|
|
395
|
+
* shipped table on failure, so the catch here only covers a throwing loader.
|
|
396
|
+
*/
|
|
397
|
+
async catalog(signal) {
|
|
398
|
+
if (this.loadCatalog === void 0) return void 0;
|
|
399
|
+
return this.loadCatalog({ signal }).catch(() => void 0);
|
|
347
400
|
}
|
|
348
401
|
async prepareCall(provider, model, signal) {
|
|
349
402
|
return {
|
|
@@ -367,6 +420,347 @@ function dshHomeDir() {
|
|
|
367
420
|
return process.env.DSH_HOME?.trim() || path.join(os.homedir(), ".dsh");
|
|
368
421
|
}
|
|
369
422
|
//#endregion
|
|
423
|
+
//#region src/host/common/catalog-snapshot.ts
|
|
424
|
+
/**
|
|
425
|
+
* Persistent snapshot for one provider's live model catalog.
|
|
426
|
+
*
|
|
427
|
+
* The in-memory catalog caches keep a process alive between requests, but they
|
|
428
|
+
* start cold: on the first call after a restart (or a plugin reinstall) the
|
|
429
|
+
* picker and the settings page both wait on the upstream listing before they
|
|
430
|
+
* can render anything, and an unreachable endpoint turns that wait into the
|
|
431
|
+
* full discovery timeout — "the model server is unavailable" on a fresh
|
|
432
|
+
* install. The catalog is not fast-moving data, so the last successful listing
|
|
433
|
+
* is persisted to `<DSH_HOME>/storages/` and rehydrated into the in-memory
|
|
434
|
+
* cache on first use. A snapshot the caller can read is therefore available
|
|
435
|
+
* from the very first call, with no network in the path.
|
|
436
|
+
*
|
|
437
|
+
* Writes are best-effort: persistence is an optimization, never a capability.
|
|
438
|
+
* A read-only home or a locked file must not fail the fetch that produced the
|
|
439
|
+
* snapshot.
|
|
440
|
+
*/
|
|
441
|
+
/** File name of the persisted snapshot for one catalog. */
|
|
442
|
+
function catalogSnapshotPath(name) {
|
|
443
|
+
return path.join(dshHomeDir(), "storages", `${name}-catalog.json`);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Snapshot name for one catalog *and* one credential scope.
|
|
447
|
+
*
|
|
448
|
+
* A persisted snapshot answers before any credential is read — that is the whole
|
|
449
|
+
* point of it — so the file itself has to carry the scope it was fetched under.
|
|
450
|
+
* Otherwise the first call after a restart, for any account, is answered by
|
|
451
|
+
* whatever listing some other scope happened to persist last: a Kimi listing
|
|
452
|
+
* fetched for the mainland-cn endpoint would answer a global caller with models
|
|
453
|
+
* and context windows that region does not serve, and a Command Code listing
|
|
454
|
+
* fetched against one API environment would answer the other.
|
|
455
|
+
*
|
|
456
|
+
* The scope is part of the file name rather than of the payload because a caller
|
|
457
|
+
* that cannot name the account — the harness resolves this catalog with no token
|
|
458
|
+
* at all — still has to find the snapshot its own scope owns.
|
|
459
|
+
*
|
|
460
|
+
* Everything outside `[a-z0-9]` is folded to `_`, so no scope can escape the
|
|
461
|
+
* `storages` directory.
|
|
462
|
+
*/
|
|
463
|
+
function catalogSnapshotName(base, scope) {
|
|
464
|
+
const normalized = scope.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
465
|
+
return normalized === "" ? base : `${base}-${normalized}`;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Read the persisted snapshot for one catalog.
|
|
469
|
+
*
|
|
470
|
+
* Shape errors and unreadable files both return `undefined` — the caller keeps
|
|
471
|
+
* its in-memory state, and the next successful fetch overwrites the file.
|
|
472
|
+
*/
|
|
473
|
+
async function readCatalogSnapshot(name, parseModels) {
|
|
474
|
+
try {
|
|
475
|
+
const content = await fs.readFile(catalogSnapshotPath(name), "utf8");
|
|
476
|
+
const parsed = JSON.parse(content);
|
|
477
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
478
|
+
const record = parsed;
|
|
479
|
+
const fetchedAt = record.fetchedAt;
|
|
480
|
+
if (typeof fetchedAt !== "number" || !Number.isFinite(fetchedAt) || fetchedAt <= 0) return void 0;
|
|
481
|
+
const models = parseModels(record.models);
|
|
482
|
+
if (models === void 0 || models.length === 0) return void 0;
|
|
483
|
+
return {
|
|
484
|
+
fetchedAt,
|
|
485
|
+
models
|
|
486
|
+
};
|
|
487
|
+
} catch {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Snapshot writes still in flight.
|
|
493
|
+
*
|
|
494
|
+
* Callers fire-and-forget these writes, so nothing in production ever awaits
|
|
495
|
+
* one. A test that deletes its private `storages` directory between cases
|
|
496
|
+
* cannot know when that is safe, and a write that lands after the delete
|
|
497
|
+
* recreates the file the next case asserts is absent — which is how a stale
|
|
498
|
+
* listing leaked into a later test and failed it on CI. Registering each write
|
|
499
|
+
* gives {@link flushCatalogSnapshots} a deterministic point to wait on.
|
|
500
|
+
*/
|
|
501
|
+
const pendingSnapshotWrites = /* @__PURE__ */ new Set();
|
|
502
|
+
/** Persist one snapshot; a failure is logged nowhere and never thrown. */
|
|
503
|
+
function writeCatalogSnapshot(name, models, fetchedAt) {
|
|
504
|
+
const write = (async () => {
|
|
505
|
+
try {
|
|
506
|
+
const filePath = catalogSnapshotPath(name);
|
|
507
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
508
|
+
const tmp = `${filePath}.tmp.${Date.now()}`;
|
|
509
|
+
await fs.writeFile(tmp, JSON.stringify({
|
|
510
|
+
fetchedAt,
|
|
511
|
+
models
|
|
512
|
+
}, null, 2), "utf8");
|
|
513
|
+
await fs.rename(tmp, filePath);
|
|
514
|
+
} catch {}
|
|
515
|
+
})();
|
|
516
|
+
pendingSnapshotWrites.add(write);
|
|
517
|
+
write.finally(() => {
|
|
518
|
+
pendingSnapshotWrites.delete(write);
|
|
519
|
+
});
|
|
520
|
+
return write;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Warm an in-memory cache from the persisted snapshot when it is fresher than
|
|
524
|
+
* what the cache currently holds. Called lazily on the first cache miss of a
|
|
525
|
+
* process, so a restart serves the previous listing without waiting on the
|
|
526
|
+
* network.
|
|
527
|
+
*
|
|
528
|
+
* @returns the snapshot's `fetchedAt`, or `0` when no usable snapshot exists —
|
|
529
|
+
* the caller then fetches as before.
|
|
530
|
+
*/
|
|
531
|
+
async function rehydrateCatalogCache(name, parseModels, accept, currentFetchedAt) {
|
|
532
|
+
const snapshot = await readCatalogSnapshot(name, parseModels);
|
|
533
|
+
if (snapshot === void 0 || snapshot.fetchedAt <= currentFetchedAt) return 0;
|
|
534
|
+
accept(snapshot.fetchedAt, snapshot.models);
|
|
535
|
+
return snapshot.fetchedAt;
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/host/wire-auth.ts
|
|
539
|
+
/**
|
|
540
|
+
* Turn-state header the Codex backend hands back on a responses response.
|
|
541
|
+
*
|
|
542
|
+
* The official CLI reads it and echoes it on the next request of the same
|
|
543
|
+
* conversation; the backend uses it to resume instead of re-ingesting the
|
|
544
|
+
* whole history. It is opaque, short-lived and account-scoped, so it is
|
|
545
|
+
* threaded through rather than computed (see `responses-client.ts`).
|
|
546
|
+
*/
|
|
547
|
+
const CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
|
|
548
|
+
function codexHeaders(credentials, sessionId, options = {}) {
|
|
549
|
+
const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
|
|
550
|
+
return {
|
|
551
|
+
authorization: `Bearer ${credentials.accessToken}`,
|
|
552
|
+
...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
|
|
553
|
+
"openai-beta": CODEX_OPENAI_BETA,
|
|
554
|
+
originator: CODEX_ORIGINATOR,
|
|
555
|
+
"user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
|
|
556
|
+
...sessionId ? { "session-id": sessionId } : {},
|
|
557
|
+
...options.turnState ? { [CODEX_TURN_STATE_HEADER]: options.turnState } : {}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function stableSessionId(value) {
|
|
561
|
+
const source = value === void 0 || value === "" ? randomUUID() : value;
|
|
562
|
+
return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
|
|
563
|
+
}
|
|
564
|
+
function retryAfterMs$1(headers) {
|
|
565
|
+
const raw = headers.get("retry-after");
|
|
566
|
+
if (raw === null) return void 0;
|
|
567
|
+
const seconds = Number(raw);
|
|
568
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
|
|
569
|
+
const timestamp = Date.parse(raw);
|
|
570
|
+
if (!Number.isFinite(timestamp)) return void 0;
|
|
571
|
+
return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
|
|
572
|
+
}
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region src/host/codex-catalog.ts
|
|
575
|
+
/** How long a live listing is reused before the server is asked again. */
|
|
576
|
+
const CATALOG_TTL_MS = 15 * 6e4;
|
|
577
|
+
/**
|
|
578
|
+
* Snapshot scope.
|
|
579
|
+
*
|
|
580
|
+
* The Codex listing is account-scoped (each plan sees its own models), so the
|
|
581
|
+
* persisted snapshot is filed under the account it was fetched for. Without the
|
|
582
|
+
* scope a rehydrated listing from one account would answer another's picker.
|
|
583
|
+
*/
|
|
584
|
+
const SNAPSHOT_SCOPE = "chatgpt-codex";
|
|
585
|
+
let catalogCache$2 = null;
|
|
586
|
+
const catalogInFlight$3 = /* @__PURE__ */ new Map();
|
|
587
|
+
let snapshotRehydrated = null;
|
|
588
|
+
/**
|
|
589
|
+
* The listing currently held, or undefined before the first load.
|
|
590
|
+
*
|
|
591
|
+
* Callers that need a catalog with no credential read the shipped table
|
|
592
|
+
* directly; returning it here under a different entry type would force every
|
|
593
|
+
* one of them to convert it.
|
|
594
|
+
*/
|
|
595
|
+
function getCachedCatalog() {
|
|
596
|
+
return catalogCache$2?.models;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Read the models this subscription can call.
|
|
600
|
+
*
|
|
601
|
+
* The shipped table in `shared/model-catalog.ts` stays the floor: it is what
|
|
602
|
+
* answers a picker before the first sign-in, and what a failed listing falls
|
|
603
|
+
* back to. The subscription listing is the authority once one is available,
|
|
604
|
+
* because only the backend knows which models a plan currently serves and at
|
|
605
|
+
* what context window — the reason subscription-only models exist at all.
|
|
606
|
+
*
|
|
607
|
+
* Single-flighted per account, because the harness resolves every model of every
|
|
608
|
+
* provider when it builds the picker; without that, one picker build is one
|
|
609
|
+
* round trip per model.
|
|
610
|
+
*/
|
|
611
|
+
function loadCodexCatalog(credentials, options = {}) {
|
|
612
|
+
const key = accountKeyFor$1(credentials);
|
|
613
|
+
if (options.force !== true) {
|
|
614
|
+
const cached = catalogCache$2;
|
|
615
|
+
if (cached !== null && cached.key === key && Date.now() - cached.at < CATALOG_TTL_MS) return Promise.resolve(cached.models);
|
|
616
|
+
}
|
|
617
|
+
const pending = catalogInFlight$3.get(key);
|
|
618
|
+
if (pending !== void 0) return pending;
|
|
619
|
+
const run = performCatalogLoad$1(credentials, options).then((models) => {
|
|
620
|
+
catalogInFlight$3.delete(key);
|
|
621
|
+
return models;
|
|
622
|
+
}, (error) => {
|
|
623
|
+
catalogInFlight$3.delete(key);
|
|
624
|
+
throw error;
|
|
625
|
+
});
|
|
626
|
+
catalogInFlight$3.set(key, run);
|
|
627
|
+
return run;
|
|
628
|
+
}
|
|
629
|
+
async function performCatalogLoad$1(credentials, options) {
|
|
630
|
+
const key = accountKeyFor$1(credentials);
|
|
631
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
632
|
+
snapshotRehydrated ??= rehydrateSnapshot(key);
|
|
633
|
+
await snapshotRehydrated;
|
|
634
|
+
const cached = catalogCache$2;
|
|
635
|
+
if (cached !== null && cached.key === key && Date.now() - cached.at < CATALOG_TTL_MS) return cached.models;
|
|
636
|
+
let models;
|
|
637
|
+
let live = false;
|
|
638
|
+
try {
|
|
639
|
+
const response = await fetchFn(CODEX_MODELS_URL, {
|
|
640
|
+
method: "GET",
|
|
641
|
+
headers: {
|
|
642
|
+
...codexHeaders(credentials),
|
|
643
|
+
accept: "application/json"
|
|
644
|
+
},
|
|
645
|
+
signal: options.signal
|
|
646
|
+
});
|
|
647
|
+
if (!response.ok) throw new Error(`Codex model listing failed (${response.status}).`);
|
|
648
|
+
const listing = parseListing(await response.json().catch(() => void 0));
|
|
649
|
+
if (listing.length === 0) throw new Error("Codex model listing named no models this line understands.");
|
|
650
|
+
models = listing;
|
|
651
|
+
live = true;
|
|
652
|
+
writeCatalogSnapshot(catalogSnapshotName(SNAPSHOT_SCOPE, key), [...models], Date.now());
|
|
653
|
+
} catch {
|
|
654
|
+
models = getCachedCatalog() ?? [];
|
|
655
|
+
live = false;
|
|
656
|
+
}
|
|
657
|
+
catalogCache$2 = {
|
|
658
|
+
at: Date.now(),
|
|
659
|
+
models,
|
|
660
|
+
key,
|
|
661
|
+
live
|
|
662
|
+
};
|
|
663
|
+
return models;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Seed the in-memory cache from the last persisted listing.
|
|
667
|
+
*
|
|
668
|
+
* Best effort by construction: a missing or malformed snapshot leaves the cache
|
|
669
|
+
* cold, and the caller then simply makes the network call.
|
|
670
|
+
*/
|
|
671
|
+
async function rehydrateSnapshot(key) {
|
|
672
|
+
const snapshot = await readCatalogSnapshot(catalogSnapshotName(SNAPSHOT_SCOPE, key), (value) => Array.isArray(value) ? value.flatMap((item) => parseListingEntry$1(item)) : void 0).catch(() => void 0);
|
|
673
|
+
if (snapshot === void 0 || snapshot.models.length === 0) return;
|
|
674
|
+
if (catalogCache$2 !== null) return;
|
|
675
|
+
catalogCache$2 = {
|
|
676
|
+
at: snapshot.fetchedAt,
|
|
677
|
+
models: snapshot.models,
|
|
678
|
+
key,
|
|
679
|
+
live: true
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Read the listing payload.
|
|
684
|
+
*
|
|
685
|
+
* The documented shape is `{ models: [...] }`, but a bare array is accepted too
|
|
686
|
+
* so a backend that flattens the envelope does not read as an empty catalog.
|
|
687
|
+
*/
|
|
688
|
+
function parseListing(payload) {
|
|
689
|
+
const record = asRecord$10(payload);
|
|
690
|
+
const models = Array.isArray(payload) ? payload : record?.models;
|
|
691
|
+
if (!Array.isArray(models)) return [];
|
|
692
|
+
return models.flatMap((item) => parseListingEntry$1(item));
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* One listing entry.
|
|
696
|
+
*
|
|
697
|
+
* Only fields the backend actually documents are read. A model id this line has
|
|
698
|
+
* never heard of still produces an entry, because the listing is the authority
|
|
699
|
+
* on what the account may call — but every capability it does not state falls
|
|
700
|
+
* back to a conservative answer rather than to a guess.
|
|
701
|
+
*/
|
|
702
|
+
function parseListingEntry$1(value) {
|
|
703
|
+
const record = asRecord$10(value);
|
|
704
|
+
if (record === void 0) return [];
|
|
705
|
+
const id = asString$14(record.slug) ?? asString$14(record.id);
|
|
706
|
+
if (id === void 0) return [];
|
|
707
|
+
const contextWindow = asNumber$4(record.context_window) ?? asNumber$4(record.contextWindow);
|
|
708
|
+
const modalities = parseModalities(record.input_modalities);
|
|
709
|
+
const efforts = parseEfforts(record.supported_reasoning_levels);
|
|
710
|
+
const fallback = CODEX_MODEL_CATALOG.find((entry) => entry.id === id);
|
|
711
|
+
return [{
|
|
712
|
+
id,
|
|
713
|
+
name: asString$14(record.display_name) ?? asString$14(record.displayName) ?? fallback?.name ?? id,
|
|
714
|
+
contextWindow: contextWindow !== void 0 && contextWindow > 0 ? contextWindow : null,
|
|
715
|
+
inputModalities: modalities ?? (fallback ? [...fallback.inputModalities] : ["text"]),
|
|
716
|
+
...efforts !== null ? { reasoningEfforts: efforts } : {},
|
|
717
|
+
defaultReasoningEffort: asString$14(record.default_reasoning_level) ?? fallback?.defaultReasoningEffort ?? null
|
|
718
|
+
}];
|
|
719
|
+
}
|
|
720
|
+
function parseModalities(value) {
|
|
721
|
+
if (!Array.isArray(value)) return null;
|
|
722
|
+
const out = [];
|
|
723
|
+
for (const item of value) {
|
|
724
|
+
const name = asString$14(item);
|
|
725
|
+
if (name === "text" || name === "image") out.push(name);
|
|
726
|
+
}
|
|
727
|
+
return out.length > 0 ? out : null;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Reasoning levels the listing states, in the order it stated them.
|
|
731
|
+
*
|
|
732
|
+
* The backend writes each level as `{ "effort": "high" }`; a bare string is
|
|
733
|
+
* accepted too, since that is the other spelling seen in the wild.
|
|
734
|
+
*/
|
|
735
|
+
function parseEfforts(value) {
|
|
736
|
+
if (!Array.isArray(value)) return null;
|
|
737
|
+
const out = [];
|
|
738
|
+
for (const item of value) {
|
|
739
|
+
const effort = asString$14(item) ?? asString$14(asRecord$10(item)?.effort);
|
|
740
|
+
if (effort !== void 0 && !out.includes(effort)) out.push(effort);
|
|
741
|
+
}
|
|
742
|
+
return out.length > 0 ? out : null;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Cache identity for one credential.
|
|
746
|
+
*
|
|
747
|
+
* The account id is the entitlement boundary: two accounts of the same user can
|
|
748
|
+
* see different listings, and a pool rotates between them, so the key has to
|
|
749
|
+
* follow the account rather than the person.
|
|
750
|
+
*/
|
|
751
|
+
function accountKeyFor$1(credentials) {
|
|
752
|
+
return credentials.accountId || "default";
|
|
753
|
+
}
|
|
754
|
+
function asRecord$10(value) {
|
|
755
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
756
|
+
}
|
|
757
|
+
function asString$14(value) {
|
|
758
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
759
|
+
}
|
|
760
|
+
function asNumber$4(value) {
|
|
761
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
762
|
+
}
|
|
763
|
+
//#endregion
|
|
370
764
|
//#region src/host/common/credential-read-cache.ts
|
|
371
765
|
/**
|
|
372
766
|
* Identity a file-backed credential store reports when nothing is stored.
|
|
@@ -905,6 +1299,16 @@ function poolCredentialAccount$1(filePath) {
|
|
|
905
1299
|
return createHash("sha256").update(path.resolve(filePath)).digest("hex");
|
|
906
1300
|
}
|
|
907
1301
|
const poolOperations = /* @__PURE__ */ new Map();
|
|
1302
|
+
const poolIdentityRevisions = /* @__PURE__ */ new Map();
|
|
1303
|
+
const poolRefreshes = /* @__PURE__ */ new Map();
|
|
1304
|
+
/**
|
|
1305
|
+
* A selected row was deleted, or became unusable, while a caller awaited it.
|
|
1306
|
+
*
|
|
1307
|
+
* Distinct from every other credential error because a MODEL REQUEST can still
|
|
1308
|
+
* be served by another account: routing excludes the row and tries the next
|
|
1309
|
+
* one, while a caller that named the account still gets the plain refusal.
|
|
1310
|
+
*/
|
|
1311
|
+
var PoolAccountUnavailableError = class extends LlmError {};
|
|
908
1312
|
/**
|
|
909
1313
|
* The shared multi-account pool: encrypted storage, eligibility filtering,
|
|
910
1314
|
* rotation strategy, 429 cooldowns and per-account auth failures.
|
|
@@ -926,11 +1330,14 @@ var AccountPoolCore = class {
|
|
|
926
1330
|
* can therefore invalidate on identity change without paying a credential read
|
|
927
1331
|
* to discover it.
|
|
928
1332
|
*/
|
|
929
|
-
|
|
1333
|
+
/** Pool-file identity of this instance: locks, revisions and flights key on it. */
|
|
1334
|
+
operationKey;
|
|
930
1335
|
constructor(hooks) {
|
|
931
1336
|
this.hooks = hooks;
|
|
932
1337
|
this.filePath = hooks.poolFile;
|
|
933
1338
|
this.backend = hooks.backend ?? createPoolBackend(hooks);
|
|
1339
|
+
const resolved = path.resolve(this.filePath);
|
|
1340
|
+
this.operationKey = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
934
1341
|
}
|
|
935
1342
|
/**
|
|
936
1343
|
* A cheap, in-memory revision of the pool's serving identity.
|
|
@@ -946,15 +1353,21 @@ var AccountPoolCore = class {
|
|
|
946
1353
|
* would report one account's usage under another account's name.
|
|
947
1354
|
*/
|
|
948
1355
|
currentIdentityRevision() {
|
|
949
|
-
return this.
|
|
1356
|
+
return poolIdentityRevisions.get(this.operationKey) ?? 0;
|
|
950
1357
|
}
|
|
951
1358
|
/** Human description of where this pool's credentials live. */
|
|
952
1359
|
path() {
|
|
953
1360
|
if (process.platform === "win32") return `${this.filePath}.dpapi`;
|
|
954
1361
|
return `${process.platform === "darwin" ? "Keychain" : "Secret Service"}: ${this.hooks.keychainService}/${poolCredentialAccount$1(this.filePath)}`;
|
|
955
1362
|
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Queue one storage transaction for this pool file.
|
|
1365
|
+
*
|
|
1366
|
+
* NOT reentrant: a transaction must use {@link loadPoolData} and
|
|
1367
|
+
* {@link updatePool} rather than calling the public read/write methods.
|
|
1368
|
+
*/
|
|
956
1369
|
serialize(operation) {
|
|
957
|
-
const key =
|
|
1370
|
+
const key = this.operationKey;
|
|
958
1371
|
const result = (poolOperations.get(key) || Promise.resolve()).then(operation);
|
|
959
1372
|
const settled = result.then(() => void 0, () => void 0);
|
|
960
1373
|
poolOperations.set(key, settled);
|
|
@@ -978,31 +1391,74 @@ var AccountPoolCore = class {
|
|
|
978
1391
|
* getter write to disk.
|
|
979
1392
|
*/
|
|
980
1393
|
read() {
|
|
981
|
-
return this.serialize(
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1394
|
+
return this.serialize(() => this.loadPoolData());
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Load the pool without holding the file lock.
|
|
1398
|
+
*
|
|
1399
|
+
* Providers override this to maintain their own in-memory indexes, so every
|
|
1400
|
+
* load — including one inside {@link updatePool} — refreshes them.
|
|
1401
|
+
*/
|
|
1402
|
+
async loadPoolData() {
|
|
1403
|
+
let current = null;
|
|
1404
|
+
try {
|
|
1405
|
+
current = await this.backend.load();
|
|
1406
|
+
} catch {}
|
|
1407
|
+
if (current !== null && Array.isArray(current.accounts) && current.accounts.length > 0) return current;
|
|
1408
|
+
try {
|
|
1409
|
+
const legacy = await this.hooks.legacyAccount?.();
|
|
1410
|
+
if (legacy) return {
|
|
997
1411
|
version: 1,
|
|
998
|
-
|
|
999
|
-
|
|
1412
|
+
activeAccountId: legacy.id,
|
|
1413
|
+
rotationStrategy: current?.rotationStrategy || "sequential",
|
|
1414
|
+
accounts: [legacy]
|
|
1000
1415
|
};
|
|
1001
|
-
}
|
|
1416
|
+
} catch {}
|
|
1417
|
+
return current || {
|
|
1418
|
+
version: 1,
|
|
1419
|
+
rotationStrategy: "sequential",
|
|
1420
|
+
accounts: []
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
async persistPoolData(data) {
|
|
1424
|
+
poolIdentityRevisions.set(this.operationKey, this.currentIdentityRevision() + 1);
|
|
1425
|
+
await this.saveVerified(data);
|
|
1002
1426
|
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Replace the whole document (initialization, import), NOT a read-modify-write.
|
|
1429
|
+
* Prefer {@link updatePool} for mutations so the draft is read under the lock.
|
|
1430
|
+
*/
|
|
1003
1431
|
write(data) {
|
|
1004
|
-
this.
|
|
1005
|
-
|
|
1432
|
+
return this.serialize(() => this.persistPoolData(data));
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Read the LATEST document, mutate it and persist it as one file transaction.
|
|
1436
|
+
*
|
|
1437
|
+
* This is the whole fix for the read-modify-write race: the old shape read a
|
|
1438
|
+
* snapshot, awaited (a token refresh, a settings write, an encrypted round
|
|
1439
|
+
* trip) and then wrote that whole stale snapshot back, erasing whatever landed
|
|
1440
|
+
* in between — including another account's freshly rotated token.
|
|
1441
|
+
*
|
|
1442
|
+
* The callback runs under the file lock, so it must not call read, write or
|
|
1443
|
+
* updatePool (the lock is not reentrant) and must not do network I/O.
|
|
1444
|
+
* Provider-local stores belong in `afterCommit`, which runs only once the
|
|
1445
|
+
* pool write is verified, and must not re-enter the pool either.
|
|
1446
|
+
*/
|
|
1447
|
+
updatePool(mutate, afterCommit) {
|
|
1448
|
+
return this.serialize(async () => {
|
|
1449
|
+
const data = await this.loadPoolData();
|
|
1450
|
+
const before = structuredClone(data);
|
|
1451
|
+
const result = await mutate(data);
|
|
1452
|
+
if (!isDeepStrictEqual(before, data)) {
|
|
1453
|
+
await this.persistPoolData(data);
|
|
1454
|
+
await afterCommit?.(data, result).catch(() => void 0);
|
|
1455
|
+
for (const account of data.accounts) if (!isDeepStrictEqual(before.accounts.find((entry) => entry.id === account.id)?.credentials, account.credentials)) await this.hooks.credentialsCommitted?.(account).catch(() => void 0);
|
|
1456
|
+
const previousPrimary = before.accounts.find((account) => account.isPrimary);
|
|
1457
|
+
const primary = data.accounts.find((account) => account.isPrimary);
|
|
1458
|
+
if (previousPrimary?.id !== primary?.id || !isDeepStrictEqual(previousPrimary?.credentials, primary?.credentials)) await this.hooks.mirrorPrimary?.(primary?.credentials ?? null).catch(() => void 0);
|
|
1459
|
+
}
|
|
1460
|
+
return result;
|
|
1461
|
+
});
|
|
1006
1462
|
}
|
|
1007
1463
|
/** Public summaries, with cooldowns that already expired reported as absent. */
|
|
1008
1464
|
async listAccounts() {
|
|
@@ -1031,8 +1487,17 @@ var AccountPoolCore = class {
|
|
|
1031
1487
|
return this.hooks.extendSummary ? this.hooks.extendSummary(account, summary, now) : summary;
|
|
1032
1488
|
}
|
|
1033
1489
|
/** Add or re-authorize one account; an existing dedupe key updates in place. */
|
|
1034
|
-
|
|
1035
|
-
|
|
1490
|
+
addAccount(credentials, alias) {
|
|
1491
|
+
return this.updatePool((data) => this.addAccountToPool(data, credentials, alias));
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Add or re-authorize inside an existing transaction.
|
|
1495
|
+
*
|
|
1496
|
+
* Capacity, dedupe and the primary decision are all taken against the live
|
|
1497
|
+
* document, so two simultaneous sign-ins cannot each pass a check the other
|
|
1498
|
+
* has already invalidated.
|
|
1499
|
+
*/
|
|
1500
|
+
addAccountToPool(data, credentials, alias) {
|
|
1036
1501
|
const key = this.hooks.dedupeKey?.(credentials);
|
|
1037
1502
|
const existingIndex = key === void 0 ? -1 : data.accounts.findIndex((account) => this.hooks.dedupeKey?.(account.credentials) === key);
|
|
1038
1503
|
const maxAccounts = this.hooks.maxAccounts ?? 20;
|
|
@@ -1058,77 +1523,79 @@ var AccountPoolCore = class {
|
|
|
1058
1523
|
if (existingIndex >= 0) data.accounts[existingIndex] = account;
|
|
1059
1524
|
else data.accounts.push(account);
|
|
1060
1525
|
if (!data.activeAccountId || account.isPrimary) data.activeAccountId = account.id;
|
|
1061
|
-
await this.write(data);
|
|
1062
|
-
if (account.isPrimary) this.hooks.mirrorPrimary?.(credentials).catch(() => void 0);
|
|
1063
1526
|
return account;
|
|
1064
1527
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
this.hooks.mirrorPrimary?.(primary.credentials).catch(() => void 0);
|
|
1072
|
-
}
|
|
1073
|
-
await this.write(data);
|
|
1528
|
+
setPrimary(accountId) {
|
|
1529
|
+
return this.updatePool((data) => {
|
|
1530
|
+
for (const account of data.accounts) account.isPrimary = account.id === accountId;
|
|
1531
|
+
const primary = data.accounts.find((account) => account.isPrimary);
|
|
1532
|
+
if (primary) data.activeAccountId = primary.id;
|
|
1533
|
+
});
|
|
1074
1534
|
}
|
|
1075
1535
|
/**
|
|
1076
1536
|
* Persist a refreshed credential for one account.
|
|
1077
1537
|
*
|
|
1538
|
+
* `expected` makes this a conditional write: when the stored credential is
|
|
1539
|
+
* no longer the one the caller started from, something newer won the race (a
|
|
1540
|
+
* re-login, a rotation committed while this call was in flight) and the
|
|
1541
|
+
* latest row is returned WITHOUT being overwritten. A deleted account is
|
|
1542
|
+
* never brought back.
|
|
1543
|
+
*
|
|
1078
1544
|
* Used by a forced refresh (a 401 mid-request) rather than by the rotating
|
|
1079
|
-
* getEffectiveAccount path, which
|
|
1545
|
+
* getEffectiveAccount path, which refreshes through its own single-flight.
|
|
1080
1546
|
*/
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (target.isPrimary) this.hooks.mirrorPrimary?.(credentials).catch(() => void 0);
|
|
1088
|
-
return target;
|
|
1547
|
+
updateAccountCredentials(accountId, credentials, expected) {
|
|
1548
|
+
return this.updatePool((data) => {
|
|
1549
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1550
|
+
if (target && (expected === void 0 || isDeepStrictEqual(target.credentials, expected))) target.credentials = credentials;
|
|
1551
|
+
return target;
|
|
1552
|
+
});
|
|
1089
1553
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
await this.write(data);
|
|
1096
|
-
}
|
|
1554
|
+
setAlias(accountId, alias) {
|
|
1555
|
+
return this.updatePool((data) => {
|
|
1556
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1557
|
+
if (target && alias.trim()) target.alias = alias.trim();
|
|
1558
|
+
});
|
|
1097
1559
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1560
|
+
deleteAccount(accountId) {
|
|
1561
|
+
return this.updatePool((data) => this.deleteAccountFromPool(data, accountId));
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Remove one account inside a transaction.
|
|
1565
|
+
*
|
|
1566
|
+
* Provider refusal rules (a snapshot this plugin does not own, a desktop
|
|
1567
|
+
* account it may only hide) belong in an override of this, so the check and
|
|
1568
|
+
* the removal cannot be separated by a concurrent change.
|
|
1569
|
+
*/
|
|
1570
|
+
deleteAccountFromPool(data, accountId) {
|
|
1100
1571
|
const wasPrimary = data.accounts.find((account) => account.id === accountId)?.isPrimary;
|
|
1101
1572
|
data.accounts = data.accounts.filter((account) => account.id !== accountId);
|
|
1102
|
-
if (wasPrimary && data.accounts.length > 0)
|
|
1103
|
-
data.accounts[0].isPrimary = true;
|
|
1104
|
-
this.hooks.mirrorPrimary?.(data.accounts[0].credentials).catch(() => void 0);
|
|
1105
|
-
} else if (data.accounts.length === 0) this.hooks.mirrorPrimary?.(null).catch(() => void 0);
|
|
1573
|
+
if (wasPrimary && data.accounts.length > 0) data.accounts[0].isPrimary = true;
|
|
1106
1574
|
if (data.activeAccountId === accountId) data.activeAccountId = data.accounts.find((account) => account.isPrimary)?.id ?? data.accounts[0]?.id;
|
|
1107
|
-
await this.write(data);
|
|
1108
1575
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1576
|
+
setStrategy(strategy) {
|
|
1577
|
+
return this.updatePool((data) => {
|
|
1578
|
+
data.rotationStrategy = strategy;
|
|
1579
|
+
});
|
|
1113
1580
|
}
|
|
1114
1581
|
/** Cool one account down after an upstream 429. */
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1582
|
+
markCooldown(accountId, durationMs, reason) {
|
|
1583
|
+
return this.updatePool((data) => {
|
|
1584
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1585
|
+
if (target) {
|
|
1586
|
+
target.cooldownUntil = Date.now() + Math.max(MIN_COOLDOWN_MS, durationMs);
|
|
1587
|
+
target.cooldownReason = reason;
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1123
1590
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1591
|
+
clearCooldown(accountId) {
|
|
1592
|
+
return this.updatePool((data) => {
|
|
1593
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1594
|
+
if (target) {
|
|
1595
|
+
target.cooldownUntil = void 0;
|
|
1596
|
+
target.cooldownReason = void 0;
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1132
1599
|
}
|
|
1133
1600
|
/**
|
|
1134
1601
|
* Record that an account can no longer authenticate.
|
|
@@ -1136,23 +1603,23 @@ var AccountPoolCore = class {
|
|
|
1136
1603
|
* The account stays in the pool: signing in again is what restores it, and a
|
|
1137
1604
|
* deleted account would take the user's alias and ordering with it.
|
|
1138
1605
|
*/
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1606
|
+
markAuthFailed(accountId, reason, status = "expired") {
|
|
1607
|
+
return this.updatePool((data) => {
|
|
1608
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1609
|
+
if (target) {
|
|
1610
|
+
target.authStatus = status;
|
|
1611
|
+
target.authFailedReason = reason;
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1147
1614
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1615
|
+
clearAuthFailed(accountId) {
|
|
1616
|
+
return this.updatePool((data) => {
|
|
1617
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
1618
|
+
if (target) {
|
|
1619
|
+
target.authStatus = void 0;
|
|
1620
|
+
target.authFailedReason = void 0;
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1156
1623
|
}
|
|
1157
1624
|
/** Whether an account may serve a request right now. */
|
|
1158
1625
|
isEligible(account, now, triedAccountIds) {
|
|
@@ -1192,24 +1659,16 @@ var AccountPoolCore = class {
|
|
|
1192
1659
|
*
|
|
1193
1660
|
* @param fetchFn - fetch used if the credential is close to expiry.
|
|
1194
1661
|
*/
|
|
1195
|
-
async getCredentialAccount(fetchFn = fetch) {
|
|
1662
|
+
async getCredentialAccount(fetchFn = fetch, forceRefresh = false) {
|
|
1196
1663
|
const data = await this.read();
|
|
1197
1664
|
if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`);
|
|
1198
|
-
const now = Date.now();
|
|
1199
1665
|
const usable = data.accounts.filter((account) => this.isCredentialUsable(account));
|
|
1200
1666
|
if (usable.length === 0) throw new LlmError(`全部 ${data.accounts.length} 个 ${this.hooks.displayName} 账号均需要重新登录。`, "AUTH", { status: 401 });
|
|
1201
1667
|
const selected = this.selectAccount(usable, data);
|
|
1202
|
-
|
|
1203
|
-
const refreshed = await this.hooks.refresh(selected.credentials, fetchFn);
|
|
1204
|
-
await this.updateAccountCredentials(selected.id, refreshed);
|
|
1205
|
-
return {
|
|
1206
|
-
account: selected,
|
|
1207
|
-
credentials: refreshed
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1668
|
+
const account = await this.freshAccount(selected, fetchFn, forceRefresh);
|
|
1210
1669
|
return {
|
|
1211
|
-
account
|
|
1212
|
-
credentials:
|
|
1670
|
+
account,
|
|
1671
|
+
credentials: account.credentials
|
|
1213
1672
|
};
|
|
1214
1673
|
}
|
|
1215
1674
|
/**
|
|
@@ -1244,8 +1703,8 @@ var AccountPoolCore = class {
|
|
|
1244
1703
|
* looks unexpired, and only the service's own refusal says so. Renewing it is
|
|
1245
1704
|
* what separates that from a credential that genuinely needs a new sign-in.
|
|
1246
1705
|
*/
|
|
1247
|
-
async renewCredential(accountId, fetchFn = fetch) {
|
|
1248
|
-
return this.credentialFor(accountId, fetchFn, true);
|
|
1706
|
+
async renewCredential(accountId, fetchFn = fetch, expected) {
|
|
1707
|
+
return this.credentialFor(accountId, fetchFn, true, expected);
|
|
1249
1708
|
}
|
|
1250
1709
|
/**
|
|
1251
1710
|
* Resolve, and if needed renew, one account's credential.
|
|
@@ -1255,29 +1714,106 @@ var AccountPoolCore = class {
|
|
|
1255
1714
|
* not name an account (the catalog loader) still has to present the
|
|
1256
1715
|
* credential that is actually in play.
|
|
1257
1716
|
*/
|
|
1258
|
-
async credentialFor(accountId, fetchFn, force) {
|
|
1717
|
+
async credentialFor(accountId, fetchFn, force, expected) {
|
|
1259
1718
|
const data = await this.read();
|
|
1260
1719
|
const target = accountId === void 0 ? data.accounts.find((account) => account.id === data.activeAccountId) ?? data.accounts.find((account) => account.isPrimary) ?? data.accounts[0] : data.accounts.find((account) => account.id === accountId);
|
|
1261
1720
|
if (target === void 0) throw new LlmError(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`, "AUTH");
|
|
1262
1721
|
if (!this.isCredentialUsable(target)) throw new LlmError(`${this.hooks.displayName} 账号的凭据已被拒绝,需要重新登录。`, "AUTH", { status: 401 });
|
|
1263
|
-
if (
|
|
1264
|
-
|
|
1722
|
+
if (expected !== void 0 && !isDeepStrictEqual(expected, target.credentials)) return target.credentials;
|
|
1723
|
+
return (await this.freshAccount(target, fetchFn, force)).credentials;
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Return one account's current row, refreshing it at most once per generation.
|
|
1727
|
+
*
|
|
1728
|
+
* Shared by every entry point — routing, auxiliary tools, the settings card
|
|
1729
|
+
* and forced renewals — across all instances of the pool file, because a
|
|
1730
|
+
* rotating refresh token may be spent once: two parallel exchanges both fail,
|
|
1731
|
+
* and the losing one gets recorded as a dead sign-in.
|
|
1732
|
+
*
|
|
1733
|
+
* Reading the current generation and registering/joining its flight happen in
|
|
1734
|
+
* one short transaction, so a commit cannot slip between the two decisions.
|
|
1735
|
+
* The network call is fired AFTER the lock is released, and the plan returned
|
|
1736
|
+
* from the lock is a plain value: awaiting a flight while holding the lock
|
|
1737
|
+
* would deadlock that flight's own commit.
|
|
1738
|
+
*/
|
|
1739
|
+
async freshAccount(target, fetchFn, force = false) {
|
|
1740
|
+
let launch;
|
|
1741
|
+
const plan = await this.serialize(async () => {
|
|
1742
|
+
const current = (await this.loadPoolData()).accounts.find((account) => account.id === target.id);
|
|
1743
|
+
if (current === void 0 || !this.isCredentialUsable(current)) throw new PoolAccountUnavailableError(this.hooks.emptyMessage ?? "未登录 " + this.hooks.displayName + " 账号,请先在设置页添加账号。", "AUTH");
|
|
1744
|
+
if (!isDeepStrictEqual(current.credentials, target.credentials)) return { account: current };
|
|
1745
|
+
if (!this.hooks.refresh || !force && !this.shouldRefreshCredential(current, Date.now())) return { account: current };
|
|
1746
|
+
const key = JSON.stringify([this.operationKey, current.id]);
|
|
1747
|
+
const pending = poolRefreshes.get(key);
|
|
1748
|
+
if (pending !== void 0 && isDeepStrictEqual(pending.credentials, current.credentials)) return { result: pending.result };
|
|
1749
|
+
const result = new Promise((resolve, reject) => {
|
|
1750
|
+
launch = () => {
|
|
1751
|
+
this.refreshAccount(current, fetchFn).then(resolve, reject);
|
|
1752
|
+
};
|
|
1753
|
+
});
|
|
1754
|
+
const flight = {
|
|
1755
|
+
credentials: structuredClone(current.credentials),
|
|
1756
|
+
result
|
|
1757
|
+
};
|
|
1758
|
+
poolRefreshes.set(key, flight);
|
|
1759
|
+
const release = () => {
|
|
1760
|
+
if (poolRefreshes.get(key) === flight) poolRefreshes.delete(key);
|
|
1761
|
+
};
|
|
1762
|
+
result.then(release, release);
|
|
1763
|
+
return { result };
|
|
1764
|
+
});
|
|
1765
|
+
launch?.();
|
|
1766
|
+
return plan.result ?? plan.account;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Refresh one account: network outside the lock, then a conditional commit.
|
|
1770
|
+
*
|
|
1771
|
+
* Every write-back is checked against the credential the refresh started from,
|
|
1772
|
+
* which is what keeps a late response from restoring a spent token over a
|
|
1773
|
+
* re-login, and keeps a deleted account from coming back.
|
|
1774
|
+
*/
|
|
1775
|
+
async refreshAccount(target, fetchFn) {
|
|
1776
|
+
const expected = structuredClone(target.credentials);
|
|
1265
1777
|
const live = await this.hooks.liveCredentialsFor?.(target).catch(() => null);
|
|
1266
1778
|
if (live !== null && live !== void 0 && live.advanced) {
|
|
1267
|
-
target.
|
|
1268
|
-
|
|
1269
|
-
|
|
1779
|
+
const account = await this.updateAccountCredentials(target.id, live.credentials, expected).catch(() => ({
|
|
1780
|
+
...target,
|
|
1781
|
+
credentials: live.credentials
|
|
1782
|
+
}));
|
|
1783
|
+
this.assertUsableAccount(account);
|
|
1784
|
+
return account;
|
|
1270
1785
|
}
|
|
1271
1786
|
let refreshed;
|
|
1272
1787
|
try {
|
|
1273
1788
|
refreshed = await this.hooks.refresh(target.credentials, fetchFn);
|
|
1274
1789
|
} catch (error) {
|
|
1275
1790
|
const status = this.hooks.refreshFailureStatus?.(error);
|
|
1276
|
-
|
|
1791
|
+
let superseded;
|
|
1792
|
+
await this.updatePool((data) => {
|
|
1793
|
+
const current = data.accounts.find((account) => account.id === target.id);
|
|
1794
|
+
if (current === void 0) return;
|
|
1795
|
+
if (!isDeepStrictEqual(current.credentials, expected)) {
|
|
1796
|
+
superseded = current;
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (status !== void 0) {
|
|
1800
|
+
current.authStatus = status;
|
|
1801
|
+
current.authFailedReason = error instanceof Error ? error.message : String(error);
|
|
1802
|
+
}
|
|
1803
|
+
}).catch(() => void 0);
|
|
1804
|
+
if (superseded !== void 0) {
|
|
1805
|
+
this.assertUsableAccount(superseded);
|
|
1806
|
+
return superseded;
|
|
1807
|
+
}
|
|
1277
1808
|
throw error;
|
|
1278
1809
|
}
|
|
1279
|
-
await this.updateAccountCredentials(target.id, refreshed);
|
|
1280
|
-
|
|
1810
|
+
const account = await this.updateAccountCredentials(target.id, refreshed, expected);
|
|
1811
|
+
this.assertUsableAccount(account);
|
|
1812
|
+
return account;
|
|
1813
|
+
}
|
|
1814
|
+
assertUsableAccount(account) {
|
|
1815
|
+
if (account === void 0) throw new PoolAccountUnavailableError(this.hooks.emptyMessage ?? "未登录 " + this.hooks.displayName + " 账号,请先在设置页添加账号。", "AUTH");
|
|
1816
|
+
if (!this.isCredentialUsable(account)) throw new PoolAccountUnavailableError(this.hooks.displayName + " 账号的凭据已被拒绝,需要重新登录。", "AUTH", { status: 401 });
|
|
1281
1817
|
}
|
|
1282
1818
|
/** Whether one account's credential must be refreshed before it is handed out. */
|
|
1283
1819
|
shouldRefreshCredential(account, now) {
|
|
@@ -1321,37 +1857,34 @@ var AccountPoolCore = class {
|
|
|
1321
1857
|
* @param fetchFn - fetch used by the provider's token refresh.
|
|
1322
1858
|
* @throws LlmError with `RATE_LIMIT` when every account is cooling down.
|
|
1323
1859
|
*/
|
|
1324
|
-
async getEffectiveAccount(excludeIds, fetchFn = fetch) {
|
|
1860
|
+
async getEffectiveAccount(excludeIds, fetchFn = fetch, forceRefresh = false) {
|
|
1325
1861
|
const data = await this.read();
|
|
1326
1862
|
if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? `未登录 ${this.hooks.displayName} 账号,请先在设置页添加账号。`);
|
|
1327
1863
|
const now = Date.now();
|
|
1328
1864
|
const eligible = data.accounts.filter((account) => this.isEligible(account, now, excludeIds));
|
|
1329
1865
|
if (eligible.length === 0) throw this.allUnavailableError(data, now);
|
|
1330
1866
|
const selected = this.selectAccount(eligible, data);
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
selected.lastUsedAt = now;
|
|
1350
|
-
data.activeAccountId = selected.id;
|
|
1351
|
-
await this.write(data).catch(() => void 0);
|
|
1867
|
+
let refreshed;
|
|
1868
|
+
try {
|
|
1869
|
+
refreshed = await this.freshAccount(selected, fetchFn, forceRefresh);
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
if (this.hooks.refreshFailureStatus?.(error) === void 0 && !(error instanceof PoolAccountUnavailableError)) throw error;
|
|
1872
|
+
const nextExclude = new Set(excludeIds ?? []);
|
|
1873
|
+
nextExclude.add(selected.id);
|
|
1874
|
+
if (await this.hasAnotherAvailableAccount(nextExclude)) return this.getEffectiveAccount(nextExclude, fetchFn, forceRefresh);
|
|
1875
|
+
throw error;
|
|
1876
|
+
}
|
|
1877
|
+
const account = await this.updatePool((current) => {
|
|
1878
|
+
const latest = current.accounts.find((entry) => entry.id === refreshed.id);
|
|
1879
|
+
if (latest === void 0 || !isDeepStrictEqual(latest.credentials, refreshed.credentials)) return latest;
|
|
1880
|
+
latest.lastUsedAt = now;
|
|
1881
|
+
if (current.activeAccountId === data.activeAccountId) current.activeAccountId = latest.id;
|
|
1882
|
+
return latest;
|
|
1883
|
+
}).catch(() => refreshed);
|
|
1884
|
+
this.assertUsableAccount(account);
|
|
1352
1885
|
return {
|
|
1353
|
-
account
|
|
1354
|
-
credentials:
|
|
1886
|
+
account,
|
|
1887
|
+
credentials: account.credentials
|
|
1355
1888
|
};
|
|
1356
1889
|
}
|
|
1357
1890
|
};
|
|
@@ -1724,8 +2257,7 @@ var OAuthService = class {
|
|
|
1724
2257
|
async refresh() {
|
|
1725
2258
|
this.assertAvailable();
|
|
1726
2259
|
if (this.pool !== null) {
|
|
1727
|
-
|
|
1728
|
-
await this.refreshAccount(credentials).then((refreshed) => this.pool.updateAccountCredentials(account.id, refreshed));
|
|
2260
|
+
await this.pool.getEffectiveAccount(void 0, this.fetchFn, true);
|
|
1729
2261
|
return this.status();
|
|
1730
2262
|
}
|
|
1731
2263
|
const stored = await this.loadAuthenticated();
|
|
@@ -1787,18 +2319,8 @@ var OAuthService = class {
|
|
|
1787
2319
|
*/
|
|
1788
2320
|
async credentials(forceRefresh = false, access = {}) {
|
|
1789
2321
|
if (this.pool !== null) {
|
|
1790
|
-
if (access.purpose === "tool")
|
|
1791
|
-
|
|
1792
|
-
if (!forceRefresh) return credentials;
|
|
1793
|
-
const refreshed = await this.refreshAccount(credentials, this.fetchFn);
|
|
1794
|
-
await this.pool.updateAccountCredentials(account.id, refreshed);
|
|
1795
|
-
return refreshed;
|
|
1796
|
-
}
|
|
1797
|
-
const { account, credentials } = await this.pool.getEffectiveAccount(void 0, this.fetchFn);
|
|
1798
|
-
if (!forceRefresh) return credentials;
|
|
1799
|
-
const refreshed = await this.refreshAccount(credentials, this.fetchFn);
|
|
1800
|
-
await this.pool.updateAccountCredentials(account.id, refreshed);
|
|
1801
|
-
return refreshed;
|
|
2322
|
+
if (access.purpose === "tool") return (await this.pool.getCredentialAccount(this.fetchFn, forceRefresh)).credentials;
|
|
2323
|
+
return (await this.pool.getEffectiveAccount(void 0, this.fetchFn, forceRefresh)).credentials;
|
|
1802
2324
|
}
|
|
1803
2325
|
const stored = await this.loadAuthenticated();
|
|
1804
2326
|
if (forceRefresh || stored.expiresAt - this.now() <= 6e4) return this.refreshCredentials(stored);
|
|
@@ -2251,11 +2773,8 @@ var CodexAccountPool = class extends AccountPoolCore {
|
|
|
2251
2773
|
async refreshAccountNow(accountId) {
|
|
2252
2774
|
const account = (await this.read()).accounts.find((entry) => entry.id === accountId);
|
|
2253
2775
|
if (account === void 0) throw new Error("ChatGPT 账号已不在号池中");
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
const refreshed = await refresher.refreshAccount(account.credentials);
|
|
2257
|
-
await this.updateAccountCredentials(accountId, refreshed);
|
|
2258
|
-
return refreshed;
|
|
2776
|
+
if (this.refresherRef.current === void 0) throw new Error("ChatGPT 凭据刷新服务尚未就绪");
|
|
2777
|
+
return this.renewCredential(accountId, fetch, account.credentials);
|
|
2259
2778
|
}
|
|
2260
2779
|
/** The single-credential store this pool mirrors its primary account into. */
|
|
2261
2780
|
mirrorStore() {
|
|
@@ -2627,31 +3146,6 @@ function normalizeGenerateOptions(options) {
|
|
|
2627
3146
|
};
|
|
2628
3147
|
}
|
|
2629
3148
|
//#endregion
|
|
2630
|
-
//#region src/host/wire-auth.ts
|
|
2631
|
-
function codexHeaders(credentials, sessionId) {
|
|
2632
|
-
const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
|
|
2633
|
-
return {
|
|
2634
|
-
authorization: `Bearer ${credentials.accessToken}`,
|
|
2635
|
-
...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
|
|
2636
|
-
originator: CODEX_ORIGINATOR,
|
|
2637
|
-
"user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
|
|
2638
|
-
...sessionId ? { "session-id": sessionId } : {}
|
|
2639
|
-
};
|
|
2640
|
-
}
|
|
2641
|
-
function stableSessionId(value) {
|
|
2642
|
-
const source = value === void 0 || value === "" ? randomUUID() : value;
|
|
2643
|
-
return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
|
|
2644
|
-
}
|
|
2645
|
-
function retryAfterMs$1(headers) {
|
|
2646
|
-
const raw = headers.get("retry-after");
|
|
2647
|
-
if (raw === null) return void 0;
|
|
2648
|
-
const seconds = Number(raw);
|
|
2649
|
-
if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
|
|
2650
|
-
const timestamp = Date.parse(raw);
|
|
2651
|
-
if (!Number.isFinite(timestamp)) return void 0;
|
|
2652
|
-
return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
|
|
2653
|
-
}
|
|
2654
|
-
//#endregion
|
|
2655
3149
|
//#region src/host/codex-images.ts
|
|
2656
3150
|
const PNG_SIGNATURE = [
|
|
2657
3151
|
137,
|
|
@@ -2805,7 +3299,6 @@ function requestImage(fetchFn, credentials, prompt, turnId, signal) {
|
|
|
2805
3299
|
method: "POST",
|
|
2806
3300
|
headers: {
|
|
2807
3301
|
...codexHeaders(credentials),
|
|
2808
|
-
originator: "pi",
|
|
2809
3302
|
accept: "application/json",
|
|
2810
3303
|
"content-type": "application/json",
|
|
2811
3304
|
"x-codex-image-turn-id": turnId
|
|
@@ -2905,7 +3398,6 @@ function sendSearch(fetchFn, credentials, query, model, id, signal) {
|
|
|
2905
3398
|
method: "POST",
|
|
2906
3399
|
headers: {
|
|
2907
3400
|
...codexHeaders(credentials),
|
|
2908
|
-
originator: "pi",
|
|
2909
3401
|
accept: "application/json",
|
|
2910
3402
|
"content-type": "application/json"
|
|
2911
3403
|
},
|
|
@@ -3058,8 +3550,8 @@ function parseEnvProxy(env = process.env, options = {}) {
|
|
|
3058
3550
|
try {
|
|
3059
3551
|
const dshHome = env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
3060
3552
|
const envFile = options.envFile ?? path.join(dshHome, ".env");
|
|
3061
|
-
if (fs.existsSync(envFile)) {
|
|
3062
|
-
const match = fs.readFileSync(envFile, "utf8").match(/^(?:export\s+)?(?:HTTPS_PROXY|https_proxy|HTTP_PROXY|http_proxy|ALL_PROXY|all_proxy)\s*=\s*["']?([^"'\r\n]+)["']?/m);
|
|
3553
|
+
if (fs$1.existsSync(envFile)) {
|
|
3554
|
+
const match = fs$1.readFileSync(envFile, "utf8").match(/^(?:export\s+)?(?:HTTPS_PROXY|https_proxy|HTTP_PROXY|http_proxy|ALL_PROXY|all_proxy)\s*=\s*["']?([^"'\r\n]+)["']?/m);
|
|
3063
3555
|
if (match && match[1]?.trim()) return normalizeProxyUrl(match[1].trim());
|
|
3064
3556
|
}
|
|
3065
3557
|
} catch {}
|
|
@@ -3680,7 +4172,7 @@ var FileCredentialStore$2 = class {
|
|
|
3680
4172
|
}
|
|
3681
4173
|
async removeLegacy() {
|
|
3682
4174
|
try {
|
|
3683
|
-
await
|
|
4175
|
+
await fs.unlink(this.filePath);
|
|
3684
4176
|
} catch (error) {
|
|
3685
4177
|
if (error.code !== "ENOENT") throw new Error("Antigravity legacy credential removal failed");
|
|
3686
4178
|
}
|
|
@@ -3699,11 +4191,11 @@ var FileCredentialStore$2 = class {
|
|
|
3699
4191
|
}
|
|
3700
4192
|
let legacy;
|
|
3701
4193
|
try {
|
|
3702
|
-
const stats = await
|
|
4194
|
+
const stats = await fs.lstat(this.filePath);
|
|
3703
4195
|
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
|
|
3704
4196
|
if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
|
|
3705
|
-
if (process.platform !== "win32") await
|
|
3706
|
-
legacy = await
|
|
4197
|
+
if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
|
|
4198
|
+
legacy = await fs.readFile(this.filePath, "utf8");
|
|
3707
4199
|
} catch (error) {
|
|
3708
4200
|
if (error.code === "ENOENT") return null;
|
|
3709
4201
|
throw new Error("Antigravity legacy credential read failed");
|
|
@@ -3738,7 +4230,7 @@ var FileModelSettingsStore$2 = class {
|
|
|
3738
4230
|
}
|
|
3739
4231
|
async read() {
|
|
3740
4232
|
try {
|
|
3741
|
-
const content = await
|
|
4233
|
+
const content = await fs.readFile(this.filePath, "utf8");
|
|
3742
4234
|
const parsed = JSON.parse(content);
|
|
3743
4235
|
if (typeof parsed === "object" && parsed !== null) {
|
|
3744
4236
|
const record = parsed;
|
|
@@ -3764,10 +4256,10 @@ var FileModelSettingsStore$2 = class {
|
|
|
3764
4256
|
};
|
|
3765
4257
|
}
|
|
3766
4258
|
async write(settings) {
|
|
3767
|
-
await
|
|
4259
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
3768
4260
|
const tmp = `${this.filePath}.tmp.${Date.now()}`;
|
|
3769
|
-
await
|
|
3770
|
-
await
|
|
4261
|
+
await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
|
|
4262
|
+
await fs.rename(tmp, this.filePath);
|
|
3771
4263
|
}
|
|
3772
4264
|
async updateSettings(patch) {
|
|
3773
4265
|
const current = await this.read();
|
|
@@ -3893,7 +4385,7 @@ var FilePreferencesStore = class {
|
|
|
3893
4385
|
async readDocument() {
|
|
3894
4386
|
let parsed;
|
|
3895
4387
|
try {
|
|
3896
|
-
parsed = JSON.parse(await
|
|
4388
|
+
parsed = JSON.parse(await fs.readFile(this.filePath, "utf8"));
|
|
3897
4389
|
} catch {
|
|
3898
4390
|
return;
|
|
3899
4391
|
}
|
|
@@ -3914,17 +4406,17 @@ var FilePreferencesStore = class {
|
|
|
3914
4406
|
for (const listener of [...this.listeners]) listener(next, prev);
|
|
3915
4407
|
}
|
|
3916
4408
|
async write(value) {
|
|
3917
|
-
await
|
|
4409
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
3918
4410
|
writeSequence += 1;
|
|
3919
4411
|
const tmp = `${this.filePath}.tmp.${process.pid}.${writeSequence}`;
|
|
3920
4412
|
try {
|
|
3921
|
-
await
|
|
4413
|
+
await fs.writeFile(tmp, JSON.stringify(value, null, 2), {
|
|
3922
4414
|
encoding: "utf8",
|
|
3923
4415
|
mode: 384
|
|
3924
4416
|
});
|
|
3925
|
-
await
|
|
4417
|
+
await fs.rename(tmp, this.filePath);
|
|
3926
4418
|
} finally {
|
|
3927
|
-
await
|
|
4419
|
+
await fs.unlink(tmp).catch(() => void 0);
|
|
3928
4420
|
}
|
|
3929
4421
|
}
|
|
3930
4422
|
};
|
|
@@ -3989,7 +4481,7 @@ const LEGACY_DOCUMENTS = ["settings.yaml", "settings.yaml.imported"];
|
|
|
3989
4481
|
*/
|
|
3990
4482
|
async function readLegacyPreferences(homeDir = dshHomeDir$2()) {
|
|
3991
4483
|
for (const name of LEGACY_DOCUMENTS) {
|
|
3992
|
-
const document = await
|
|
4484
|
+
const document = await fs.readFile(path.join(homeDir, name), "utf8").catch(() => void 0);
|
|
3993
4485
|
if (document === void 0) continue;
|
|
3994
4486
|
const section = sectionOf(document, PREFERENCES_NAMESPACE);
|
|
3995
4487
|
if (section !== void 0 && Object.keys(section).length > 0) return section;
|
|
@@ -4297,7 +4789,7 @@ function normalizeReplayCallIds(items, normalizeCallId) {
|
|
|
4297
4789
|
call_id: normalizeCallId(item.call_id)
|
|
4298
4790
|
} : item);
|
|
4299
4791
|
}
|
|
4300
|
-
async function buildResponsesPayload(options, attachments, localRawImages = {}, outputVerbosity = null, fastMode = false, reasoningSummary = null) {
|
|
4792
|
+
async function buildResponsesPayload(options, attachments, localRawImages = {}, outputVerbosity = null, fastMode = false, reasoningSummary = null, promptCacheKey) {
|
|
4301
4793
|
const sandboxRetryTools = recentSandboxRetryToolNames(options.messages);
|
|
4302
4794
|
const resolveLocalRawImages = supportsImageInput(options);
|
|
4303
4795
|
const normalizeCallId = createCallIdNormalizer();
|
|
@@ -4365,7 +4857,8 @@ async function buildResponsesPayload(options, attachments, localRawImages = {},
|
|
|
4365
4857
|
input,
|
|
4366
4858
|
stream: true,
|
|
4367
4859
|
store: false,
|
|
4368
|
-
include: ["reasoning.encrypted_content"]
|
|
4860
|
+
include: ["reasoning.encrypted_content"],
|
|
4861
|
+
...promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}
|
|
4369
4862
|
};
|
|
4370
4863
|
const instructions = [...instructionParts, localRawImageInstruction(localImageStats)].filter((value) => Boolean(value));
|
|
4371
4864
|
if (instructions.length > 0) payload.instructions = instructions.join("\n\n");
|
|
@@ -4381,6 +4874,8 @@ async function buildResponsesPayload(options, attachments, localRawImages = {},
|
|
|
4381
4874
|
}
|
|
4382
4875
|
if (outputVerbosity !== null) payload.text = { verbosity: outputVerbosity };
|
|
4383
4876
|
if (fastMode) payload.service_tier = "priority";
|
|
4877
|
+
const modelMaxTokens = codexModelMaxTokens(options.model);
|
|
4878
|
+
payload.max_output_tokens = options.maxTokens === void 0 ? modelMaxTokens : Math.min(options.maxTokens, modelMaxTokens);
|
|
4384
4879
|
if (options.reasoningEffort !== void 0) {
|
|
4385
4880
|
const effort = codexWireReasoningEffort(options.model, options.reasoningEffort);
|
|
4386
4881
|
payload.reasoning = codexModelSupportsReasoningSummary(options.model) ? {
|
|
@@ -4698,6 +5193,8 @@ const MAX_VISIBLE_REASONING_CHARS = 12e3;
|
|
|
4698
5193
|
const DEFAULT_POOL_COOLDOWN_MS = 15 * 6e4;
|
|
4699
5194
|
const REASONING_DELTA_FLUSH_CHARS = 768;
|
|
4700
5195
|
const REASONING_TRUNCATED_NOTICE = "\n\n[Reasoning summary truncated to keep the DSH web UI responsive.]";
|
|
5196
|
+
/** Cap on conversations whose turn state is remembered at once. */
|
|
5197
|
+
const MAX_TRACKED_TURN_STATES = 200;
|
|
4701
5198
|
var ResponsesClient = class {
|
|
4702
5199
|
oauth;
|
|
4703
5200
|
attachments;
|
|
@@ -4707,6 +5204,16 @@ var ResponsesClient = class {
|
|
|
4707
5204
|
fastMode;
|
|
4708
5205
|
reasoningSummary;
|
|
4709
5206
|
accountPool;
|
|
5207
|
+
/**
|
|
5208
|
+
* Opaque backend turn state, keyed by conversation session id.
|
|
5209
|
+
*
|
|
5210
|
+
* The Codex backend hands `x-codex-turn-state` back on each response and
|
|
5211
|
+
* expects it on the next request of the same conversation, which lets it
|
|
5212
|
+
* resume the turn instead of re-ingesting the whole history. The value is
|
|
5213
|
+
* short-lived and account-scoped, so it is stored per session and simply
|
|
5214
|
+
* dropped when the backend stops sending it — never a correctness input.
|
|
5215
|
+
*/
|
|
5216
|
+
turnStates = /* @__PURE__ */ new Map();
|
|
4710
5217
|
constructor(oauth, attachments, options = {}) {
|
|
4711
5218
|
this.oauth = oauth;
|
|
4712
5219
|
this.attachments = attachments;
|
|
@@ -4727,7 +5234,7 @@ var ResponsesClient = class {
|
|
|
4727
5234
|
let attemptOptions = options;
|
|
4728
5235
|
let response;
|
|
4729
5236
|
while (true) {
|
|
4730
|
-
const payload = await buildResponsesPayload(attemptOptions, this.attachments, this.localRawImages, this.outputVerbosity(), this.fastMode(), this.reasoningSummary());
|
|
5237
|
+
const payload = await buildResponsesPayload(attemptOptions, this.attachments, this.localRawImages, this.outputVerbosity(), this.fastMode(), this.reasoningSummary(), promptCacheKeyFor(sessionId));
|
|
4731
5238
|
try {
|
|
4732
5239
|
response = await this.send(payload, sessionId, options.signal);
|
|
4733
5240
|
break;
|
|
@@ -4762,9 +5269,36 @@ var ResponsesClient = class {
|
|
|
4762
5269
|
response = await this.request(payload, credentials, sessionId, signal);
|
|
4763
5270
|
}
|
|
4764
5271
|
if (!response.ok) throw await responseError(response);
|
|
5272
|
+
this.rememberTurnState(response, sessionId);
|
|
4765
5273
|
return response;
|
|
4766
5274
|
}
|
|
4767
5275
|
/**
|
|
5276
|
+
* Record the turn state a response carried, for the next turn of this session.
|
|
5277
|
+
*
|
|
5278
|
+
* A response that carries none clears any stored value: a backend that stops
|
|
5279
|
+
* sending the header has stopped honouring it, and replaying a stale value
|
|
5280
|
+
* would be guessing. An empty header is treated the same as absent.
|
|
5281
|
+
*/
|
|
5282
|
+
rememberTurnState(response, sessionId) {
|
|
5283
|
+
const next = response.headers.get(CODEX_TURN_STATE_HEADER)?.trim();
|
|
5284
|
+
if (next === void 0 || next === "") this.turnStates.delete(sessionId);
|
|
5285
|
+
else this.storeTurnState(sessionId, next);
|
|
5286
|
+
}
|
|
5287
|
+
/**
|
|
5288
|
+
* Bounded store: a long-lived host serves many sessions, and nothing here ever
|
|
5289
|
+
* signals that a session ended, so the map is capped and evicts the oldest
|
|
5290
|
+
* entry. The value is an optimization, so a miss only costs a full resend.
|
|
5291
|
+
*/
|
|
5292
|
+
storeTurnState(sessionId, value) {
|
|
5293
|
+
this.turnStates.delete(sessionId);
|
|
5294
|
+
this.turnStates.set(sessionId, value);
|
|
5295
|
+
while (this.turnStates.size > MAX_TRACKED_TURN_STATES) {
|
|
5296
|
+
const oldest = this.turnStates.keys().next();
|
|
5297
|
+
if (oldest.done === true) break;
|
|
5298
|
+
this.turnStates.delete(oldest.value);
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
/**
|
|
4768
5302
|
* Send one payload with account rotation.
|
|
4769
5303
|
*
|
|
4770
5304
|
* The pool picks the account — skipping cooling, quota-exhausted and failed
|
|
@@ -4806,6 +5340,7 @@ var ResponsesClient = class {
|
|
|
4806
5340
|
});
|
|
4807
5341
|
}
|
|
4808
5342
|
if (!response.ok) throw await responseError(response);
|
|
5343
|
+
this.rememberTurnState(response, sessionId);
|
|
4809
5344
|
return response;
|
|
4810
5345
|
}
|
|
4811
5346
|
}
|
|
@@ -4814,7 +5349,7 @@ var ResponsesClient = class {
|
|
|
4814
5349
|
return await this.fetchFn(CODEX_RESPONSES_URL, {
|
|
4815
5350
|
method: "POST",
|
|
4816
5351
|
headers: {
|
|
4817
|
-
...codexHeaders(credentials, sessionId),
|
|
5352
|
+
...codexHeaders(credentials, sessionId, { turnState: this.turnStates.get(sessionId) }),
|
|
4818
5353
|
"content-type": "application/json",
|
|
4819
5354
|
accept: "text/event-stream"
|
|
4820
5355
|
},
|
|
@@ -4827,6 +5362,17 @@ var ResponsesClient = class {
|
|
|
4827
5362
|
}
|
|
4828
5363
|
}
|
|
4829
5364
|
};
|
|
5365
|
+
/**
|
|
5366
|
+
* Prompt cache key for one conversation.
|
|
5367
|
+
*
|
|
5368
|
+
* The backend keys its prefix cache on this string, so it must be stable for a
|
|
5369
|
+
* conversation and distinct between conversations — otherwise one session would
|
|
5370
|
+
* be served another session's cached prefix. The session id is already a hashed
|
|
5371
|
+
* opaque string, so it is reused directly rather than hashed again.
|
|
5372
|
+
*/
|
|
5373
|
+
function promptCacheKeyFor(sessionId) {
|
|
5374
|
+
return sessionId;
|
|
5375
|
+
}
|
|
4830
5376
|
async function* parseResponsesStream(response, signal, hiddenSandboxControls = /* @__PURE__ */ new Set()) {
|
|
4831
5377
|
if (response.body === null) throw new LlmError("Codex returned no response stream.", "PROVIDER_ERROR");
|
|
4832
5378
|
const reader = response.body.getReader();
|
|
@@ -5818,7 +6364,7 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
|
|
|
5818
6364
|
"Content-Type": "application/javascript; charset=utf-8",
|
|
5819
6365
|
"Cache-Control": "public, max-age=86400"
|
|
5820
6366
|
});
|
|
5821
|
-
fs.createReadStream(mermaidPath).pipe(response);
|
|
6367
|
+
fs$1.createReadStream(mermaidPath).pipe(response);
|
|
5822
6368
|
} catch {
|
|
5823
6369
|
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
5824
6370
|
response.end("Not found");
|
|
@@ -9384,7 +9930,7 @@ var FileCredentialStore$1 = class {
|
|
|
9384
9930
|
}
|
|
9385
9931
|
async removeLegacy() {
|
|
9386
9932
|
try {
|
|
9387
|
-
await
|
|
9933
|
+
await fs.unlink(this.filePath);
|
|
9388
9934
|
} catch (error) {
|
|
9389
9935
|
if (error.code !== "ENOENT") throw new Error("Command Code legacy credential removal failed");
|
|
9390
9936
|
}
|
|
@@ -9403,11 +9949,11 @@ var FileCredentialStore$1 = class {
|
|
|
9403
9949
|
}
|
|
9404
9950
|
let legacy;
|
|
9405
9951
|
try {
|
|
9406
|
-
const stats = await
|
|
9952
|
+
const stats = await fs.lstat(this.filePath);
|
|
9407
9953
|
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
|
|
9408
9954
|
if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
|
|
9409
|
-
if (process.platform !== "win32") await
|
|
9410
|
-
legacy = await
|
|
9955
|
+
if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
|
|
9956
|
+
legacy = await fs.readFile(this.filePath, "utf8");
|
|
9411
9957
|
} catch (error) {
|
|
9412
9958
|
if (error.code === "ENOENT") return null;
|
|
9413
9959
|
throw new Error("Command Code legacy credential read failed");
|
|
@@ -9443,7 +9989,7 @@ var FileModelSettingsStore$1 = class {
|
|
|
9443
9989
|
}
|
|
9444
9990
|
async read() {
|
|
9445
9991
|
try {
|
|
9446
|
-
const content = await
|
|
9992
|
+
const content = await fs.readFile(this.filePath, "utf8");
|
|
9447
9993
|
const parsed = JSON.parse(content);
|
|
9448
9994
|
if (typeof parsed === "object" && parsed !== null) {
|
|
9449
9995
|
const record = parsed;
|
|
@@ -9469,10 +10015,10 @@ var FileModelSettingsStore$1 = class {
|
|
|
9469
10015
|
};
|
|
9470
10016
|
}
|
|
9471
10017
|
async write(settings) {
|
|
9472
|
-
await
|
|
10018
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
9473
10019
|
const tmp = `${this.filePath}.tmp.${Date.now()}`;
|
|
9474
|
-
await
|
|
9475
|
-
await
|
|
10020
|
+
await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
|
|
10021
|
+
await fs.rename(tmp, this.filePath);
|
|
9476
10022
|
}
|
|
9477
10023
|
async updateSettings(patch) {
|
|
9478
10024
|
const current = await this.read();
|
|
@@ -9571,121 +10117,6 @@ function commandCodePlanLabel(planId) {
|
|
|
9571
10117
|
return resolveCommandCodePlan(planId)?.name ?? planId;
|
|
9572
10118
|
}
|
|
9573
10119
|
//#endregion
|
|
9574
|
-
//#region src/host/common/catalog-snapshot.ts
|
|
9575
|
-
/**
|
|
9576
|
-
* Persistent snapshot for one provider's live model catalog.
|
|
9577
|
-
*
|
|
9578
|
-
* The in-memory catalog caches keep a process alive between requests, but they
|
|
9579
|
-
* start cold: on the first call after a restart (or a plugin reinstall) the
|
|
9580
|
-
* picker and the settings page both wait on the upstream listing before they
|
|
9581
|
-
* can render anything, and an unreachable endpoint turns that wait into the
|
|
9582
|
-
* full discovery timeout — "the model server is unavailable" on a fresh
|
|
9583
|
-
* install. The catalog is not fast-moving data, so the last successful listing
|
|
9584
|
-
* is persisted to `<DSH_HOME>/storages/` and rehydrated into the in-memory
|
|
9585
|
-
* cache on first use. A snapshot the caller can read is therefore available
|
|
9586
|
-
* from the very first call, with no network in the path.
|
|
9587
|
-
*
|
|
9588
|
-
* Writes are best-effort: persistence is an optimization, never a capability.
|
|
9589
|
-
* A read-only home or a locked file must not fail the fetch that produced the
|
|
9590
|
-
* snapshot.
|
|
9591
|
-
*/
|
|
9592
|
-
/** File name of the persisted snapshot for one catalog. */
|
|
9593
|
-
function catalogSnapshotPath(name) {
|
|
9594
|
-
return path.join(dshHomeDir(), "storages", `${name}-catalog.json`);
|
|
9595
|
-
}
|
|
9596
|
-
/**
|
|
9597
|
-
* Snapshot name for one catalog *and* one credential scope.
|
|
9598
|
-
*
|
|
9599
|
-
* A persisted snapshot answers before any credential is read — that is the whole
|
|
9600
|
-
* point of it — so the file itself has to carry the scope it was fetched under.
|
|
9601
|
-
* Otherwise the first call after a restart, for any account, is answered by
|
|
9602
|
-
* whatever listing some other scope happened to persist last: a Kimi listing
|
|
9603
|
-
* fetched for the mainland-cn endpoint would answer a global caller with models
|
|
9604
|
-
* and context windows that region does not serve, and a Command Code listing
|
|
9605
|
-
* fetched against one API environment would answer the other.
|
|
9606
|
-
*
|
|
9607
|
-
* The scope is part of the file name rather than of the payload because a caller
|
|
9608
|
-
* that cannot name the account — the harness resolves this catalog with no token
|
|
9609
|
-
* at all — still has to find the snapshot its own scope owns.
|
|
9610
|
-
*
|
|
9611
|
-
* Everything outside `[a-z0-9]` is folded to `_`, so no scope can escape the
|
|
9612
|
-
* `storages` directory.
|
|
9613
|
-
*/
|
|
9614
|
-
function catalogSnapshotName(base, scope) {
|
|
9615
|
-
const normalized = scope.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
9616
|
-
return normalized === "" ? base : `${base}-${normalized}`;
|
|
9617
|
-
}
|
|
9618
|
-
/**
|
|
9619
|
-
* Read the persisted snapshot for one catalog.
|
|
9620
|
-
*
|
|
9621
|
-
* Shape errors and unreadable files both return `undefined` — the caller keeps
|
|
9622
|
-
* its in-memory state, and the next successful fetch overwrites the file.
|
|
9623
|
-
*/
|
|
9624
|
-
async function readCatalogSnapshot(name, parseModels) {
|
|
9625
|
-
try {
|
|
9626
|
-
const content = await fsPromises.readFile(catalogSnapshotPath(name), "utf8");
|
|
9627
|
-
const parsed = JSON.parse(content);
|
|
9628
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
9629
|
-
const record = parsed;
|
|
9630
|
-
const fetchedAt = record.fetchedAt;
|
|
9631
|
-
if (typeof fetchedAt !== "number" || !Number.isFinite(fetchedAt) || fetchedAt <= 0) return void 0;
|
|
9632
|
-
const models = parseModels(record.models);
|
|
9633
|
-
if (models === void 0 || models.length === 0) return void 0;
|
|
9634
|
-
return {
|
|
9635
|
-
fetchedAt,
|
|
9636
|
-
models
|
|
9637
|
-
};
|
|
9638
|
-
} catch {
|
|
9639
|
-
return;
|
|
9640
|
-
}
|
|
9641
|
-
}
|
|
9642
|
-
/**
|
|
9643
|
-
* Snapshot writes still in flight.
|
|
9644
|
-
*
|
|
9645
|
-
* Callers fire-and-forget these writes, so nothing in production ever awaits
|
|
9646
|
-
* one. A test that deletes its private `storages` directory between cases
|
|
9647
|
-
* cannot know when that is safe, and a write that lands after the delete
|
|
9648
|
-
* recreates the file the next case asserts is absent — which is how a stale
|
|
9649
|
-
* listing leaked into a later test and failed it on CI. Registering each write
|
|
9650
|
-
* gives {@link flushCatalogSnapshots} a deterministic point to wait on.
|
|
9651
|
-
*/
|
|
9652
|
-
const pendingSnapshotWrites = /* @__PURE__ */ new Set();
|
|
9653
|
-
/** Persist one snapshot; a failure is logged nowhere and never thrown. */
|
|
9654
|
-
function writeCatalogSnapshot(name, models, fetchedAt) {
|
|
9655
|
-
const write = (async () => {
|
|
9656
|
-
try {
|
|
9657
|
-
const filePath = catalogSnapshotPath(name);
|
|
9658
|
-
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
|
9659
|
-
const tmp = `${filePath}.tmp.${Date.now()}`;
|
|
9660
|
-
await fsPromises.writeFile(tmp, JSON.stringify({
|
|
9661
|
-
fetchedAt,
|
|
9662
|
-
models
|
|
9663
|
-
}, null, 2), "utf8");
|
|
9664
|
-
await fsPromises.rename(tmp, filePath);
|
|
9665
|
-
} catch {}
|
|
9666
|
-
})();
|
|
9667
|
-
pendingSnapshotWrites.add(write);
|
|
9668
|
-
write.finally(() => {
|
|
9669
|
-
pendingSnapshotWrites.delete(write);
|
|
9670
|
-
});
|
|
9671
|
-
return write;
|
|
9672
|
-
}
|
|
9673
|
-
/**
|
|
9674
|
-
* Warm an in-memory cache from the persisted snapshot when it is fresher than
|
|
9675
|
-
* what the cache currently holds. Called lazily on the first cache miss of a
|
|
9676
|
-
* process, so a restart serves the previous listing without waiting on the
|
|
9677
|
-
* network.
|
|
9678
|
-
*
|
|
9679
|
-
* @returns the snapshot's `fetchedAt`, or `0` when no usable snapshot exists —
|
|
9680
|
-
* the caller then fetches as before.
|
|
9681
|
-
*/
|
|
9682
|
-
async function rehydrateCatalogCache(name, parseModels, accept, currentFetchedAt) {
|
|
9683
|
-
const snapshot = await readCatalogSnapshot(name, parseModels);
|
|
9684
|
-
if (snapshot === void 0 || snapshot.fetchedAt <= currentFetchedAt) return 0;
|
|
9685
|
-
accept(snapshot.fetchedAt, snapshot.models);
|
|
9686
|
-
return snapshot.fetchedAt;
|
|
9687
|
-
}
|
|
9688
|
-
//#endregion
|
|
9689
10120
|
//#region src/host/command-code/client.ts
|
|
9690
10121
|
const CLI_VERSION = "1.0.0";
|
|
9691
10122
|
/**
|
|
@@ -12955,6 +13386,10 @@ function maxOutputTokensFor$3(modelId, contextWindow) {
|
|
|
12955
13386
|
const capped = (contextWindow ?? kimiCodeModelDef(modelId)?.contextWindow ?? 262144) - CONTEXT_HEADROOM_TOKENS$1;
|
|
12956
13387
|
return Math.max(declared, capped);
|
|
12957
13388
|
}
|
|
13389
|
+
/** Whether a stored preference names a tier this line can send. */
|
|
13390
|
+
function isKimiCacheTtl(value) {
|
|
13391
|
+
return value === "5m" || value === "1h";
|
|
13392
|
+
}
|
|
12958
13393
|
/**
|
|
12959
13394
|
* Reduce the requested output cap so prompt + output fit the window.
|
|
12960
13395
|
*
|
|
@@ -13004,7 +13439,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
13004
13439
|
enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$3],
|
|
13005
13440
|
catalogModels: [],
|
|
13006
13441
|
contextWindowOverrides: {},
|
|
13007
|
-
defaultReasoningEffort: null
|
|
13442
|
+
defaultReasoningEffort: null,
|
|
13443
|
+
cacheTtl: null
|
|
13008
13444
|
};
|
|
13009
13445
|
fallbackStore.read().then((stored) => {
|
|
13010
13446
|
snapshot = stored;
|
|
@@ -13021,7 +13457,12 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
13021
13457
|
enabled: z.boolean().default(true),
|
|
13022
13458
|
enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS$3]),
|
|
13023
13459
|
contextWindowOverrides: z.dict(z.number()).default({}),
|
|
13024
|
-
defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null)
|
|
13460
|
+
defaultReasoningEffort: z.union([...KIMI_CODE_REASONING_EFFORTS.map((effort) => z.const(effort)), z.const(null)]).default(null),
|
|
13461
|
+
cacheTtl: z.union([
|
|
13462
|
+
z.const("5m"),
|
|
13463
|
+
z.const("1h"),
|
|
13464
|
+
z.const(null)
|
|
13465
|
+
]).default(null)
|
|
13025
13466
|
}));
|
|
13026
13467
|
return {
|
|
13027
13468
|
status: () => {
|
|
@@ -13031,7 +13472,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
13031
13472
|
enabledModelIds: value.enabledModelIds,
|
|
13032
13473
|
catalogModels: [],
|
|
13033
13474
|
contextWindowOverrides: value.contextWindowOverrides,
|
|
13034
|
-
defaultReasoningEffort: value.defaultReasoningEffort
|
|
13475
|
+
defaultReasoningEffort: value.defaultReasoningEffort,
|
|
13476
|
+
cacheTtl: isKimiCacheTtl(value.cacheTtl) ? value.cacheTtl : null
|
|
13035
13477
|
};
|
|
13036
13478
|
},
|
|
13037
13479
|
update: async (patch) => {
|
|
@@ -13040,7 +13482,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
|
|
|
13040
13482
|
enabled: patch.enabled !== void 0 ? patch.enabled : current.enabled !== false,
|
|
13041
13483
|
enabledModelIds: patch.enabledModelIds ?? current.enabledModelIds,
|
|
13042
13484
|
contextWindowOverrides: patch.contextWindowOverrides ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
|
|
13043
|
-
defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort
|
|
13485
|
+
defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort,
|
|
13486
|
+
cacheTtl: patch.cacheTtl !== void 0 ? patch.cacheTtl : current.cacheTtl
|
|
13044
13487
|
};
|
|
13045
13488
|
await scope.update(normalized);
|
|
13046
13489
|
fallbackStore.updateSettings(patch).catch(() => void 0);
|
|
@@ -13162,7 +13605,7 @@ var FileCredentialStore$3 = class {
|
|
|
13162
13605
|
}
|
|
13163
13606
|
async removeLegacy() {
|
|
13164
13607
|
try {
|
|
13165
|
-
await
|
|
13608
|
+
await fs.unlink(this.filePath);
|
|
13166
13609
|
} catch (error) {
|
|
13167
13610
|
if (error.code !== "ENOENT") throw new Error("Kimi Code legacy credential removal failed");
|
|
13168
13611
|
}
|
|
@@ -13181,11 +13624,11 @@ var FileCredentialStore$3 = class {
|
|
|
13181
13624
|
}
|
|
13182
13625
|
let legacy;
|
|
13183
13626
|
try {
|
|
13184
|
-
const stats = await
|
|
13627
|
+
const stats = await fs.lstat(this.filePath);
|
|
13185
13628
|
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
|
|
13186
13629
|
if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
|
|
13187
|
-
if (process.platform !== "win32") await
|
|
13188
|
-
legacy = await
|
|
13630
|
+
if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
|
|
13631
|
+
legacy = await fs.readFile(this.filePath, "utf8");
|
|
13189
13632
|
} catch (error) {
|
|
13190
13633
|
if (error.code === "ENOENT") return null;
|
|
13191
13634
|
throw new Error("Kimi Code legacy credential read failed");
|
|
@@ -13221,7 +13664,7 @@ var FileModelSettingsStore$3 = class {
|
|
|
13221
13664
|
}
|
|
13222
13665
|
async read() {
|
|
13223
13666
|
try {
|
|
13224
|
-
const content = await
|
|
13667
|
+
const content = await fs.readFile(this.filePath, "utf8");
|
|
13225
13668
|
const parsed = JSON.parse(content);
|
|
13226
13669
|
if (typeof parsed === "object" && parsed !== null) {
|
|
13227
13670
|
const record = parsed;
|
|
@@ -13234,7 +13677,8 @@ var FileModelSettingsStore$3 = class {
|
|
|
13234
13677
|
enabledModelIds,
|
|
13235
13678
|
catalogModels,
|
|
13236
13679
|
contextWindowOverrides,
|
|
13237
|
-
defaultReasoningEffort
|
|
13680
|
+
defaultReasoningEffort,
|
|
13681
|
+
cacheTtl: isKimiCacheTtl(record.cacheTtl) ? record.cacheTtl : null
|
|
13238
13682
|
};
|
|
13239
13683
|
}
|
|
13240
13684
|
} catch {}
|
|
@@ -13243,14 +13687,15 @@ var FileModelSettingsStore$3 = class {
|
|
|
13243
13687
|
enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$3],
|
|
13244
13688
|
catalogModels: [],
|
|
13245
13689
|
contextWindowOverrides: {},
|
|
13246
|
-
defaultReasoningEffort: null
|
|
13690
|
+
defaultReasoningEffort: null,
|
|
13691
|
+
cacheTtl: null
|
|
13247
13692
|
};
|
|
13248
13693
|
}
|
|
13249
13694
|
async write(settings) {
|
|
13250
|
-
await
|
|
13695
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
13251
13696
|
const tmp = `${this.filePath}.tmp.${Date.now()}`;
|
|
13252
|
-
await
|
|
13253
|
-
await
|
|
13697
|
+
await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
|
|
13698
|
+
await fs.rename(tmp, this.filePath);
|
|
13254
13699
|
}
|
|
13255
13700
|
async updateSettings(patch) {
|
|
13256
13701
|
const current = await this.read();
|
|
@@ -13259,7 +13704,8 @@ var FileModelSettingsStore$3 = class {
|
|
|
13259
13704
|
...patch.enabled !== void 0 ? { enabled: patch.enabled } : {},
|
|
13260
13705
|
...patch.enabledModelIds !== void 0 ? { enabledModelIds: patch.enabledModelIds } : {},
|
|
13261
13706
|
...patch.contextWindowOverrides !== void 0 ? { contextWindowOverrides: mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) } : {},
|
|
13262
|
-
...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {}
|
|
13707
|
+
...patch.defaultReasoningEffort !== void 0 ? { defaultReasoningEffort: patch.defaultReasoningEffort } : {},
|
|
13708
|
+
...patch.cacheTtl !== void 0 && patch.cacheTtl !== null ? { cacheTtl: isKimiCacheTtl(patch.cacheTtl) ? patch.cacheTtl : current.cacheTtl } : patch.cacheTtl === null ? { cacheTtl: null } : {}
|
|
13263
13709
|
};
|
|
13264
13710
|
await this.write(next);
|
|
13265
13711
|
return next;
|
|
@@ -13289,7 +13735,7 @@ async function resolveRegion(env = process.env) {
|
|
|
13289
13735
|
return pinned.includes(".ai") ? "global" : "mainland-cn";
|
|
13290
13736
|
}
|
|
13291
13737
|
try {
|
|
13292
|
-
const marker = (await
|
|
13738
|
+
const marker = (await fs.readFile(regionMarkerPath(), "utf8")).trim();
|
|
13293
13739
|
if (isRegion$4(marker)) return marker;
|
|
13294
13740
|
} catch {}
|
|
13295
13741
|
return "mainland-cn";
|
|
@@ -13298,8 +13744,8 @@ async function resolveRegion(env = process.env) {
|
|
|
13298
13744
|
async function persistRegion(region) {
|
|
13299
13745
|
try {
|
|
13300
13746
|
const file = regionMarkerPath();
|
|
13301
|
-
await
|
|
13302
|
-
await
|
|
13747
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
13748
|
+
await fs.writeFile(file, region, "utf8");
|
|
13303
13749
|
} catch {}
|
|
13304
13750
|
}
|
|
13305
13751
|
//#endregion
|
|
@@ -13401,13 +13847,13 @@ function deviceModel() {
|
|
|
13401
13847
|
async function getDeviceId() {
|
|
13402
13848
|
const file = deviceIdPath();
|
|
13403
13849
|
try {
|
|
13404
|
-
const existing = (await
|
|
13850
|
+
const existing = (await fs.readFile(file, "utf8")).trim();
|
|
13405
13851
|
if (existing !== "") return existing;
|
|
13406
13852
|
} catch {}
|
|
13407
13853
|
const created = randomUUID();
|
|
13408
13854
|
try {
|
|
13409
|
-
await
|
|
13410
|
-
await
|
|
13855
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
13856
|
+
await fs.writeFile(file, created, {
|
|
13411
13857
|
encoding: "utf8",
|
|
13412
13858
|
mode: 384
|
|
13413
13859
|
});
|
|
@@ -15223,6 +15669,39 @@ function imageBlockToInline$2(block, images) {
|
|
|
15223
15669
|
data: resolved.data
|
|
15224
15670
|
} : void 0;
|
|
15225
15671
|
}
|
|
15672
|
+
/**
|
|
15673
|
+
* Media a single request build may carry.
|
|
15674
|
+
*
|
|
15675
|
+
* Passed as one object so adding a media kind never grows a positional
|
|
15676
|
+
* signature the existing callers already bind.
|
|
15677
|
+
*/
|
|
15678
|
+
/**
|
|
15679
|
+
* The cache field for the Anthropic-compatible wire.
|
|
15680
|
+
*
|
|
15681
|
+
* TOP LEVEL, not per block: the service documents that a `cache_control` inside
|
|
15682
|
+
* a message is ignored, and omitting the field means "read a 5m entry but do
|
|
15683
|
+
* not write one" — which is why this is only attached when the caller asked
|
|
15684
|
+
* for a tier.
|
|
15685
|
+
*/
|
|
15686
|
+
function kimiAnthropicCacheControl(ttl) {
|
|
15687
|
+
return { cache_control: {
|
|
15688
|
+
type: "ephemeral",
|
|
15689
|
+
ttl
|
|
15690
|
+
} };
|
|
15691
|
+
}
|
|
15692
|
+
/**
|
|
15693
|
+
* The cache field for the OpenAI-compatible wire.
|
|
15694
|
+
*
|
|
15695
|
+
* `mode: 'implicit'` is the only mode the service supports — it identifies the
|
|
15696
|
+
* prefix itself, so this is not a request to break the cache but a request to
|
|
15697
|
+
* write it at a chosen TTL.
|
|
15698
|
+
*/
|
|
15699
|
+
function kimiOpenAICacheOptions(ttl) {
|
|
15700
|
+
return { prompt_cache_options: {
|
|
15701
|
+
mode: "implicit",
|
|
15702
|
+
ttl
|
|
15703
|
+
} };
|
|
15704
|
+
}
|
|
15226
15705
|
function textOf$3(content) {
|
|
15227
15706
|
if (typeof content === "string") return sanitizeText$3(content);
|
|
15228
15707
|
if (!Array.isArray(content)) return "";
|
|
@@ -15758,6 +16237,7 @@ function buildOpenAIRequest(options, images = NO_RESOLVED_IMAGES$3, preserveThin
|
|
|
15758
16237
|
stream: true,
|
|
15759
16238
|
stream_options: { include_usage: true },
|
|
15760
16239
|
max_completion_tokens: maxTokens,
|
|
16240
|
+
...media.cacheTtl === null || media.cacheTtl === void 0 ? {} : kimiOpenAICacheOptions(media.cacheTtl),
|
|
15761
16241
|
...stopSequences(options.stop).length > 0 ? { stop: stopSequences(options.stop) } : {},
|
|
15762
16242
|
...options.tools && options.tools.length > 0 ? {
|
|
15763
16243
|
tools: options.tools.map((tool) => ({
|
|
@@ -15891,7 +16371,8 @@ function mergeAnthropicMessages$1(entries) {
|
|
|
15891
16371
|
return merged;
|
|
15892
16372
|
}
|
|
15893
16373
|
/** Build one `/v1/messages` body. */
|
|
15894
|
-
function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3,
|
|
16374
|
+
function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {}) {
|
|
16375
|
+
const cacheTtl = media.cacheTtl ?? null;
|
|
15895
16376
|
const carriers = options.messages.filter((message) => message.role === "system" && messageToolsOf(message) !== void 0);
|
|
15896
16377
|
const unsentDeclarations = carriers.reduce((total, message) => total + (messageToolsOf(message)?.length ?? 0), 0);
|
|
15897
16378
|
const carrierText = carriers.map((message) => textOf$3(message.content)).filter((text) => text !== "");
|
|
@@ -15922,6 +16403,7 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, _media =
|
|
|
15922
16403
|
model: options.model,
|
|
15923
16404
|
max_tokens: maxTokens,
|
|
15924
16405
|
messages: mergeAnthropicMessages$1(entries),
|
|
16406
|
+
...cacheTtl === null || cacheTtl === void 0 ? {} : kimiAnthropicCacheControl(cacheTtl),
|
|
15925
16407
|
...system === void 0 ? {} : { system },
|
|
15926
16408
|
...options.temperature === void 0 || budget !== void 0 ? {} : { temperature: options.temperature },
|
|
15927
16409
|
...options.stop && options.stop.length > 0 ? { stop_sequences: options.stop } : {},
|
|
@@ -16724,12 +17206,13 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
16724
17206
|
const entry = catalog.find((model) => model.id === options.model);
|
|
16725
17207
|
const requestOptions = offloadOldestRequestVideos(offloadOldestRequestImages$2(normalizeGenerateOptions(options)));
|
|
16726
17208
|
const [images, videos] = await Promise.all([resolveRequestImages$2(requestOptions, this.options.attachments, signal), resolveRequestVideos(requestOptions, this.options.videos, signal)]);
|
|
17209
|
+
const settings = await this.settings();
|
|
16727
17210
|
const media = {
|
|
16728
17211
|
videos,
|
|
16729
17212
|
videoAccepted: inputModalitiesForEntry(options.model, catalog).includes("video"),
|
|
16730
|
-
messageTools: dynamicToolsForEntry(options.model, catalog)
|
|
17213
|
+
messageTools: dynamicToolsForEntry(options.model, catalog),
|
|
17214
|
+
cacheTtl: settings.cacheTtl
|
|
16731
17215
|
};
|
|
16732
|
-
const settings = await this.settings();
|
|
16733
17216
|
const contextWindow = this.contextWindowFor(options.model, entry, settings.contextWindowOverrides);
|
|
16734
17217
|
const requestedMax = options.maxTokens ?? maxOutputTokensFor$3(options.model, contextWindow);
|
|
16735
17218
|
const body = assertRequestBodyFits$1(buildRequest({
|
|
@@ -17108,6 +17591,7 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
|
|
|
17108
17591
|
models,
|
|
17109
17592
|
contextWindowOverrides: settings.contextWindowOverrides,
|
|
17110
17593
|
defaultReasoningEffort: settings.defaultReasoningEffort,
|
|
17594
|
+
cacheTtl: settings.cacheTtl,
|
|
17111
17595
|
loginRegion: region,
|
|
17112
17596
|
serving: readOption$3(options.serving, true),
|
|
17113
17597
|
conflict: readOption$3(options.conflict, null)
|
|
@@ -17333,6 +17817,10 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
|
|
|
17333
17817
|
const effort = body.defaultReasoningEffort;
|
|
17334
17818
|
if (effort === null || isKimiCodeEffort(effort)) patch.defaultReasoningEffort = effort;
|
|
17335
17819
|
}
|
|
17820
|
+
if (body.cacheTtl !== void 0) {
|
|
17821
|
+
const ttl = body.cacheTtl;
|
|
17822
|
+
if (ttl === null || isKimiCacheTtl(ttl)) patch.cacheTtl = ttl;
|
|
17823
|
+
}
|
|
17336
17824
|
if (preferences) await preferences.update(patch);
|
|
17337
17825
|
else await modelSettings.updateSettings(patch);
|
|
17338
17826
|
if (patch.enabledModelIds !== void 0 || patch.enabled !== void 0) ctx.emit?.("llm/adapters-updated");
|
|
@@ -17542,15 +18030,15 @@ async function saveVideo(input) {
|
|
|
17542
18030
|
const digest = createHash("sha256").update(input.data).digest("hex");
|
|
17543
18031
|
const attachmentId = `sha256:${digest}`;
|
|
17544
18032
|
const dir = videoStoreDir();
|
|
17545
|
-
await
|
|
18033
|
+
await fs.mkdir(dir, {
|
|
17546
18034
|
recursive: true,
|
|
17547
18035
|
mode: 448
|
|
17548
18036
|
});
|
|
17549
18037
|
const target = path.join(dir, digest + extensionFor(input.declaredType));
|
|
17550
|
-
if (!await
|
|
18038
|
+
if (!await fs.stat(target).then(() => true).catch(() => false)) {
|
|
17551
18039
|
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
17552
|
-
await
|
|
17553
|
-
await
|
|
18040
|
+
await fs.writeFile(tmp, input.data);
|
|
18041
|
+
await fs.rename(tmp, target);
|
|
17554
18042
|
}
|
|
17555
18043
|
pruneVideoStore().catch(() => void 0);
|
|
17556
18044
|
return {
|
|
@@ -17580,14 +18068,14 @@ async function pruneVideoStore(budgetBytes = VIDEO_STORE_BUDGET_BYTES) {
|
|
|
17580
18068
|
const dir = videoStoreDir();
|
|
17581
18069
|
let entries;
|
|
17582
18070
|
try {
|
|
17583
|
-
const names = await
|
|
18071
|
+
const names = await fs.readdir(dir);
|
|
17584
18072
|
entries = [];
|
|
17585
18073
|
for (const name of names) {
|
|
17586
18074
|
if (name.includes(".tmp.")) {
|
|
17587
|
-
await
|
|
18075
|
+
await fs.rm(path.join(dir, name), { force: true }).catch(() => void 0);
|
|
17588
18076
|
continue;
|
|
17589
18077
|
}
|
|
17590
|
-
const stats = await
|
|
18078
|
+
const stats = await fs.stat(path.join(dir, name)).catch(() => void 0);
|
|
17591
18079
|
if (stats === void 0 || !stats.isFile()) continue;
|
|
17592
18080
|
entries.push({
|
|
17593
18081
|
path: path.join(dir, name),
|
|
@@ -17604,7 +18092,7 @@ async function pruneVideoStore(budgetBytes = VIDEO_STORE_BUDGET_BYTES) {
|
|
|
17604
18092
|
let removed = 0;
|
|
17605
18093
|
for (const entry of entries) {
|
|
17606
18094
|
if (total <= budgetBytes) break;
|
|
17607
|
-
await
|
|
18095
|
+
await fs.rm(entry.path, { force: true }).catch(() => void 0);
|
|
17608
18096
|
total -= entry.size;
|
|
17609
18097
|
removed += 1;
|
|
17610
18098
|
}
|
|
@@ -17628,7 +18116,7 @@ async function readVideoBytes(ref) {
|
|
|
17628
18116
|
if (stored === void 0) throw new VideoIngestError("video reference is not one this store issued", "VIDEO_REFERENCE_INVALID");
|
|
17629
18117
|
let data;
|
|
17630
18118
|
try {
|
|
17631
|
-
data = await
|
|
18119
|
+
data = await fs.readFile(stored);
|
|
17632
18120
|
} catch {
|
|
17633
18121
|
throw new VideoIngestError("stored video is missing; attach it again", "VIDEO_MISSING");
|
|
17634
18122
|
}
|
|
@@ -17689,14 +18177,14 @@ function humanBytes(bytes) {
|
|
|
17689
18177
|
/** Read one local file, refusing anything that is not a regular file. */
|
|
17690
18178
|
async function readLocalFile(input) {
|
|
17691
18179
|
if (!path.isAbsolute(input)) throw new HarnessError(`video path must be absolute: ${input}`, "KIMI_VIDEO_PATH_NOT_ABSOLUTE");
|
|
17692
|
-
const stats = await
|
|
18180
|
+
const stats = await fs.stat(input).catch(() => void 0);
|
|
17693
18181
|
if (stats === void 0) throw new HarnessError(`video file not found: ${input}`, "KIMI_VIDEO_NOT_FOUND");
|
|
17694
18182
|
if (!stats.isFile()) throw new HarnessError(`video path is not a regular file: ${input}`, "KIMI_VIDEO_NOT_A_FILE");
|
|
17695
18183
|
if (stats.size > 31457280) throw new HarnessError(`video is ${humanBytes(stats.size)}, above the ${humanBytes(MAX_VIDEO_FILE_BYTES)} limit this route sends`, "KIMI_VIDEO_TOO_LARGE");
|
|
17696
18184
|
const type = mediaTypeForPath(input);
|
|
17697
18185
|
if (type === void 0) throw new HarnessError(`unsupported video extension "${path.extname(input)}"; supported: .mp4 .m4v .webm .mov .avi .mpeg .mpg .flv .wmv .3gp`, "KIMI_VIDEO_TYPE_UNSUPPORTED");
|
|
17698
18186
|
return {
|
|
17699
|
-
data: await
|
|
18187
|
+
data: await fs.readFile(input),
|
|
17700
18188
|
name: path.basename(input),
|
|
17701
18189
|
type
|
|
17702
18190
|
};
|
|
@@ -19177,16 +19665,16 @@ function credentialNeedsRefresh(credentials, now = Date.now()) {
|
|
|
19177
19665
|
* leaves the destination untouched.
|
|
19178
19666
|
*/
|
|
19179
19667
|
async function writeFileAtomic(filePath, contents) {
|
|
19180
|
-
await
|
|
19668
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
19181
19669
|
const temporary = filePath + ".dsh-tmp-" + process.pid + "-" + Date.now();
|
|
19182
19670
|
try {
|
|
19183
|
-
await
|
|
19671
|
+
await fs.writeFile(temporary, contents, {
|
|
19184
19672
|
encoding: "utf8",
|
|
19185
19673
|
mode: 384
|
|
19186
19674
|
});
|
|
19187
|
-
await
|
|
19675
|
+
await fs.rename(temporary, filePath);
|
|
19188
19676
|
} catch (error) {
|
|
19189
|
-
await
|
|
19677
|
+
await fs.rm(temporary, { force: true }).catch(() => void 0);
|
|
19190
19678
|
throw new Error("MiniMax Code credential write failed; the previous file was left untouched (" + (error instanceof Error ? error.message : String(error)) + ")");
|
|
19191
19679
|
}
|
|
19192
19680
|
}
|
|
@@ -19396,7 +19884,7 @@ async function readNativeDocument(region) {
|
|
|
19396
19884
|
const filePath = authJsonPath(region);
|
|
19397
19885
|
let raw;
|
|
19398
19886
|
try {
|
|
19399
|
-
raw = await
|
|
19887
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
19400
19888
|
} catch {
|
|
19401
19889
|
return null;
|
|
19402
19890
|
}
|
|
@@ -19433,7 +19921,7 @@ async function syncAuthState(region, options) {
|
|
|
19433
19921
|
const filePath = authStateJsonPath(region);
|
|
19434
19922
|
let parsed;
|
|
19435
19923
|
try {
|
|
19436
|
-
parsed = JSON.parse(await
|
|
19924
|
+
parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
19437
19925
|
} catch {
|
|
19438
19926
|
return;
|
|
19439
19927
|
}
|
|
@@ -19481,7 +19969,7 @@ var MinimaxCodeCredentialStore = class {
|
|
|
19481
19969
|
async readPluginFile() {
|
|
19482
19970
|
let raw;
|
|
19483
19971
|
try {
|
|
19484
|
-
raw = await
|
|
19972
|
+
raw = await fs.readFile(pluginCredentialPath(), "utf8");
|
|
19485
19973
|
} catch {
|
|
19486
19974
|
return null;
|
|
19487
19975
|
}
|
|
@@ -19623,7 +20111,7 @@ var MinimaxCodeCredentialStore = class {
|
|
|
19623
20111
|
*/
|
|
19624
20112
|
delete() {
|
|
19625
20113
|
return serialize(pluginCredentialPath(), async () => {
|
|
19626
|
-
await
|
|
20114
|
+
await fs.rm(pluginCredentialPath(), { force: true });
|
|
19627
20115
|
recentRead = null;
|
|
19628
20116
|
});
|
|
19629
20117
|
}
|
|
@@ -19722,17 +20210,17 @@ var MinimaxCodeModelSettingsStore = class {
|
|
|
19722
20210
|
}
|
|
19723
20211
|
async read() {
|
|
19724
20212
|
try {
|
|
19725
|
-
return parseMinimaxCodeModelSettings(JSON.parse(await
|
|
20213
|
+
return parseMinimaxCodeModelSettings(JSON.parse(await fs.readFile(this.filePath, "utf8")));
|
|
19726
20214
|
} catch {
|
|
19727
20215
|
return defaultMinimaxCodeSettings();
|
|
19728
20216
|
}
|
|
19729
20217
|
}
|
|
19730
20218
|
async write(settings) {
|
|
19731
|
-
await
|
|
20219
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
19732
20220
|
const temporary = this.filePath + ".tmp." + Date.now();
|
|
19733
|
-
await
|
|
19734
|
-
await
|
|
19735
|
-
if (!isDeepStrictEqual(parseMinimaxCodeModelSettings(JSON.parse(await
|
|
20221
|
+
await fs.writeFile(temporary, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
20222
|
+
await fs.rename(temporary, this.filePath);
|
|
20223
|
+
if (!isDeepStrictEqual(parseMinimaxCodeModelSettings(JSON.parse(await fs.readFile(this.filePath, "utf8"))), settings)) throw new Error("MiniMax Code model settings write failed; the stored selection does not match what was written");
|
|
19736
20224
|
}
|
|
19737
20225
|
/**
|
|
19738
20226
|
* Read-modify-write under the same per-path lock the credential writes use.
|
|
@@ -21552,23 +22040,30 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
|
|
|
21552
22040
|
* because that one genuinely needs a sign-in.
|
|
21553
22041
|
*/
|
|
21554
22042
|
async healStaleAuthFailures(current) {
|
|
21555
|
-
const
|
|
21556
|
-
if (
|
|
22043
|
+
const failedAccounts = (current ?? await this.read()).accounts.filter(isMarkedFailed);
|
|
22044
|
+
if (failedAccounts.length === 0) return false;
|
|
22045
|
+
const targets = new Map(failedAccounts.map((account) => [account.id, {
|
|
22046
|
+
credentials: account.credentials,
|
|
22047
|
+
authStatus: account.authStatus
|
|
22048
|
+
}]));
|
|
21557
22049
|
const live = await this.store.read().catch(() => null);
|
|
21558
22050
|
if (live === null) return false;
|
|
21559
|
-
|
|
21560
|
-
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
21567
|
-
|
|
21568
|
-
|
|
21569
|
-
|
|
21570
|
-
|
|
21571
|
-
|
|
22051
|
+
return await this.updatePool((draft) => {
|
|
22052
|
+
let changed = false;
|
|
22053
|
+
for (const account of draft.accounts) {
|
|
22054
|
+
const expected = targets.get(account.id);
|
|
22055
|
+
if (!expected) continue;
|
|
22056
|
+
if (account.authStatus !== expected.authStatus) continue;
|
|
22057
|
+
if (!isDeepStrictEqual(account.credentials, expected.credentials)) continue;
|
|
22058
|
+
if (!minimaxCodeSameCredentialSession(live, account.credentials)) continue;
|
|
22059
|
+
if (!minimaxCodeCredentialAdvancedPast(live, account.credentials)) continue;
|
|
22060
|
+
account.authStatus = void 0;
|
|
22061
|
+
account.authFailedReason = void 0;
|
|
22062
|
+
account.credentials = live;
|
|
22063
|
+
changed = true;
|
|
22064
|
+
}
|
|
22065
|
+
return changed;
|
|
22066
|
+
});
|
|
21572
22067
|
}
|
|
21573
22068
|
/**
|
|
21574
22069
|
* Pick the account for the next request.
|
|
@@ -21582,7 +22077,13 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
|
|
|
21582
22077
|
const effective = await this.getEffectiveAccount(excludeIds, fetchFn);
|
|
21583
22078
|
const live = await this.liveNativeCredential(effective.account);
|
|
21584
22079
|
if (live === null) return effective;
|
|
21585
|
-
if (live.accessToken !== effective.credentials.accessToken)
|
|
22080
|
+
if (live.accessToken !== effective.credentials.accessToken) {
|
|
22081
|
+
const updated = await this.updateAccountCredentials(effective.account.id, live, effective.credentials).catch(() => void 0);
|
|
22082
|
+
if (updated) return {
|
|
22083
|
+
account: updated,
|
|
22084
|
+
credentials: updated.credentials
|
|
22085
|
+
};
|
|
22086
|
+
}
|
|
21586
22087
|
return {
|
|
21587
22088
|
account: effective.account,
|
|
21588
22089
|
credentials: live
|
|
@@ -21644,7 +22145,7 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
|
|
|
21644
22145
|
return this.addAccount(credentials, alias);
|
|
21645
22146
|
}
|
|
21646
22147
|
/**
|
|
21647
|
-
*
|
|
22148
|
+
* Remove one account inside a transaction.
|
|
21648
22149
|
*
|
|
21649
22150
|
* An account whose credential is the desktop app's own \`auth.json\` is refused:
|
|
21650
22151
|
* this plugin does not own that file, and removing the account would either
|
|
@@ -21652,9 +22153,9 @@ var MinimaxCodeAccountPool = class extends AccountPoolCore {
|
|
|
21652
22153
|
* file — sign the user out of MiniMax Code itself. Signing out there is the
|
|
21653
22154
|
* supported way to end it, and the card is told so by \`removable: false\`.
|
|
21654
22155
|
*/
|
|
21655
|
-
|
|
21656
|
-
if (
|
|
21657
|
-
|
|
22156
|
+
deleteAccountFromPool(data, accountId) {
|
|
22157
|
+
if (data.accounts.find((account) => account.id === accountId)?.source === "minimax-native") throw new Error("该账号来自 MiniMax Code 桌面端(~/.minimax/auth),本插件只能读取和续期,不能删除。请在 MiniMax Code 应用中退出登录。");
|
|
22158
|
+
super.deleteAccountFromPool(data, accountId);
|
|
21658
22159
|
}
|
|
21659
22160
|
/** The credential store this pool mirrors its primary account into. */
|
|
21660
22161
|
mirrorStore() {
|
|
@@ -23434,7 +23935,7 @@ function isCanonicalFile(name) {
|
|
|
23434
23935
|
async function scanCredentials(dir = codeBuddyAuthDir()) {
|
|
23435
23936
|
let names;
|
|
23436
23937
|
try {
|
|
23437
|
-
names = await
|
|
23938
|
+
names = await fs.readdir(dir);
|
|
23438
23939
|
} catch (error) {
|
|
23439
23940
|
if (error.code === "ENOENT") return [];
|
|
23440
23941
|
throw new Error(`WorkBuddy credential directory could not be read: ${dir}`);
|
|
@@ -23444,9 +23945,9 @@ async function scanCredentials(dir = codeBuddyAuthDir()) {
|
|
|
23444
23945
|
if (!name.endsWith(".info")) continue;
|
|
23445
23946
|
const file = path.join(dir, name);
|
|
23446
23947
|
try {
|
|
23447
|
-
const stats = await
|
|
23948
|
+
const stats = await fs.stat(file);
|
|
23448
23949
|
if (!stats.isFile()) continue;
|
|
23449
|
-
const raw = await
|
|
23950
|
+
const raw = await fs.readFile(file, "utf8");
|
|
23450
23951
|
found.push(parseCredentialFile(JSON.parse(raw), file, stats.mtimeMs));
|
|
23451
23952
|
} catch {
|
|
23452
23953
|
continue;
|
|
@@ -23583,7 +24084,7 @@ var FileCredentialStore$4 = class {
|
|
|
23583
24084
|
async isStillCurrent(credentials) {
|
|
23584
24085
|
if (credentials.source === "managed") return true;
|
|
23585
24086
|
try {
|
|
23586
|
-
return (await
|
|
24087
|
+
return (await fs.stat(credentials.sourceFile)).mtimeMs === credentials.sourceMtimeMs;
|
|
23587
24088
|
} catch {
|
|
23588
24089
|
return false;
|
|
23589
24090
|
}
|
|
@@ -23623,9 +24124,9 @@ var FileCredentialStore$4 = class {
|
|
|
23623
24124
|
*/
|
|
23624
24125
|
async readDesktopFile(filePath) {
|
|
23625
24126
|
try {
|
|
23626
|
-
const stats = await
|
|
24127
|
+
const stats = await fs.stat(filePath);
|
|
23627
24128
|
if (!stats.isFile()) return null;
|
|
23628
|
-
const raw = await
|
|
24129
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
23629
24130
|
return parseCredentialFile(JSON.parse(raw), filePath, stats.mtimeMs);
|
|
23630
24131
|
} catch {
|
|
23631
24132
|
return null;
|
|
@@ -23684,25 +24185,25 @@ var FileCredentialStore$4 = class {
|
|
|
23684
24185
|
*/
|
|
23685
24186
|
async writeBack(credentials) {
|
|
23686
24187
|
try {
|
|
23687
|
-
const raw = await
|
|
24188
|
+
const raw = await fs.readFile(credentials.sourceFile, "utf8");
|
|
23688
24189
|
const parsed = JSON.parse(raw);
|
|
23689
24190
|
if (!isRecord$11(parsed) || !isRecord$11(parsed.auth)) return;
|
|
23690
24191
|
parsed.auth.accessToken = credentials.accessToken;
|
|
23691
24192
|
if (credentials.refreshToken !== "") parsed.auth.refreshToken = credentials.refreshToken;
|
|
23692
24193
|
parsed.auth.expiresAt = credentials.expiresAt;
|
|
23693
24194
|
parsed.auth.lastRefreshTime = Date.now();
|
|
23694
|
-
const sourceMode = process.platform === "win32" ? void 0 : (await
|
|
24195
|
+
const sourceMode = process.platform === "win32" ? void 0 : (await fs.stat(credentials.sourceFile)).mode & 511;
|
|
23695
24196
|
const tmp = `${credentials.sourceFile}.tmp.${process.pid}`;
|
|
23696
24197
|
try {
|
|
23697
|
-
await
|
|
24198
|
+
await fs.writeFile(tmp, JSON.stringify(parsed, null, 2), {
|
|
23698
24199
|
encoding: "utf8",
|
|
23699
24200
|
...sourceMode === void 0 ? {} : { mode: sourceMode }
|
|
23700
24201
|
});
|
|
23701
|
-
await
|
|
24202
|
+
await fs.rename(tmp, credentials.sourceFile);
|
|
23702
24203
|
} finally {
|
|
23703
|
-
await
|
|
24204
|
+
await fs.unlink(tmp).catch(() => void 0);
|
|
23704
24205
|
}
|
|
23705
|
-
credentials.sourceMtimeMs = (await
|
|
24206
|
+
credentials.sourceMtimeMs = (await fs.stat(credentials.sourceFile)).mtimeMs;
|
|
23706
24207
|
} catch {}
|
|
23707
24208
|
}
|
|
23708
24209
|
};
|
|
@@ -23717,7 +24218,7 @@ var FileModelSettingsStore$4 = class {
|
|
|
23717
24218
|
}
|
|
23718
24219
|
async read() {
|
|
23719
24220
|
try {
|
|
23720
|
-
const content = await
|
|
24221
|
+
const content = await fs.readFile(this.filePath, "utf8");
|
|
23721
24222
|
const parsed = JSON.parse(content);
|
|
23722
24223
|
if (isRecord$11(parsed)) {
|
|
23723
24224
|
const enabledModelIds = Array.isArray(parsed.enabledModelIds) ? parsed.enabledModelIds.filter((id) => typeof id === "string") : [...DEFAULT_ENABLED_MODEL_IDS$1];
|
|
@@ -23745,10 +24246,10 @@ var FileModelSettingsStore$4 = class {
|
|
|
23745
24246
|
};
|
|
23746
24247
|
}
|
|
23747
24248
|
async write(settings) {
|
|
23748
|
-
await
|
|
24249
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
23749
24250
|
const tmp = `${this.filePath}.tmp.${Date.now()}`;
|
|
23750
|
-
await
|
|
23751
|
-
await
|
|
24251
|
+
await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
|
|
24252
|
+
await fs.rename(tmp, this.filePath);
|
|
23752
24253
|
}
|
|
23753
24254
|
async updateSettings(patch) {
|
|
23754
24255
|
const current = await this.read();
|
|
@@ -26157,15 +26658,15 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
|
|
|
26157
26658
|
this.options_selection = options.selection;
|
|
26158
26659
|
}
|
|
26159
26660
|
/**
|
|
26160
|
-
*
|
|
26661
|
+
* Load the pool, refreshing the legacy-key index on the way.
|
|
26161
26662
|
*
|
|
26162
|
-
* Every routing decision goes through this method before it
|
|
26163
|
-
* settings selection, so building the index here is what keeps
|
|
26164
|
-
* before the identity fix resolvable when
|
|
26165
|
-
* for it.
|
|
26663
|
+
* Every routing decision and mutation goes through this method before it
|
|
26664
|
+
* consults the settings selection, so building the index here is what keeps
|
|
26665
|
+
* a pin written before the identity fix resolvable when
|
|
26666
|
+
* {@link resolveSelectionId} is asked for it.
|
|
26166
26667
|
*/
|
|
26167
|
-
async
|
|
26168
|
-
const data = await super.
|
|
26668
|
+
async loadPoolData() {
|
|
26669
|
+
const data = await super.loadPoolData();
|
|
26169
26670
|
this.legacyIds.clear();
|
|
26170
26671
|
for (const account of data.accounts) for (const alias of workBuddyAccountIdAliases(account.credentials)) if (alias !== account.id) this.legacyIds.set(alias, account.id);
|
|
26171
26672
|
return data;
|
|
@@ -26185,20 +26686,34 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
|
|
|
26185
26686
|
*/
|
|
26186
26687
|
async syncDesktopAccounts() {
|
|
26187
26688
|
const discovered = await this.store.discoverDesktopCredentials();
|
|
26188
|
-
|
|
26189
|
-
|
|
26190
|
-
|
|
26191
|
-
const
|
|
26192
|
-
|
|
26193
|
-
|
|
26194
|
-
|
|
26195
|
-
|
|
26196
|
-
|
|
26197
|
-
|
|
26198
|
-
|
|
26199
|
-
|
|
26689
|
+
const candidates = [];
|
|
26690
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26691
|
+
for (const raw of discovered) {
|
|
26692
|
+
const credentials = withResolvedIdentity({
|
|
26693
|
+
...raw,
|
|
26694
|
+
source: "desktop"
|
|
26695
|
+
});
|
|
26696
|
+
const id = workBuddyAccountId(credentials);
|
|
26697
|
+
if (seen.has(id)) continue;
|
|
26698
|
+
seen.add(id);
|
|
26699
|
+
candidates.push(credentials);
|
|
26700
|
+
}
|
|
26701
|
+
if (candidates.length > 0) await this.updatePool((data) => {
|
|
26702
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
26703
|
+
for (const account of data.accounts) {
|
|
26704
|
+
existingKeys.add(account.id);
|
|
26705
|
+
const key = this.hooks.dedupeKey?.(account.credentials);
|
|
26706
|
+
if (key !== void 0) existingKeys.add(key);
|
|
26200
26707
|
}
|
|
26201
|
-
|
|
26708
|
+
for (const credentials of candidates) {
|
|
26709
|
+
const key = this.hooks.dedupeKey?.(credentials);
|
|
26710
|
+
const id = this.hooks.accountId?.(credentials);
|
|
26711
|
+
if (key !== void 0 && existingKeys.has(key) || id !== void 0 && existingKeys.has(id)) continue;
|
|
26712
|
+
this.addAccountToPool(data, credentials);
|
|
26713
|
+
if (key !== void 0) existingKeys.add(key);
|
|
26714
|
+
if (id !== void 0) existingKeys.add(id);
|
|
26715
|
+
}
|
|
26716
|
+
});
|
|
26202
26717
|
return this.listAccounts();
|
|
26203
26718
|
}
|
|
26204
26719
|
/**
|
|
@@ -26214,7 +26729,13 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
|
|
|
26214
26729
|
const live = await this.store.readDesktopFile(effective.account.sourceFile);
|
|
26215
26730
|
if (live === null) return effective;
|
|
26216
26731
|
if (isExpired(live)) return effective;
|
|
26217
|
-
if (live.accessToken !== effective.credentials.accessToken)
|
|
26732
|
+
if (live.accessToken !== effective.credentials.accessToken) {
|
|
26733
|
+
const updated = await this.updateAccountCredentials(effective.account.id, live, effective.credentials).catch(() => void 0);
|
|
26734
|
+
if (updated) return {
|
|
26735
|
+
account: updated,
|
|
26736
|
+
credentials: updated.credentials
|
|
26737
|
+
};
|
|
26738
|
+
}
|
|
26218
26739
|
return {
|
|
26219
26740
|
account: effective.account,
|
|
26220
26741
|
credentials: live
|
|
@@ -26251,15 +26772,15 @@ var WorkBuddyAccountPool = class extends AccountPoolCore {
|
|
|
26251
26772
|
return super.addAccount(withResolvedIdentity(credentials), alias);
|
|
26252
26773
|
}
|
|
26253
26774
|
/**
|
|
26254
|
-
*
|
|
26775
|
+
* Remove one account inside a transaction.
|
|
26255
26776
|
*
|
|
26256
26777
|
* A desktop account is refused: the plugin does not own that credential file,
|
|
26257
26778
|
* so removing it from the pool would strand the IDE's session while telling
|
|
26258
26779
|
* the user their account was deleted. Hiding is the supported action there.
|
|
26259
26780
|
*/
|
|
26260
|
-
|
|
26261
|
-
if (
|
|
26262
|
-
|
|
26781
|
+
deleteAccountFromPool(data, accountId) {
|
|
26782
|
+
if (data.accounts.find((account) => account.id === accountId)?.source === "desktop") throw new Error("桌面账号由 CodeBuddy 客户端所有,只能隐藏,不能删除。");
|
|
26783
|
+
super.deleteAccountFromPool(data, accountId);
|
|
26263
26784
|
}
|
|
26264
26785
|
};
|
|
26265
26786
|
//#endregion
|
|
@@ -26352,6 +26873,25 @@ const CLAUDE_CODE_BETA = "claude-code-20250219";
|
|
|
26352
26873
|
/** Lets thinking blocks interleave with tool use across a multi-turn loop. */
|
|
26353
26874
|
const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
|
|
26354
26875
|
/**
|
|
26876
|
+
* Licenses the extended one-hour prompt-cache TTL.
|
|
26877
|
+
*
|
|
26878
|
+
* `cache_control: { type: 'ephemeral', ttl: '1h' }` is NOT accepted without this
|
|
26879
|
+
* beta: the one-hour tier is a separate licensed capability, and a body that
|
|
26880
|
+
* asks for it without the marker is refused the same way `block_binding` is.
|
|
26881
|
+
*/
|
|
26882
|
+
const EXTENDED_CACHE_TTL_BETA = "extended-cache-ttl-2025-04-11";
|
|
26883
|
+
/** A cache marker for one tier. Absent `ttl` means the five-minute default. */
|
|
26884
|
+
function cacheControlFor(ttl) {
|
|
26885
|
+
return {
|
|
26886
|
+
type: "ephemeral",
|
|
26887
|
+
ttl
|
|
26888
|
+
};
|
|
26889
|
+
}
|
|
26890
|
+
/** Whether a stored preference names a tier this line can send. */
|
|
26891
|
+
function isClaudeCacheTtl$1(value) {
|
|
26892
|
+
return value === "5m" || value === "1h";
|
|
26893
|
+
}
|
|
26894
|
+
/**
|
|
26355
26895
|
* Licenses `thinking.block_binding` in a request body.
|
|
26356
26896
|
*
|
|
26357
26897
|
* The field is not optional to license: the vendor documents that a body
|
|
@@ -27320,6 +27860,7 @@ function defaultClaudeSettings() {
|
|
|
27320
27860
|
enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS],
|
|
27321
27861
|
contextWindowOverrides: {},
|
|
27322
27862
|
defaultReasoningEffort: null,
|
|
27863
|
+
cacheTtl: null,
|
|
27323
27864
|
selectedAccountId: null
|
|
27324
27865
|
};
|
|
27325
27866
|
}
|
|
@@ -27555,12 +28096,14 @@ function parseClaudeModelSettings(value) {
|
|
|
27555
28096
|
for (const [model, window] of Object.entries(record.contextWindowOverrides)) if (typeof window === "number" && Number.isFinite(window) && window > 0) contextWindowOverrides[model] = window;
|
|
27556
28097
|
}
|
|
27557
28098
|
const defaultReasoningEffort = nonEmptyString$3(record.defaultReasoningEffort) ?? null;
|
|
28099
|
+
const cacheTtl = isClaudeCacheTtl$1(record.cacheTtl) ? record.cacheTtl : null;
|
|
27558
28100
|
const selectedAccountId = nonEmptyString$3(record.selectedAccountId) ?? null;
|
|
27559
28101
|
return {
|
|
27560
28102
|
enabled: record.enabled !== false,
|
|
27561
28103
|
enabledModelIds,
|
|
27562
28104
|
contextWindowOverrides,
|
|
27563
28105
|
defaultReasoningEffort,
|
|
28106
|
+
cacheTtl,
|
|
27564
28107
|
selectedAccountId
|
|
27565
28108
|
};
|
|
27566
28109
|
}
|
|
@@ -27571,6 +28114,7 @@ function mergeClaudeSettings(current, patch) {
|
|
|
27571
28114
|
enabledModelIds: patch.enabledModelIds !== void 0 ? [...patch.enabledModelIds] : current.enabledModelIds,
|
|
27572
28115
|
contextWindowOverrides: patch.contextWindowOverrides !== void 0 ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
|
|
27573
28116
|
defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort,
|
|
28117
|
+
cacheTtl: patch.cacheTtl !== void 0 ? patch.cacheTtl : current.cacheTtl,
|
|
27574
28118
|
selectedAccountId: patch.selectedAccountId !== void 0 ? patch.selectedAccountId : current.selectedAccountId
|
|
27575
28119
|
};
|
|
27576
28120
|
}
|
|
@@ -27643,7 +28187,7 @@ var FileCredentialStore = class {
|
|
|
27643
28187
|
}
|
|
27644
28188
|
async removeLegacy() {
|
|
27645
28189
|
try {
|
|
27646
|
-
await
|
|
28190
|
+
await fs.unlink(this.filePath);
|
|
27647
28191
|
} catch (error) {
|
|
27648
28192
|
if (error.code !== "ENOENT") throw new Error("Claude legacy credential removal failed");
|
|
27649
28193
|
}
|
|
@@ -27664,11 +28208,11 @@ var FileCredentialStore = class {
|
|
|
27664
28208
|
}
|
|
27665
28209
|
let legacy;
|
|
27666
28210
|
try {
|
|
27667
|
-
const stats = await
|
|
28211
|
+
const stats = await fs.lstat(this.filePath);
|
|
27668
28212
|
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
|
|
27669
28213
|
if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
|
|
27670
|
-
if (process.platform !== "win32") await
|
|
27671
|
-
legacy = await
|
|
28214
|
+
if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
|
|
28215
|
+
legacy = await fs.readFile(this.filePath, "utf8");
|
|
27672
28216
|
} catch (error) {
|
|
27673
28217
|
if (error.code === "ENOENT") return null;
|
|
27674
28218
|
throw new Error("Claude legacy credential read failed");
|
|
@@ -27776,17 +28320,17 @@ var FileModelSettingsStore = class {
|
|
|
27776
28320
|
}
|
|
27777
28321
|
async read() {
|
|
27778
28322
|
try {
|
|
27779
|
-
const content = await
|
|
28323
|
+
const content = await fs.readFile(this.filePath, "utf8");
|
|
27780
28324
|
return parseClaudeModelSettings(JSON.parse(content));
|
|
27781
28325
|
} catch {
|
|
27782
28326
|
return defaultClaudeSettings();
|
|
27783
28327
|
}
|
|
27784
28328
|
}
|
|
27785
28329
|
async write(settings) {
|
|
27786
|
-
await
|
|
28330
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
27787
28331
|
const tmp = this.filePath + ".tmp." + Date.now();
|
|
27788
|
-
await
|
|
27789
|
-
await
|
|
28332
|
+
await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
|
|
28333
|
+
await fs.rename(tmp, this.filePath);
|
|
27790
28334
|
}
|
|
27791
28335
|
/**
|
|
27792
28336
|
* Read-modify-write under the same per-path lock the credentials use.
|
|
@@ -27828,6 +28372,11 @@ function registerClaudePreferenceStore(settings, fallbackStore = new FileModelSe
|
|
|
27828
28372
|
enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS]),
|
|
27829
28373
|
contextWindowOverrides: z.dict(z.number()).default({}),
|
|
27830
28374
|
defaultReasoningEffort: z.union([z.string(), z.const(null)]).default(null),
|
|
28375
|
+
cacheTtl: z.union([
|
|
28376
|
+
z.const("5m"),
|
|
28377
|
+
z.const("1h"),
|
|
28378
|
+
z.const(null)
|
|
28379
|
+
]).default(null),
|
|
27831
28380
|
selectedAccountId: z.union([z.string(), z.const(null)]).default(null)
|
|
27832
28381
|
}));
|
|
27833
28382
|
return {
|
|
@@ -27838,6 +28387,7 @@ function registerClaudePreferenceStore(settings, fallbackStore = new FileModelSe
|
|
|
27838
28387
|
enabledModelIds: value.enabledModelIds,
|
|
27839
28388
|
contextWindowOverrides: value.contextWindowOverrides,
|
|
27840
28389
|
defaultReasoningEffort: value.defaultReasoningEffort,
|
|
28390
|
+
cacheTtl: isClaudeCacheTtl$1(value.cacheTtl) ? value.cacheTtl : null,
|
|
27841
28391
|
selectedAccountId: value.selectedAccountId ?? null
|
|
27842
28392
|
};
|
|
27843
28393
|
},
|
|
@@ -28033,6 +28583,7 @@ function claudeBetas(options = {}) {
|
|
|
28033
28583
|
betas.push(OAUTH_BETA);
|
|
28034
28584
|
if (options.thinking === true) betas.push(INTERLEAVED_THINKING_BETA);
|
|
28035
28585
|
if (options.thinkingBinding === true) betas.push(THINKING_BINDING_CONTROLS_BETA);
|
|
28586
|
+
if (options.cacheTtl === "1h") betas.push(EXTENDED_CACHE_TTL_BETA);
|
|
28036
28587
|
return betas;
|
|
28037
28588
|
}
|
|
28038
28589
|
/** The `User-Agent` value this line reports for one surface. */
|
|
@@ -29630,7 +30181,7 @@ function leadingSystemText(options) {
|
|
|
29630
30181
|
*
|
|
29631
30182
|
* @param cacheControl - false is the opt-OUT; the default requests caching.
|
|
29632
30183
|
*/
|
|
29633
|
-
function buildClaudeSystemBlocks(options, cacheControl = true) {
|
|
30184
|
+
function buildClaudeSystemBlocks(options, cacheControl = true, ttl = "5m") {
|
|
29634
30185
|
const blocks = [{
|
|
29635
30186
|
type: "text",
|
|
29636
30187
|
text: CLAUDE_CODE_IDENTITY_TEXT
|
|
@@ -29642,7 +30193,7 @@ function buildClaudeSystemBlocks(options, cacheControl = true) {
|
|
|
29642
30193
|
});
|
|
29643
30194
|
if (cacheControl) {
|
|
29644
30195
|
const last = blocks[blocks.length - 1];
|
|
29645
|
-
last.cache_control =
|
|
30196
|
+
last.cache_control = cacheControlFor(ttl);
|
|
29646
30197
|
}
|
|
29647
30198
|
return blocks;
|
|
29648
30199
|
}
|
|
@@ -29680,13 +30231,14 @@ function lastCacheableUserBlock(messages) {
|
|
|
29680
30231
|
* and has not been handed to a caller yet.
|
|
29681
30232
|
* @returns the number of breakpoints written.
|
|
29682
30233
|
*/
|
|
29683
|
-
function markClaudeCacheBreakpoints(body) {
|
|
30234
|
+
function markClaudeCacheBreakpoints(body, ttl = "5m") {
|
|
29684
30235
|
let marked = 0;
|
|
30236
|
+
const marker = cacheControlFor(ttl);
|
|
29685
30237
|
const system = body.system;
|
|
29686
30238
|
if (Array.isArray(system)) {
|
|
29687
30239
|
const last = system[system.length - 1];
|
|
29688
30240
|
if (last !== void 0) {
|
|
29689
|
-
last.cache_control = {
|
|
30241
|
+
last.cache_control = { ...marker };
|
|
29690
30242
|
marked += 1;
|
|
29691
30243
|
}
|
|
29692
30244
|
}
|
|
@@ -29694,7 +30246,7 @@ function markClaudeCacheBreakpoints(body) {
|
|
|
29694
30246
|
if (Array.isArray(messages)) {
|
|
29695
30247
|
const target = lastCacheableUserBlock(messages);
|
|
29696
30248
|
if (target !== void 0) {
|
|
29697
|
-
target.cache_control = {
|
|
30249
|
+
target.cache_control = { ...marker };
|
|
29698
30250
|
marked += 1;
|
|
29699
30251
|
}
|
|
29700
30252
|
}
|
|
@@ -29702,7 +30254,7 @@ function markClaudeCacheBreakpoints(body) {
|
|
|
29702
30254
|
if (Array.isArray(tools) && tools.length > 0) {
|
|
29703
30255
|
const last = tools[tools.length - 1];
|
|
29704
30256
|
if (last !== void 0) {
|
|
29705
|
-
last.cache_control = {
|
|
30257
|
+
last.cache_control = { ...marker };
|
|
29706
30258
|
marked += 1;
|
|
29707
30259
|
}
|
|
29708
30260
|
}
|
|
@@ -29936,11 +30488,12 @@ function buildClaudeRequestBody(options, images = NO_RESOLVED_IMAGES, request =
|
|
|
29936
30488
|
const thinkingEnabled = thinking !== void 0 && thinking.type !== "disabled";
|
|
29937
30489
|
const temperatureAllowed = claudeModelSupportsTemperature(options.model) && !thinkingEnabled;
|
|
29938
30490
|
const caching = request.cacheControl !== false;
|
|
30491
|
+
const cacheTtl = request.cacheTtl ?? "5m";
|
|
29939
30492
|
const body = {
|
|
29940
30493
|
model: options.model,
|
|
29941
30494
|
max_tokens: maxTokens,
|
|
29942
30495
|
stream: true,
|
|
29943
|
-
system: buildClaudeSystemBlocks(options, caching),
|
|
30496
|
+
system: buildClaudeSystemBlocks(options, caching, cacheTtl),
|
|
29944
30497
|
messages: mergeClaudeMessages(entries),
|
|
29945
30498
|
...options.tools !== void 0 && options.tools.length > 0 ? { tools: options.tools.map((tool) => wireTool(tool, names)) } : {},
|
|
29946
30499
|
...thinking === void 0 ? {} : { thinking },
|
|
@@ -29948,7 +30501,7 @@ function buildClaudeRequestBody(options, images = NO_RESOLVED_IMAGES, request =
|
|
|
29948
30501
|
...temperatureAllowed && options.temperature !== void 0 ? { temperature: options.temperature } : {},
|
|
29949
30502
|
...options.stop !== void 0 && options.stop.length > 0 ? { stop_sequences: [...options.stop] } : {}
|
|
29950
30503
|
};
|
|
29951
|
-
if (caching) markClaudeCacheBreakpoints(body);
|
|
30504
|
+
if (caching) markClaudeCacheBreakpoints(body, cacheTtl);
|
|
29952
30505
|
return body;
|
|
29953
30506
|
}
|
|
29954
30507
|
/**
|
|
@@ -31730,7 +32283,7 @@ var ClaudeAccountTokenStore = class {
|
|
|
31730
32283
|
await this.store.saveAccount(credentials, { internalId: this.internalId });
|
|
31731
32284
|
}
|
|
31732
32285
|
};
|
|
31733
|
-
var ClaudeAdapter = class extends LlmAdapter {
|
|
32286
|
+
var ClaudeAdapter = class ClaudeAdapter extends LlmAdapter {
|
|
31734
32287
|
store;
|
|
31735
32288
|
modelSettings;
|
|
31736
32289
|
preferences;
|
|
@@ -31912,10 +32465,25 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
31912
32465
|
* driven by {@link shouldRotateAccount}, which bypasses the pool ENTIRELY when
|
|
31913
32466
|
* no credentials were resolved for it.
|
|
31914
32467
|
*/
|
|
31915
|
-
|
|
32468
|
+
/**
|
|
32469
|
+
* The prompt-cache tier this line requests.
|
|
32470
|
+
*
|
|
32471
|
+
* A stored value wins; nothing stored falls back to the tier the OFFICIAL
|
|
32472
|
+
* client uses for a subscription's main conversation, so a user comparing
|
|
32473
|
+
* this plugin with Claude Code sees the same cache lifetime rather than half
|
|
32474
|
+
* of it.
|
|
32475
|
+
*
|
|
32476
|
+
* @param settings - the document the caller already read for this turn.
|
|
32477
|
+
*/
|
|
32478
|
+
static cacheTtlFor(settings) {
|
|
32479
|
+
return settings?.cacheTtl ?? "1h";
|
|
32480
|
+
}
|
|
32481
|
+
async attemptRequest(credentials, requestOptions, images, toolNames, thinking, settings, signal, fetchFn) {
|
|
32482
|
+
const cacheTtl = ClaudeAdapter.cacheTtlFor(settings);
|
|
31916
32483
|
const payload = buildClaudeRequestBody(requestOptions, images, {
|
|
31917
32484
|
toolNames,
|
|
31918
|
-
cacheControl: true
|
|
32485
|
+
cacheControl: true,
|
|
32486
|
+
cacheTtl
|
|
31919
32487
|
});
|
|
31920
32488
|
try {
|
|
31921
32489
|
return await fetchFn("https://api.anthropic.com/v1/messages?beta=true", {
|
|
@@ -31924,6 +32492,7 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
31924
32492
|
model: requestOptions.model,
|
|
31925
32493
|
thinking,
|
|
31926
32494
|
thinkingBinding: claudeBodyBindsThinking(payload),
|
|
32495
|
+
cacheTtl,
|
|
31927
32496
|
method: "POST"
|
|
31928
32497
|
}),
|
|
31929
32498
|
body: JSON.stringify(payload),
|
|
@@ -31942,6 +32511,7 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
31942
32511
|
assertClaudeCliVersionMeetsFloor(requestOptions.model, await this.catalogForRequest());
|
|
31943
32512
|
const effort = requestOptions.reasoningEffort === void 0 || requestOptions.reasoningEffort === null ? void 0 : String(requestOptions.reasoningEffort);
|
|
31944
32513
|
const thinking = claudeRequestThinks(requestOptions.model, effort);
|
|
32514
|
+
const settings = await this.settings().catch(() => null);
|
|
31945
32515
|
const pool = this.accountPool;
|
|
31946
32516
|
const tried = /* @__PURE__ */ new Set();
|
|
31947
32517
|
let response;
|
|
@@ -31961,7 +32531,7 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
31961
32531
|
let outputStarted = false;
|
|
31962
32532
|
while (true) {
|
|
31963
32533
|
const { credentials, accountId } = await this.resolveCredential(pool, tried, fetchFn, signal);
|
|
31964
|
-
response = await this.attemptRequest(credentials, requestOptions, images, toolNames, thinking, signal, fetchFn);
|
|
32534
|
+
response = await this.attemptRequest(credentials, requestOptions, images, toolNames, thinking, settings, signal, fetchFn);
|
|
31965
32535
|
if (response.ok) break;
|
|
31966
32536
|
const detail = (await response.text().catch(() => "")).slice(0, 2e3);
|
|
31967
32537
|
const failure = classifyFailure(response.status, detail, response.headers);
|
|
@@ -32577,7 +33147,7 @@ function isAdoptedCredentialExpired(credential, now = Date.now()) {
|
|
|
32577
33147
|
* caller in the process, so with a pool it would hand the second account the
|
|
32578
33148
|
* first account's freshly rotated credential — and, through the adapter above,
|
|
32579
33149
|
* write it into the second account's record. The pool therefore keeps its OWN
|
|
32580
|
-
* single-flight, per
|
|
33150
|
+
* single-flight, per pool file and account, which preserves the property
|
|
32581
33151
|
* the frozen module wanted (one rotation per expiry, no two callers spending the
|
|
32582
33152
|
* same rotating token) without the cross-account contamination.
|
|
32583
33153
|
*
|
|
@@ -32963,7 +33533,6 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
32963
33533
|
* marked expired immediately after a refresh that in fact succeeded. Keyed by
|
|
32964
33534
|
* the record's id — never by a token — so it cannot mix two accounts up.
|
|
32965
33535
|
*/
|
|
32966
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
32967
33536
|
constructor(options = {}) {
|
|
32968
33537
|
const store = options.store ?? new FileCredentialStore();
|
|
32969
33538
|
const refreshSeam = options.refreshCredentials ?? ((credentials, fetchFn) => refreshClaudeCredential(credentials, fetchFn));
|
|
@@ -33010,6 +33579,10 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33010
33579
|
if (record === void 0) return null;
|
|
33011
33580
|
return poolAccountForRecord(record, true);
|
|
33012
33581
|
},
|
|
33582
|
+
credentialsCommitted: async (account) => {
|
|
33583
|
+
if (isAdoptedPoolCredential(account.credentials)) return;
|
|
33584
|
+
await store.saveAccount(account.credentials, { internalId: account.id });
|
|
33585
|
+
},
|
|
33013
33586
|
mirrorPrimary: async (credentials) => {
|
|
33014
33587
|
if (credentials === null) {
|
|
33015
33588
|
await store.delete();
|
|
@@ -33071,8 +33644,8 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33071
33644
|
* {@link recordIdsFor} reads is kept in step with the rows that actually exist.
|
|
33072
33645
|
* See the recordIds field for why entries are not pruned.
|
|
33073
33646
|
*/
|
|
33074
|
-
async
|
|
33075
|
-
const data = await super.
|
|
33647
|
+
async loadPoolData() {
|
|
33648
|
+
const data = await super.loadPoolData();
|
|
33076
33649
|
for (const account of data.accounts) this.indexAccount(account);
|
|
33077
33650
|
return data;
|
|
33078
33651
|
}
|
|
@@ -33085,10 +33658,27 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33085
33658
|
* 'one account, one key' section.
|
|
33086
33659
|
*/
|
|
33087
33660
|
async addAccount(credentials, alias) {
|
|
33088
|
-
|
|
33661
|
+
return this.updatePool((data) => this.addManagedAccount(data, credentials, alias)).catch((error) => {
|
|
33662
|
+
this.recordIds.clear();
|
|
33663
|
+
throw error;
|
|
33664
|
+
});
|
|
33665
|
+
}
|
|
33666
|
+
async addManagedAccount(data, credentials, alias) {
|
|
33089
33667
|
if (isAdoptedPoolCredential(credentials)) return this.addAdoptedAccount(credentials, data, alias);
|
|
33668
|
+
parseClaudeCredentials(credentials);
|
|
33090
33669
|
const knownId = await this.recordIdFor(credentials);
|
|
33091
|
-
const
|
|
33670
|
+
const document = await this.store.read();
|
|
33671
|
+
const keys = identityKeysFor(credentials);
|
|
33672
|
+
const stored = document?.accounts.find((record) => record.identityKeys.some((key) => keys.includes(key)));
|
|
33673
|
+
const record = stored === void 0 ? {
|
|
33674
|
+
internalId: data.accounts.find((account) => account.id === knownId)?.adopted === false ? knownId ?? createInternalId() : createInternalId(),
|
|
33675
|
+
identityKeys: keys,
|
|
33676
|
+
credentials
|
|
33677
|
+
} : {
|
|
33678
|
+
...stored,
|
|
33679
|
+
identityKeys: mergeIdentityKeys(stored.identityKeys, keys),
|
|
33680
|
+
credentials
|
|
33681
|
+
};
|
|
33092
33682
|
this.indexRecord(record);
|
|
33093
33683
|
if (knownId !== void 0 && knownId !== record.internalId) {
|
|
33094
33684
|
const row = data.accounts.find((account) => account.id === knownId);
|
|
@@ -33100,10 +33690,9 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33100
33690
|
delete row.sourcePath;
|
|
33101
33691
|
row.identityKeys = mergeIdentityKeys(row.identityKeys, record.identityKeys);
|
|
33102
33692
|
if (data.activeAccountId === knownId) data.activeAccountId = record.internalId;
|
|
33103
|
-
await this.write(data);
|
|
33104
33693
|
}
|
|
33105
33694
|
}
|
|
33106
|
-
return
|
|
33695
|
+
return this.addAccountToPool(data, credentials, alias);
|
|
33107
33696
|
}
|
|
33108
33697
|
/**
|
|
33109
33698
|
* Add, or re-import, one borrowed Claude Code snapshot.
|
|
@@ -33132,9 +33721,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33132
33721
|
const candidateId = byIdentity ?? this.recordIds.get(sourceKey);
|
|
33133
33722
|
const existing = candidateId === void 0 ? void 0 : data.accounts.find((account) => account.id === candidateId);
|
|
33134
33723
|
if (existing !== void 0 && existing.adopted !== true) {
|
|
33135
|
-
const before = existing.identityKeys.length;
|
|
33136
33724
|
existing.identityKeys = mergeIdentityKeys(existing.identityKeys, identityKeysFor(credentials));
|
|
33137
|
-
if (existing.identityKeys.length !== before) await this.write(data);
|
|
33138
33725
|
return existing;
|
|
33139
33726
|
}
|
|
33140
33727
|
if (candidateId === void 0) {
|
|
@@ -33145,7 +33732,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33145
33732
|
this.recordIds.set(sourceKey, candidateId);
|
|
33146
33733
|
this.indexCredential(credentials, candidateId);
|
|
33147
33734
|
}
|
|
33148
|
-
return
|
|
33735
|
+
return this.addAccountToPool(data, credentials, alias);
|
|
33149
33736
|
}
|
|
33150
33737
|
/**
|
|
33151
33738
|
* Merge two rows that are one account.
|
|
@@ -33168,7 +33755,21 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33168
33755
|
*/
|
|
33169
33756
|
async mergeAccounts(keepAccountId, dropAccountId) {
|
|
33170
33757
|
if (keepAccountId === dropAccountId) throw new Error("A Claude pool merge needs two different accounts.");
|
|
33171
|
-
|
|
33758
|
+
let originals;
|
|
33759
|
+
return await this.updatePool(async (data) => {
|
|
33760
|
+
const keep = data.accounts.find((account) => account.id === keepAccountId);
|
|
33761
|
+
const drop = data.accounts.find((account) => account.id === dropAccountId);
|
|
33762
|
+
if (keep === void 0 || drop === void 0) throw new Error("The Claude pool merge addressed an account the pool does not have.");
|
|
33763
|
+
originals = {
|
|
33764
|
+
keep,
|
|
33765
|
+
drop
|
|
33766
|
+
};
|
|
33767
|
+
return this.mergeAccountsInPool(data, keepAccountId, dropAccountId);
|
|
33768
|
+
}, async (_data, result) => {
|
|
33769
|
+
if (originals !== void 0) await this.reconcileMergedDocument(originals.keep, originals.drop, result);
|
|
33770
|
+
});
|
|
33771
|
+
}
|
|
33772
|
+
async mergeAccountsInPool(data, keepAccountId, dropAccountId) {
|
|
33172
33773
|
const keep = data.accounts.find((account) => account.id === keepAccountId);
|
|
33173
33774
|
const drop = data.accounts.find((account) => account.id === dropAccountId);
|
|
33174
33775
|
if (keep === void 0 || drop === void 0) throw new Error("The Claude pool merge addressed an account the pool does not have.");
|
|
@@ -33217,9 +33818,6 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33217
33818
|
data.accounts[index] = merged;
|
|
33218
33819
|
data.accounts = data.accounts.filter((account) => account.id !== dropAccountId);
|
|
33219
33820
|
if (data.activeAccountId === dropAccountId) data.activeAccountId = merged.id;
|
|
33220
|
-
await this.write(data);
|
|
33221
|
-
if (merged.isPrimary) this.hooks.mirrorPrimary?.(merged.credentials).catch(() => void 0);
|
|
33222
|
-
await this.reconcileMergedDocument(keep, drop, merged);
|
|
33223
33821
|
return merged;
|
|
33224
33822
|
}
|
|
33225
33823
|
/**
|
|
@@ -33259,10 +33857,12 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33259
33857
|
* neither path.
|
|
33260
33858
|
*/
|
|
33261
33859
|
async removeImportedAccount(accountId) {
|
|
33262
|
-
|
|
33263
|
-
|
|
33264
|
-
|
|
33265
|
-
|
|
33860
|
+
return this.updatePool((data) => {
|
|
33861
|
+
const target = data.accounts.find((account) => account.id === accountId);
|
|
33862
|
+
if (target === void 0) return;
|
|
33863
|
+
if (target.adopted !== true) throw new Error("That account was signed in through this plugin; delete it instead of removing an import.");
|
|
33864
|
+
super.deleteAccountFromPool(data, accountId);
|
|
33865
|
+
});
|
|
33266
33866
|
}
|
|
33267
33867
|
/**
|
|
33268
33868
|
* Delete one plugin-owned account.
|
|
@@ -33275,9 +33875,12 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33275
33875
|
* back a credential the user just deleted.
|
|
33276
33876
|
*/
|
|
33277
33877
|
async deleteAccount(accountId) {
|
|
33278
|
-
|
|
33279
|
-
|
|
33280
|
-
|
|
33878
|
+
return this.updatePool((data) => {
|
|
33879
|
+
if (data.accounts.find((account) => account.id === accountId)?.adopted === true) throw new Error("The sign-in imported from Claude Code is not an account this plugin owns; remove the import instead.");
|
|
33880
|
+
super.deleteAccountFromPool(data, accountId);
|
|
33881
|
+
}, async () => {
|
|
33882
|
+
await this.store.deleteAccount(accountId);
|
|
33883
|
+
});
|
|
33281
33884
|
}
|
|
33282
33885
|
/**
|
|
33283
33886
|
* The immutable id of the record that owns a credential, or nothing.
|
|
@@ -33357,44 +33960,7 @@ var ClaudeAccountPool = class extends AccountPoolCore {
|
|
|
33357
33960
|
*/
|
|
33358
33961
|
refreshCredentials(credentials, fetchFn) {
|
|
33359
33962
|
if (isAdoptedPoolCredential(credentials)) return Promise.reject(new ClaudeAdoptedCredentialError(adoptedCredentialExpired(credentials) ? ADOPTED_CREDENTIAL_EXPIRED_HINT : ADOPTED_NEVER_REFRESHED_MESSAGE));
|
|
33360
|
-
return this.
|
|
33361
|
-
}
|
|
33362
|
-
/**
|
|
33363
|
-
* The per-account single-flight, resolving the account's id first.
|
|
33364
|
-
*
|
|
33365
|
-
* Separated from the hook above only because resolving the id consults the
|
|
33366
|
-
* credential document, and a hook cannot await: the guard stays synchronous so
|
|
33367
|
-
* it can never be skipped by a caller that forgets to wait.
|
|
33368
|
-
*/
|
|
33369
|
-
async singleFlightRefresh(credentials, fetchFn) {
|
|
33370
|
-
const key = await this.recordIdFor(credentials);
|
|
33371
|
-
if (key !== void 0) {
|
|
33372
|
-
const shared = this.inFlight.get(key);
|
|
33373
|
-
if (shared !== void 0) return shared;
|
|
33374
|
-
}
|
|
33375
|
-
const pending = this.performRefresh(credentials, fetchFn);
|
|
33376
|
-
if (key !== void 0) {
|
|
33377
|
-
this.inFlight.set(key, pending);
|
|
33378
|
-
pending.then(() => void 0, () => void 0).then(() => {
|
|
33379
|
-
if (this.inFlight.get(key) === pending) this.inFlight.delete(key);
|
|
33380
|
-
});
|
|
33381
|
-
}
|
|
33382
|
-
return pending;
|
|
33383
|
-
}
|
|
33384
|
-
/**
|
|
33385
|
-
* Rotate one managed credential and keep the credential document in step.
|
|
33386
|
-
*
|
|
33387
|
-
* The document write is what stops the pre-pool projection from handing out a
|
|
33388
|
-
* token the rotation has already spent. Its failure is swallowed on purpose:
|
|
33389
|
-
* the credential WAS refreshed, the pool's own record holds it, and a mirror
|
|
33390
|
-
* that could not be written must not fail a request that can still be served —
|
|
33391
|
-
* the same rule the core applies to its own bookkeeping writes.
|
|
33392
|
-
*/
|
|
33393
|
-
async performRefresh(credentials, fetchFn) {
|
|
33394
|
-
const recordId = await this.recordIdFor(credentials);
|
|
33395
|
-
const refreshed = await this.refreshSeam(credentials, fetchFn);
|
|
33396
|
-
if (recordId !== void 0) await new ClaudeCredentialStoreAdapter(this.store, recordId).write(refreshed).catch(() => void 0);
|
|
33397
|
-
return refreshed;
|
|
33963
|
+
return this.refreshSeam(credentials, fetchFn);
|
|
33398
33964
|
}
|
|
33399
33965
|
};
|
|
33400
33966
|
//#endregion
|
|
@@ -33435,6 +34001,16 @@ const CLAUDE_REASONING_EFFORTS = [
|
|
|
33435
34001
|
function isClaudeReasoningEffort(value) {
|
|
33436
34002
|
return typeof value === "string" && CLAUDE_REASONING_EFFORTS.includes(value);
|
|
33437
34003
|
}
|
|
34004
|
+
/**
|
|
34005
|
+
* Whether a posted value names a tier this route can send.
|
|
34006
|
+
*
|
|
34007
|
+
* Membership is checked at the wire boundary for the same reason effort levels
|
|
34008
|
+
* are: the one-hour tier is a LICENSED capability, so a value that reached the
|
|
34009
|
+
* body without the matching beta would be refused by the service.
|
|
34010
|
+
*/
|
|
34011
|
+
function isClaudeCacheTtl(value) {
|
|
34012
|
+
return value === "5m" || value === "1h";
|
|
34013
|
+
}
|
|
33438
34014
|
//#endregion
|
|
33439
34015
|
//#region src/host/claude/routes.ts
|
|
33440
34016
|
/** Ceiling on one request body. Identical to every sibling line's. */
|
|
@@ -33626,6 +34202,7 @@ async function getClaudeWebStatus(store, modelSettings, preferences, options = {
|
|
|
33626
34202
|
models,
|
|
33627
34203
|
contextWindowOverrides: settings.contextWindowOverrides,
|
|
33628
34204
|
defaultReasoningEffort: settings.defaultReasoningEffort,
|
|
34205
|
+
cacheTtl: settings.cacheTtl,
|
|
33629
34206
|
selectedAccountId: settings.selectedAccountId,
|
|
33630
34207
|
accounts: poolAccounts.map((summary) => summary),
|
|
33631
34208
|
rotationStrategy: poolData?.rotationStrategy ?? "sequential",
|
|
@@ -33904,6 +34481,10 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
|
|
|
33904
34481
|
const effort = body.defaultReasoningEffort;
|
|
33905
34482
|
if (effort === null || isClaudeReasoningEffort(effort)) patch.defaultReasoningEffort = effort;
|
|
33906
34483
|
}
|
|
34484
|
+
if (body.cacheTtl !== void 0) {
|
|
34485
|
+
const ttl = body.cacheTtl;
|
|
34486
|
+
if (ttl === null || isClaudeCacheTtl(ttl)) patch.cacheTtl = ttl;
|
|
34487
|
+
}
|
|
33907
34488
|
if (body.selectedAccountId !== void 0) {
|
|
33908
34489
|
const selected = body.selectedAccountId;
|
|
33909
34490
|
if (selected !== null && typeof selected !== "string") return sendJson(response, 400, {
|
|
@@ -34439,7 +35020,7 @@ var WorkBuddyCheckinService = class {
|
|
|
34439
35020
|
if (this.loaded) return;
|
|
34440
35021
|
this.loaded = true;
|
|
34441
35022
|
try {
|
|
34442
|
-
const parsed = JSON.parse(await
|
|
35023
|
+
const parsed = JSON.parse(await fs.readFile(this.statePath, "utf8"));
|
|
34443
35024
|
if (isRecord$2(parsed)) {
|
|
34444
35025
|
const lastRunAt = asCount$1(parsed.lastRunAt);
|
|
34445
35026
|
if (lastRunAt !== void 0) this.lastRunAt = lastRunAt;
|
|
@@ -34452,15 +35033,15 @@ var WorkBuddyCheckinService = class {
|
|
|
34452
35033
|
}
|
|
34453
35034
|
async save() {
|
|
34454
35035
|
try {
|
|
34455
|
-
await
|
|
35036
|
+
await fs.mkdir(path.dirname(this.statePath), { recursive: true });
|
|
34456
35037
|
const tmp = `${this.statePath}.tmp.${process.pid}`;
|
|
34457
35038
|
const document = {
|
|
34458
35039
|
version: 2,
|
|
34459
35040
|
lastRunAt: this.lastRunAt,
|
|
34460
35041
|
accounts: this.state
|
|
34461
35042
|
};
|
|
34462
|
-
await
|
|
34463
|
-
await
|
|
35043
|
+
await fs.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
|
|
35044
|
+
await fs.rename(tmp, this.statePath);
|
|
34464
35045
|
} catch {}
|
|
34465
35046
|
}
|
|
34466
35047
|
async post(url, headers) {
|
|
@@ -35108,7 +35689,7 @@ var MinimaxCodeCheckinService = class {
|
|
|
35108
35689
|
if (this.loaded) return;
|
|
35109
35690
|
this.loaded = true;
|
|
35110
35691
|
try {
|
|
35111
|
-
const parsed = JSON.parse(await
|
|
35692
|
+
const parsed = JSON.parse(await fs.readFile(this.statePath, "utf8"));
|
|
35112
35693
|
if (isRecord(parsed)) {
|
|
35113
35694
|
const lastRunAt = asCount(parsed.lastRunAt);
|
|
35114
35695
|
if (lastRunAt !== void 0) this.lastRunAt = lastRunAt;
|
|
@@ -35127,7 +35708,7 @@ var MinimaxCodeCheckinService = class {
|
|
|
35127
35708
|
}
|
|
35128
35709
|
async save() {
|
|
35129
35710
|
try {
|
|
35130
|
-
await
|
|
35711
|
+
await fs.mkdir(path.dirname(this.statePath), { recursive: true });
|
|
35131
35712
|
const tmp = `${this.statePath}.tmp.${process.pid}`;
|
|
35132
35713
|
const document = {
|
|
35133
35714
|
version: 1,
|
|
@@ -35135,8 +35716,8 @@ var MinimaxCodeCheckinService = class {
|
|
|
35135
35716
|
accounts: this.state,
|
|
35136
35717
|
identities: this.identities
|
|
35137
35718
|
};
|
|
35138
|
-
await
|
|
35139
|
-
await
|
|
35719
|
+
await fs.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
|
|
35720
|
+
await fs.rename(tmp, this.statePath);
|
|
35140
35721
|
} catch {}
|
|
35141
35722
|
}
|
|
35142
35723
|
};
|
|
@@ -37112,9 +37693,9 @@ function appendWindow(current, addition, limit) {
|
|
|
37112
37693
|
/**
|
|
37113
37694
|
* Install the reasoning-collapse guard on a Context.
|
|
37114
37695
|
*
|
|
37115
|
-
* The guard is inert unless the host exposes the
|
|
37116
|
-
*
|
|
37117
|
-
*
|
|
37696
|
+
* The guard is inert unless the host exposes the event bus, so a harness
|
|
37697
|
+
* without it keeps the built-in behaviour instead of failing this plugin's
|
|
37698
|
+
* load.
|
|
37118
37699
|
*
|
|
37119
37700
|
* Resuming needs the live Agent for `cancel` and `steer`, and no supported
|
|
37120
37701
|
* generation hands the agent to `llm/stream`. `agent/created` reports it on
|
|
@@ -37124,19 +37705,19 @@ function appendWindow(current, addition, limit) {
|
|
|
37124
37705
|
*
|
|
37125
37706
|
* @param ctx - plugin context.
|
|
37126
37707
|
* @param options - see {@link GuardOptions}; validated fail-loud.
|
|
37127
|
-
* @returns a disposer, or undefined when the
|
|
37708
|
+
* @returns a disposer removing both listeners, or undefined when the host
|
|
37709
|
+
* exposes no event bus.
|
|
37128
37710
|
*/
|
|
37129
37711
|
function installReasoningCollapseGuard(ctx, options = {}) {
|
|
37130
37712
|
const resolved = resolveGuardOptions(options);
|
|
37131
|
-
|
|
37132
|
-
if (runtime === void 0 || typeof runtime.stream !== "function") return void 0;
|
|
37133
|
-
const original = runtime.stream;
|
|
37713
|
+
if (typeof ctx.on !== "function") return void 0;
|
|
37134
37714
|
const watched = new Set(resolved.includeModels);
|
|
37135
37715
|
const breakers = /* @__PURE__ */ new WeakMap();
|
|
37136
37716
|
let currentAgent;
|
|
37137
|
-
const releaseAgent =
|
|
37138
|
-
currentAgent = agent;
|
|
37139
|
-
})
|
|
37717
|
+
const releaseAgent = ctx.on("agent/created", (payload) => {
|
|
37718
|
+
currentAgent = payload.agent;
|
|
37719
|
+
});
|
|
37720
|
+
const releaseStream = ctx.on("llm/stream", guardStream, { global: true });
|
|
37140
37721
|
function breakerFor(key) {
|
|
37141
37722
|
let state = breakers.get(key);
|
|
37142
37723
|
if (state === void 0) {
|
|
@@ -37174,7 +37755,7 @@ function installReasoningCollapseGuard(ctx, options = {}) {
|
|
|
37174
37755
|
agent.steer?.(createResumeMessage(text));
|
|
37175
37756
|
});
|
|
37176
37757
|
}
|
|
37177
|
-
|
|
37758
|
+
function guardStream(request, next) {
|
|
37178
37759
|
const state = breakerFor(request.signal ?? STATIC_KEY);
|
|
37179
37760
|
if (state.exhausted || !watchedModel(request.model)) return next();
|
|
37180
37761
|
async function* guarded() {
|
|
@@ -37196,9 +37777,9 @@ function installReasoningCollapseGuard(ctx, options = {}) {
|
|
|
37196
37777
|
}
|
|
37197
37778
|
}
|
|
37198
37779
|
return guarded();
|
|
37199
|
-
}
|
|
37780
|
+
}
|
|
37200
37781
|
return () => {
|
|
37201
|
-
|
|
37782
|
+
releaseListener(releaseStream);
|
|
37202
37783
|
releaseListener(releaseAgent);
|
|
37203
37784
|
};
|
|
37204
37785
|
}
|
|
@@ -37290,7 +37871,7 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
37290
37871
|
ctx.effect(() => {
|
|
37291
37872
|
const dispose = installReasoningCollapseGuard(ctx, guardOptions);
|
|
37292
37873
|
if (dispose === void 0) {
|
|
37293
|
-
ctx.logger.warn("[dsh-chatgpt-subscription] reasoning-collapse guard skipped: this harness exposes no
|
|
37874
|
+
ctx.logger.warn("[dsh-chatgpt-subscription] reasoning-collapse guard skipped: this harness exposes no event bus");
|
|
37294
37875
|
return () => void 0;
|
|
37295
37876
|
}
|
|
37296
37877
|
return dispose;
|
|
@@ -37528,7 +38109,10 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
37528
38109
|
outputVerbosity: () => preferences.status().outputVerbosity,
|
|
37529
38110
|
fastMode: () => preferences.status().fastMode,
|
|
37530
38111
|
reasoningSummary: () => preferences.status().reasoningSummary
|
|
37531
|
-
}), preferences)
|
|
38112
|
+
}), preferences, (options) => oauth.credentials(void 0, { purpose: "tool" }).then((credentials) => loadCodexCatalog(credentials, {
|
|
38113
|
+
fetchFn: proxyFetch,
|
|
38114
|
+
signal: options?.signal
|
|
38115
|
+
})).catch(() => []));
|
|
37532
38116
|
const searchSwitcher = new SearchProviderSwitcher(ctx.loader);
|
|
37533
38117
|
const applyWebProviders = (current = preferences.status()) => {
|
|
37534
38118
|
const pluginFetch = proxyManager.resolveActiveProxyUrl() !== null;
|