@eddyskywalker/dsh-chatgpt-subscription 0.10.12 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +176 -42
  3. package/lib/client.js +133 -26
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +710 -280
  6. package/lib/types/client/claude/ClaudeSection.d.ts.map +1 -1
  7. package/lib/types/client/claude/locales.d.ts +17 -2
  8. package/lib/types/client/claude/locales.d.ts.map +1 -1
  9. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  10. package/lib/types/client/kimi-code/locales.d.ts +17 -2
  11. package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
  12. package/lib/types/compat.d.ts +35 -2
  13. package/lib/types/compat.d.ts.map +1 -1
  14. package/lib/types/host/adapter.d.ts +20 -1
  15. package/lib/types/host/adapter.d.ts.map +1 -1
  16. package/lib/types/host/claude/adapter.d.ts +11 -0
  17. package/lib/types/host/claude/adapter.d.ts.map +1 -1
  18. package/lib/types/host/claude/client.d.ts +7 -2
  19. package/lib/types/host/claude/client.d.ts.map +1 -1
  20. package/lib/types/host/claude/mapper.d.ts +13 -1
  21. package/lib/types/host/claude/mapper.d.ts.map +1 -1
  22. package/lib/types/host/claude/routes.d.ts.map +1 -1
  23. package/lib/types/host/claude/token-store.d.ts +9 -0
  24. package/lib/types/host/claude/token-store.d.ts.map +1 -1
  25. package/lib/types/host/claude/types.d.ts +35 -0
  26. package/lib/types/host/claude/types.d.ts.map +1 -1
  27. package/lib/types/host/codex-catalog.d.ts +54 -0
  28. package/lib/types/host/codex-catalog.d.ts.map +1 -0
  29. package/lib/types/host/codex-images.d.ts.map +1 -1
  30. package/lib/types/host/codex-search.d.ts.map +1 -1
  31. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  32. package/lib/types/host/kimi-code/mapper.d.ts +13 -7
  33. package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
  34. package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
  35. package/lib/types/host/kimi-code/token-store.d.ts +11 -1
  36. package/lib/types/host/kimi-code/token-store.d.ts.map +1 -1
  37. package/lib/types/host/kimi-code/types.d.ts +13 -0
  38. package/lib/types/host/kimi-code/types.d.ts.map +1 -1
  39. package/lib/types/host/model-catalog.d.ts +3 -2
  40. package/lib/types/host/model-catalog.d.ts.map +1 -1
  41. package/lib/types/host/responses-client.d.ts +24 -0
  42. package/lib/types/host/responses-client.d.ts.map +1 -1
  43. package/lib/types/host/responses-mapper.d.ts +5 -1
  44. package/lib/types/host/responses-mapper.d.ts.map +1 -1
  45. package/lib/types/host/wire-auth.d.ts +14 -1
  46. package/lib/types/host/wire-auth.d.ts.map +1 -1
  47. package/lib/types/index.d.ts.map +1 -1
  48. package/lib/types/shared/claude-contracts.d.ts +19 -0
  49. package/lib/types/shared/claude-contracts.d.ts.map +1 -1
  50. package/lib/types/shared/kimi-code-contracts.d.ts +16 -0
  51. package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
  52. 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 fsPromises, { chmod, lstat, mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
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
- const OAUTH_ORIGINATOR = "opencode";
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 visible = new Set(status?.visibleModelIds ?? CODEX_MODEL_CATALOG.map((entry) => entry.id));
277
- return CODEX_MODEL_CATALOG.filter((entry) => visible.has(entry.id)).map((entry) => ({
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 efforts = reasoningEffortsForModel(model);
288
- const defaultEffort = efforts.includes(entry.defaultReasoningEffort) ? entry.defaultReasoningEffort : efforts[0];
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
- constructor(client, preferences) {
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.
@@ -2752,31 +3146,6 @@ function normalizeGenerateOptions(options) {
2752
3146
  };
2753
3147
  }
2754
3148
  //#endregion
2755
- //#region src/host/wire-auth.ts
2756
- function codexHeaders(credentials, sessionId) {
2757
- const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
2758
- return {
2759
- authorization: `Bearer ${credentials.accessToken}`,
2760
- ...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
2761
- originator: CODEX_ORIGINATOR,
2762
- "user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
2763
- ...sessionId ? { "session-id": sessionId } : {}
2764
- };
2765
- }
2766
- function stableSessionId(value) {
2767
- const source = value === void 0 || value === "" ? randomUUID() : value;
2768
- return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
2769
- }
2770
- function retryAfterMs$1(headers) {
2771
- const raw = headers.get("retry-after");
2772
- if (raw === null) return void 0;
2773
- const seconds = Number(raw);
2774
- if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
2775
- const timestamp = Date.parse(raw);
2776
- if (!Number.isFinite(timestamp)) return void 0;
2777
- return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
2778
- }
2779
- //#endregion
2780
3149
  //#region src/host/codex-images.ts
2781
3150
  const PNG_SIGNATURE = [
2782
3151
  137,
@@ -2930,7 +3299,6 @@ function requestImage(fetchFn, credentials, prompt, turnId, signal) {
2930
3299
  method: "POST",
2931
3300
  headers: {
2932
3301
  ...codexHeaders(credentials),
2933
- originator: "pi",
2934
3302
  accept: "application/json",
2935
3303
  "content-type": "application/json",
2936
3304
  "x-codex-image-turn-id": turnId
@@ -3030,7 +3398,6 @@ function sendSearch(fetchFn, credentials, query, model, id, signal) {
3030
3398
  method: "POST",
3031
3399
  headers: {
3032
3400
  ...codexHeaders(credentials),
3033
- originator: "pi",
3034
3401
  accept: "application/json",
3035
3402
  "content-type": "application/json"
3036
3403
  },
@@ -3183,8 +3550,8 @@ function parseEnvProxy(env = process.env, options = {}) {
3183
3550
  try {
3184
3551
  const dshHome = env.DSH_HOME || path.join(os.homedir(), ".dsh");
3185
3552
  const envFile = options.envFile ?? path.join(dshHome, ".env");
3186
- if (fs.existsSync(envFile)) {
3187
- 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);
3188
3555
  if (match && match[1]?.trim()) return normalizeProxyUrl(match[1].trim());
3189
3556
  }
3190
3557
  } catch {}
@@ -3805,7 +4172,7 @@ var FileCredentialStore$2 = class {
3805
4172
  }
3806
4173
  async removeLegacy() {
3807
4174
  try {
3808
- await fsPromises.unlink(this.filePath);
4175
+ await fs.unlink(this.filePath);
3809
4176
  } catch (error) {
3810
4177
  if (error.code !== "ENOENT") throw new Error("Antigravity legacy credential removal failed");
3811
4178
  }
@@ -3824,11 +4191,11 @@ var FileCredentialStore$2 = class {
3824
4191
  }
3825
4192
  let legacy;
3826
4193
  try {
3827
- const stats = await fsPromises.lstat(this.filePath);
4194
+ const stats = await fs.lstat(this.filePath);
3828
4195
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
3829
4196
  if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
3830
- if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
3831
- legacy = await fsPromises.readFile(this.filePath, "utf8");
4197
+ if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
4198
+ legacy = await fs.readFile(this.filePath, "utf8");
3832
4199
  } catch (error) {
3833
4200
  if (error.code === "ENOENT") return null;
3834
4201
  throw new Error("Antigravity legacy credential read failed");
@@ -3863,7 +4230,7 @@ var FileModelSettingsStore$2 = class {
3863
4230
  }
3864
4231
  async read() {
3865
4232
  try {
3866
- const content = await fsPromises.readFile(this.filePath, "utf8");
4233
+ const content = await fs.readFile(this.filePath, "utf8");
3867
4234
  const parsed = JSON.parse(content);
3868
4235
  if (typeof parsed === "object" && parsed !== null) {
3869
4236
  const record = parsed;
@@ -3889,10 +4256,10 @@ var FileModelSettingsStore$2 = class {
3889
4256
  };
3890
4257
  }
3891
4258
  async write(settings) {
3892
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
4259
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
3893
4260
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
3894
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
3895
- await fsPromises.rename(tmp, this.filePath);
4261
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
4262
+ await fs.rename(tmp, this.filePath);
3896
4263
  }
3897
4264
  async updateSettings(patch) {
3898
4265
  const current = await this.read();
@@ -4018,7 +4385,7 @@ var FilePreferencesStore = class {
4018
4385
  async readDocument() {
4019
4386
  let parsed;
4020
4387
  try {
4021
- parsed = JSON.parse(await fsPromises.readFile(this.filePath, "utf8"));
4388
+ parsed = JSON.parse(await fs.readFile(this.filePath, "utf8"));
4022
4389
  } catch {
4023
4390
  return;
4024
4391
  }
@@ -4039,17 +4406,17 @@ var FilePreferencesStore = class {
4039
4406
  for (const listener of [...this.listeners]) listener(next, prev);
4040
4407
  }
4041
4408
  async write(value) {
4042
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
4409
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
4043
4410
  writeSequence += 1;
4044
4411
  const tmp = `${this.filePath}.tmp.${process.pid}.${writeSequence}`;
4045
4412
  try {
4046
- await fsPromises.writeFile(tmp, JSON.stringify(value, null, 2), {
4413
+ await fs.writeFile(tmp, JSON.stringify(value, null, 2), {
4047
4414
  encoding: "utf8",
4048
4415
  mode: 384
4049
4416
  });
4050
- await fsPromises.rename(tmp, this.filePath);
4417
+ await fs.rename(tmp, this.filePath);
4051
4418
  } finally {
4052
- await fsPromises.unlink(tmp).catch(() => void 0);
4419
+ await fs.unlink(tmp).catch(() => void 0);
4053
4420
  }
4054
4421
  }
4055
4422
  };
@@ -4114,7 +4481,7 @@ const LEGACY_DOCUMENTS = ["settings.yaml", "settings.yaml.imported"];
4114
4481
  */
4115
4482
  async function readLegacyPreferences(homeDir = dshHomeDir$2()) {
4116
4483
  for (const name of LEGACY_DOCUMENTS) {
4117
- const document = await fsPromises.readFile(path.join(homeDir, name), "utf8").catch(() => void 0);
4484
+ const document = await fs.readFile(path.join(homeDir, name), "utf8").catch(() => void 0);
4118
4485
  if (document === void 0) continue;
4119
4486
  const section = sectionOf(document, PREFERENCES_NAMESPACE);
4120
4487
  if (section !== void 0 && Object.keys(section).length > 0) return section;
@@ -4422,7 +4789,7 @@ function normalizeReplayCallIds(items, normalizeCallId) {
4422
4789
  call_id: normalizeCallId(item.call_id)
4423
4790
  } : item);
4424
4791
  }
4425
- 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) {
4426
4793
  const sandboxRetryTools = recentSandboxRetryToolNames(options.messages);
4427
4794
  const resolveLocalRawImages = supportsImageInput(options);
4428
4795
  const normalizeCallId = createCallIdNormalizer();
@@ -4490,7 +4857,8 @@ async function buildResponsesPayload(options, attachments, localRawImages = {},
4490
4857
  input,
4491
4858
  stream: true,
4492
4859
  store: false,
4493
- include: ["reasoning.encrypted_content"]
4860
+ include: ["reasoning.encrypted_content"],
4861
+ ...promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}
4494
4862
  };
4495
4863
  const instructions = [...instructionParts, localRawImageInstruction(localImageStats)].filter((value) => Boolean(value));
4496
4864
  if (instructions.length > 0) payload.instructions = instructions.join("\n\n");
@@ -4506,6 +4874,8 @@ async function buildResponsesPayload(options, attachments, localRawImages = {},
4506
4874
  }
4507
4875
  if (outputVerbosity !== null) payload.text = { verbosity: outputVerbosity };
4508
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);
4509
4879
  if (options.reasoningEffort !== void 0) {
4510
4880
  const effort = codexWireReasoningEffort(options.model, options.reasoningEffort);
4511
4881
  payload.reasoning = codexModelSupportsReasoningSummary(options.model) ? {
@@ -4823,6 +5193,8 @@ const MAX_VISIBLE_REASONING_CHARS = 12e3;
4823
5193
  const DEFAULT_POOL_COOLDOWN_MS = 15 * 6e4;
4824
5194
  const REASONING_DELTA_FLUSH_CHARS = 768;
4825
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;
4826
5198
  var ResponsesClient = class {
4827
5199
  oauth;
4828
5200
  attachments;
@@ -4832,6 +5204,16 @@ var ResponsesClient = class {
4832
5204
  fastMode;
4833
5205
  reasoningSummary;
4834
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();
4835
5217
  constructor(oauth, attachments, options = {}) {
4836
5218
  this.oauth = oauth;
4837
5219
  this.attachments = attachments;
@@ -4852,7 +5234,7 @@ var ResponsesClient = class {
4852
5234
  let attemptOptions = options;
4853
5235
  let response;
4854
5236
  while (true) {
4855
- 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));
4856
5238
  try {
4857
5239
  response = await this.send(payload, sessionId, options.signal);
4858
5240
  break;
@@ -4887,9 +5269,36 @@ var ResponsesClient = class {
4887
5269
  response = await this.request(payload, credentials, sessionId, signal);
4888
5270
  }
4889
5271
  if (!response.ok) throw await responseError(response);
5272
+ this.rememberTurnState(response, sessionId);
4890
5273
  return response;
4891
5274
  }
4892
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
+ /**
4893
5302
  * Send one payload with account rotation.
4894
5303
  *
4895
5304
  * The pool picks the account — skipping cooling, quota-exhausted and failed
@@ -4931,6 +5340,7 @@ var ResponsesClient = class {
4931
5340
  });
4932
5341
  }
4933
5342
  if (!response.ok) throw await responseError(response);
5343
+ this.rememberTurnState(response, sessionId);
4934
5344
  return response;
4935
5345
  }
4936
5346
  }
@@ -4939,7 +5349,7 @@ var ResponsesClient = class {
4939
5349
  return await this.fetchFn(CODEX_RESPONSES_URL, {
4940
5350
  method: "POST",
4941
5351
  headers: {
4942
- ...codexHeaders(credentials, sessionId),
5352
+ ...codexHeaders(credentials, sessionId, { turnState: this.turnStates.get(sessionId) }),
4943
5353
  "content-type": "application/json",
4944
5354
  accept: "text/event-stream"
4945
5355
  },
@@ -4952,6 +5362,17 @@ var ResponsesClient = class {
4952
5362
  }
4953
5363
  }
4954
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
+ }
4955
5376
  async function* parseResponsesStream(response, signal, hiddenSandboxControls = /* @__PURE__ */ new Set()) {
4956
5377
  if (response.body === null) throw new LlmError("Codex returned no response stream.", "PROVIDER_ERROR");
4957
5378
  const reader = response.body.getReader();
@@ -5943,7 +6364,7 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
5943
6364
  "Content-Type": "application/javascript; charset=utf-8",
5944
6365
  "Cache-Control": "public, max-age=86400"
5945
6366
  });
5946
- fs.createReadStream(mermaidPath).pipe(response);
6367
+ fs$1.createReadStream(mermaidPath).pipe(response);
5947
6368
  } catch {
5948
6369
  response.writeHead(404, { "Content-Type": "text/plain" });
5949
6370
  response.end("Not found");
@@ -9509,7 +9930,7 @@ var FileCredentialStore$1 = class {
9509
9930
  }
9510
9931
  async removeLegacy() {
9511
9932
  try {
9512
- await fsPromises.unlink(this.filePath);
9933
+ await fs.unlink(this.filePath);
9513
9934
  } catch (error) {
9514
9935
  if (error.code !== "ENOENT") throw new Error("Command Code legacy credential removal failed");
9515
9936
  }
@@ -9528,11 +9949,11 @@ var FileCredentialStore$1 = class {
9528
9949
  }
9529
9950
  let legacy;
9530
9951
  try {
9531
- const stats = await fsPromises.lstat(this.filePath);
9952
+ const stats = await fs.lstat(this.filePath);
9532
9953
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
9533
9954
  if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
9534
- if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
9535
- legacy = await fsPromises.readFile(this.filePath, "utf8");
9955
+ if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
9956
+ legacy = await fs.readFile(this.filePath, "utf8");
9536
9957
  } catch (error) {
9537
9958
  if (error.code === "ENOENT") return null;
9538
9959
  throw new Error("Command Code legacy credential read failed");
@@ -9568,7 +9989,7 @@ var FileModelSettingsStore$1 = class {
9568
9989
  }
9569
9990
  async read() {
9570
9991
  try {
9571
- const content = await fsPromises.readFile(this.filePath, "utf8");
9992
+ const content = await fs.readFile(this.filePath, "utf8");
9572
9993
  const parsed = JSON.parse(content);
9573
9994
  if (typeof parsed === "object" && parsed !== null) {
9574
9995
  const record = parsed;
@@ -9594,10 +10015,10 @@ var FileModelSettingsStore$1 = class {
9594
10015
  };
9595
10016
  }
9596
10017
  async write(settings) {
9597
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
10018
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
9598
10019
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
9599
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
9600
- await fsPromises.rename(tmp, this.filePath);
10020
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
10021
+ await fs.rename(tmp, this.filePath);
9601
10022
  }
9602
10023
  async updateSettings(patch) {
9603
10024
  const current = await this.read();
@@ -9696,121 +10117,6 @@ function commandCodePlanLabel(planId) {
9696
10117
  return resolveCommandCodePlan(planId)?.name ?? planId;
9697
10118
  }
9698
10119
  //#endregion
9699
- //#region src/host/common/catalog-snapshot.ts
9700
- /**
9701
- * Persistent snapshot for one provider's live model catalog.
9702
- *
9703
- * The in-memory catalog caches keep a process alive between requests, but they
9704
- * start cold: on the first call after a restart (or a plugin reinstall) the
9705
- * picker and the settings page both wait on the upstream listing before they
9706
- * can render anything, and an unreachable endpoint turns that wait into the
9707
- * full discovery timeout — "the model server is unavailable" on a fresh
9708
- * install. The catalog is not fast-moving data, so the last successful listing
9709
- * is persisted to `<DSH_HOME>/storages/` and rehydrated into the in-memory
9710
- * cache on first use. A snapshot the caller can read is therefore available
9711
- * from the very first call, with no network in the path.
9712
- *
9713
- * Writes are best-effort: persistence is an optimization, never a capability.
9714
- * A read-only home or a locked file must not fail the fetch that produced the
9715
- * snapshot.
9716
- */
9717
- /** File name of the persisted snapshot for one catalog. */
9718
- function catalogSnapshotPath(name) {
9719
- return path.join(dshHomeDir(), "storages", `${name}-catalog.json`);
9720
- }
9721
- /**
9722
- * Snapshot name for one catalog *and* one credential scope.
9723
- *
9724
- * A persisted snapshot answers before any credential is read — that is the whole
9725
- * point of it — so the file itself has to carry the scope it was fetched under.
9726
- * Otherwise the first call after a restart, for any account, is answered by
9727
- * whatever listing some other scope happened to persist last: a Kimi listing
9728
- * fetched for the mainland-cn endpoint would answer a global caller with models
9729
- * and context windows that region does not serve, and a Command Code listing
9730
- * fetched against one API environment would answer the other.
9731
- *
9732
- * The scope is part of the file name rather than of the payload because a caller
9733
- * that cannot name the account — the harness resolves this catalog with no token
9734
- * at all — still has to find the snapshot its own scope owns.
9735
- *
9736
- * Everything outside `[a-z0-9]` is folded to `_`, so no scope can escape the
9737
- * `storages` directory.
9738
- */
9739
- function catalogSnapshotName(base, scope) {
9740
- const normalized = scope.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
9741
- return normalized === "" ? base : `${base}-${normalized}`;
9742
- }
9743
- /**
9744
- * Read the persisted snapshot for one catalog.
9745
- *
9746
- * Shape errors and unreadable files both return `undefined` — the caller keeps
9747
- * its in-memory state, and the next successful fetch overwrites the file.
9748
- */
9749
- async function readCatalogSnapshot(name, parseModels) {
9750
- try {
9751
- const content = await fsPromises.readFile(catalogSnapshotPath(name), "utf8");
9752
- const parsed = JSON.parse(content);
9753
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
9754
- const record = parsed;
9755
- const fetchedAt = record.fetchedAt;
9756
- if (typeof fetchedAt !== "number" || !Number.isFinite(fetchedAt) || fetchedAt <= 0) return void 0;
9757
- const models = parseModels(record.models);
9758
- if (models === void 0 || models.length === 0) return void 0;
9759
- return {
9760
- fetchedAt,
9761
- models
9762
- };
9763
- } catch {
9764
- return;
9765
- }
9766
- }
9767
- /**
9768
- * Snapshot writes still in flight.
9769
- *
9770
- * Callers fire-and-forget these writes, so nothing in production ever awaits
9771
- * one. A test that deletes its private `storages` directory between cases
9772
- * cannot know when that is safe, and a write that lands after the delete
9773
- * recreates the file the next case asserts is absent — which is how a stale
9774
- * listing leaked into a later test and failed it on CI. Registering each write
9775
- * gives {@link flushCatalogSnapshots} a deterministic point to wait on.
9776
- */
9777
- const pendingSnapshotWrites = /* @__PURE__ */ new Set();
9778
- /** Persist one snapshot; a failure is logged nowhere and never thrown. */
9779
- function writeCatalogSnapshot(name, models, fetchedAt) {
9780
- const write = (async () => {
9781
- try {
9782
- const filePath = catalogSnapshotPath(name);
9783
- await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
9784
- const tmp = `${filePath}.tmp.${Date.now()}`;
9785
- await fsPromises.writeFile(tmp, JSON.stringify({
9786
- fetchedAt,
9787
- models
9788
- }, null, 2), "utf8");
9789
- await fsPromises.rename(tmp, filePath);
9790
- } catch {}
9791
- })();
9792
- pendingSnapshotWrites.add(write);
9793
- write.finally(() => {
9794
- pendingSnapshotWrites.delete(write);
9795
- });
9796
- return write;
9797
- }
9798
- /**
9799
- * Warm an in-memory cache from the persisted snapshot when it is fresher than
9800
- * what the cache currently holds. Called lazily on the first cache miss of a
9801
- * process, so a restart serves the previous listing without waiting on the
9802
- * network.
9803
- *
9804
- * @returns the snapshot's `fetchedAt`, or `0` when no usable snapshot exists —
9805
- * the caller then fetches as before.
9806
- */
9807
- async function rehydrateCatalogCache(name, parseModels, accept, currentFetchedAt) {
9808
- const snapshot = await readCatalogSnapshot(name, parseModels);
9809
- if (snapshot === void 0 || snapshot.fetchedAt <= currentFetchedAt) return 0;
9810
- accept(snapshot.fetchedAt, snapshot.models);
9811
- return snapshot.fetchedAt;
9812
- }
9813
- //#endregion
9814
10120
  //#region src/host/command-code/client.ts
9815
10121
  const CLI_VERSION = "1.0.0";
9816
10122
  /**
@@ -13080,6 +13386,10 @@ function maxOutputTokensFor$3(modelId, contextWindow) {
13080
13386
  const capped = (contextWindow ?? kimiCodeModelDef(modelId)?.contextWindow ?? 262144) - CONTEXT_HEADROOM_TOKENS$1;
13081
13387
  return Math.max(declared, capped);
13082
13388
  }
13389
+ /** Whether a stored preference names a tier this line can send. */
13390
+ function isKimiCacheTtl(value) {
13391
+ return value === "5m" || value === "1h";
13392
+ }
13083
13393
  /**
13084
13394
  * Reduce the requested output cap so prompt + output fit the window.
13085
13395
  *
@@ -13129,7 +13439,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
13129
13439
  enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$3],
13130
13440
  catalogModels: [],
13131
13441
  contextWindowOverrides: {},
13132
- defaultReasoningEffort: null
13442
+ defaultReasoningEffort: null,
13443
+ cacheTtl: null
13133
13444
  };
13134
13445
  fallbackStore.read().then((stored) => {
13135
13446
  snapshot = stored;
@@ -13146,7 +13457,12 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
13146
13457
  enabled: z.boolean().default(true),
13147
13458
  enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS$3]),
13148
13459
  contextWindowOverrides: z.dict(z.number()).default({}),
13149
- 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)
13150
13466
  }));
13151
13467
  return {
13152
13468
  status: () => {
@@ -13156,7 +13472,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
13156
13472
  enabledModelIds: value.enabledModelIds,
13157
13473
  catalogModels: [],
13158
13474
  contextWindowOverrides: value.contextWindowOverrides,
13159
- defaultReasoningEffort: value.defaultReasoningEffort
13475
+ defaultReasoningEffort: value.defaultReasoningEffort,
13476
+ cacheTtl: isKimiCacheTtl(value.cacheTtl) ? value.cacheTtl : null
13160
13477
  };
13161
13478
  },
13162
13479
  update: async (patch) => {
@@ -13165,7 +13482,8 @@ function registerKimiCodePreferenceStore(settings, fallbackStore = new FileModel
13165
13482
  enabled: patch.enabled !== void 0 ? patch.enabled : current.enabled !== false,
13166
13483
  enabledModelIds: patch.enabledModelIds ?? current.enabledModelIds,
13167
13484
  contextWindowOverrides: patch.contextWindowOverrides ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
13168
- 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
13169
13487
  };
13170
13488
  await scope.update(normalized);
13171
13489
  fallbackStore.updateSettings(patch).catch(() => void 0);
@@ -13287,7 +13605,7 @@ var FileCredentialStore$3 = class {
13287
13605
  }
13288
13606
  async removeLegacy() {
13289
13607
  try {
13290
- await fsPromises.unlink(this.filePath);
13608
+ await fs.unlink(this.filePath);
13291
13609
  } catch (error) {
13292
13610
  if (error.code !== "ENOENT") throw new Error("Kimi Code legacy credential removal failed");
13293
13611
  }
@@ -13306,11 +13624,11 @@ var FileCredentialStore$3 = class {
13306
13624
  }
13307
13625
  let legacy;
13308
13626
  try {
13309
- const stats = await fsPromises.lstat(this.filePath);
13627
+ const stats = await fs.lstat(this.filePath);
13310
13628
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
13311
13629
  if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
13312
- if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
13313
- legacy = await fsPromises.readFile(this.filePath, "utf8");
13630
+ if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
13631
+ legacy = await fs.readFile(this.filePath, "utf8");
13314
13632
  } catch (error) {
13315
13633
  if (error.code === "ENOENT") return null;
13316
13634
  throw new Error("Kimi Code legacy credential read failed");
@@ -13346,7 +13664,7 @@ var FileModelSettingsStore$3 = class {
13346
13664
  }
13347
13665
  async read() {
13348
13666
  try {
13349
- const content = await fsPromises.readFile(this.filePath, "utf8");
13667
+ const content = await fs.readFile(this.filePath, "utf8");
13350
13668
  const parsed = JSON.parse(content);
13351
13669
  if (typeof parsed === "object" && parsed !== null) {
13352
13670
  const record = parsed;
@@ -13359,7 +13677,8 @@ var FileModelSettingsStore$3 = class {
13359
13677
  enabledModelIds,
13360
13678
  catalogModels,
13361
13679
  contextWindowOverrides,
13362
- defaultReasoningEffort
13680
+ defaultReasoningEffort,
13681
+ cacheTtl: isKimiCacheTtl(record.cacheTtl) ? record.cacheTtl : null
13363
13682
  };
13364
13683
  }
13365
13684
  } catch {}
@@ -13368,14 +13687,15 @@ var FileModelSettingsStore$3 = class {
13368
13687
  enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS$3],
13369
13688
  catalogModels: [],
13370
13689
  contextWindowOverrides: {},
13371
- defaultReasoningEffort: null
13690
+ defaultReasoningEffort: null,
13691
+ cacheTtl: null
13372
13692
  };
13373
13693
  }
13374
13694
  async write(settings) {
13375
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
13695
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
13376
13696
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
13377
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
13378
- await fsPromises.rename(tmp, this.filePath);
13697
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
13698
+ await fs.rename(tmp, this.filePath);
13379
13699
  }
13380
13700
  async updateSettings(patch) {
13381
13701
  const current = await this.read();
@@ -13384,7 +13704,8 @@ var FileModelSettingsStore$3 = class {
13384
13704
  ...patch.enabled !== void 0 ? { enabled: patch.enabled } : {},
13385
13705
  ...patch.enabledModelIds !== void 0 ? { enabledModelIds: patch.enabledModelIds } : {},
13386
13706
  ...patch.contextWindowOverrides !== void 0 ? { contextWindowOverrides: mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) } : {},
13387
- ...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 } : {}
13388
13709
  };
13389
13710
  await this.write(next);
13390
13711
  return next;
@@ -13414,7 +13735,7 @@ async function resolveRegion(env = process.env) {
13414
13735
  return pinned.includes(".ai") ? "global" : "mainland-cn";
13415
13736
  }
13416
13737
  try {
13417
- const marker = (await fsPromises.readFile(regionMarkerPath(), "utf8")).trim();
13738
+ const marker = (await fs.readFile(regionMarkerPath(), "utf8")).trim();
13418
13739
  if (isRegion$4(marker)) return marker;
13419
13740
  } catch {}
13420
13741
  return "mainland-cn";
@@ -13423,8 +13744,8 @@ async function resolveRegion(env = process.env) {
13423
13744
  async function persistRegion(region) {
13424
13745
  try {
13425
13746
  const file = regionMarkerPath();
13426
- await fsPromises.mkdir(path.dirname(file), { recursive: true });
13427
- await fsPromises.writeFile(file, region, "utf8");
13747
+ await fs.mkdir(path.dirname(file), { recursive: true });
13748
+ await fs.writeFile(file, region, "utf8");
13428
13749
  } catch {}
13429
13750
  }
13430
13751
  //#endregion
@@ -13526,13 +13847,13 @@ function deviceModel() {
13526
13847
  async function getDeviceId() {
13527
13848
  const file = deviceIdPath();
13528
13849
  try {
13529
- const existing = (await fsPromises.readFile(file, "utf8")).trim();
13850
+ const existing = (await fs.readFile(file, "utf8")).trim();
13530
13851
  if (existing !== "") return existing;
13531
13852
  } catch {}
13532
13853
  const created = randomUUID();
13533
13854
  try {
13534
- await fsPromises.mkdir(path.dirname(file), { recursive: true });
13535
- await fsPromises.writeFile(file, created, {
13855
+ await fs.mkdir(path.dirname(file), { recursive: true });
13856
+ await fs.writeFile(file, created, {
13536
13857
  encoding: "utf8",
13537
13858
  mode: 384
13538
13859
  });
@@ -15348,6 +15669,39 @@ function imageBlockToInline$2(block, images) {
15348
15669
  data: resolved.data
15349
15670
  } : void 0;
15350
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
+ }
15351
15705
  function textOf$3(content) {
15352
15706
  if (typeof content === "string") return sanitizeText$3(content);
15353
15707
  if (!Array.isArray(content)) return "";
@@ -15883,6 +16237,7 @@ function buildOpenAIRequest(options, images = NO_RESOLVED_IMAGES$3, preserveThin
15883
16237
  stream: true,
15884
16238
  stream_options: { include_usage: true },
15885
16239
  max_completion_tokens: maxTokens,
16240
+ ...media.cacheTtl === null || media.cacheTtl === void 0 ? {} : kimiOpenAICacheOptions(media.cacheTtl),
15886
16241
  ...stopSequences(options.stop).length > 0 ? { stop: stopSequences(options.stop) } : {},
15887
16242
  ...options.tools && options.tools.length > 0 ? {
15888
16243
  tools: options.tools.map((tool) => ({
@@ -16016,7 +16371,8 @@ function mergeAnthropicMessages$1(entries) {
16016
16371
  return merged;
16017
16372
  }
16018
16373
  /** Build one `/v1/messages` body. */
16019
- function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, _media = {}) {
16374
+ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, media = {}) {
16375
+ const cacheTtl = media.cacheTtl ?? null;
16020
16376
  const carriers = options.messages.filter((message) => message.role === "system" && messageToolsOf(message) !== void 0);
16021
16377
  const unsentDeclarations = carriers.reduce((total, message) => total + (messageToolsOf(message)?.length ?? 0), 0);
16022
16378
  const carrierText = carriers.map((message) => textOf$3(message.content)).filter((text) => text !== "");
@@ -16047,6 +16403,7 @@ function buildAnthropicRequest(options, images = NO_RESOLVED_IMAGES$3, _media =
16047
16403
  model: options.model,
16048
16404
  max_tokens: maxTokens,
16049
16405
  messages: mergeAnthropicMessages$1(entries),
16406
+ ...cacheTtl === null || cacheTtl === void 0 ? {} : kimiAnthropicCacheControl(cacheTtl),
16050
16407
  ...system === void 0 ? {} : { system },
16051
16408
  ...options.temperature === void 0 || budget !== void 0 ? {} : { temperature: options.temperature },
16052
16409
  ...options.stop && options.stop.length > 0 ? { stop_sequences: options.stop } : {},
@@ -16849,12 +17206,13 @@ var KimiCodeAdapter = class extends LlmAdapter {
16849
17206
  const entry = catalog.find((model) => model.id === options.model);
16850
17207
  const requestOptions = offloadOldestRequestVideos(offloadOldestRequestImages$2(normalizeGenerateOptions(options)));
16851
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();
16852
17210
  const media = {
16853
17211
  videos,
16854
17212
  videoAccepted: inputModalitiesForEntry(options.model, catalog).includes("video"),
16855
- messageTools: dynamicToolsForEntry(options.model, catalog)
17213
+ messageTools: dynamicToolsForEntry(options.model, catalog),
17214
+ cacheTtl: settings.cacheTtl
16856
17215
  };
16857
- const settings = await this.settings();
16858
17216
  const contextWindow = this.contextWindowFor(options.model, entry, settings.contextWindowOverrides);
16859
17217
  const requestedMax = options.maxTokens ?? maxOutputTokensFor$3(options.model, contextWindow);
16860
17218
  const body = assertRequestBodyFits$1(buildRequest({
@@ -17233,6 +17591,7 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
17233
17591
  models,
17234
17592
  contextWindowOverrides: settings.contextWindowOverrides,
17235
17593
  defaultReasoningEffort: settings.defaultReasoningEffort,
17594
+ cacheTtl: settings.cacheTtl,
17236
17595
  loginRegion: region,
17237
17596
  serving: readOption$3(options.serving, true),
17238
17597
  conflict: readOption$3(options.conflict, null)
@@ -17458,6 +17817,10 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
17458
17817
  const effort = body.defaultReasoningEffort;
17459
17818
  if (effort === null || isKimiCodeEffort(effort)) patch.defaultReasoningEffort = effort;
17460
17819
  }
17820
+ if (body.cacheTtl !== void 0) {
17821
+ const ttl = body.cacheTtl;
17822
+ if (ttl === null || isKimiCacheTtl(ttl)) patch.cacheTtl = ttl;
17823
+ }
17461
17824
  if (preferences) await preferences.update(patch);
17462
17825
  else await modelSettings.updateSettings(patch);
17463
17826
  if (patch.enabledModelIds !== void 0 || patch.enabled !== void 0) ctx.emit?.("llm/adapters-updated");
@@ -17667,15 +18030,15 @@ async function saveVideo(input) {
17667
18030
  const digest = createHash("sha256").update(input.data).digest("hex");
17668
18031
  const attachmentId = `sha256:${digest}`;
17669
18032
  const dir = videoStoreDir();
17670
- await fsPromises.mkdir(dir, {
18033
+ await fs.mkdir(dir, {
17671
18034
  recursive: true,
17672
18035
  mode: 448
17673
18036
  });
17674
18037
  const target = path.join(dir, digest + extensionFor(input.declaredType));
17675
- if (!await fsPromises.stat(target).then(() => true).catch(() => false)) {
18038
+ if (!await fs.stat(target).then(() => true).catch(() => false)) {
17676
18039
  const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
17677
- await fsPromises.writeFile(tmp, input.data);
17678
- await fsPromises.rename(tmp, target);
18040
+ await fs.writeFile(tmp, input.data);
18041
+ await fs.rename(tmp, target);
17679
18042
  }
17680
18043
  pruneVideoStore().catch(() => void 0);
17681
18044
  return {
@@ -17705,14 +18068,14 @@ async function pruneVideoStore(budgetBytes = VIDEO_STORE_BUDGET_BYTES) {
17705
18068
  const dir = videoStoreDir();
17706
18069
  let entries;
17707
18070
  try {
17708
- const names = await fsPromises.readdir(dir);
18071
+ const names = await fs.readdir(dir);
17709
18072
  entries = [];
17710
18073
  for (const name of names) {
17711
18074
  if (name.includes(".tmp.")) {
17712
- await fsPromises.rm(path.join(dir, name), { force: true }).catch(() => void 0);
18075
+ await fs.rm(path.join(dir, name), { force: true }).catch(() => void 0);
17713
18076
  continue;
17714
18077
  }
17715
- const stats = await fsPromises.stat(path.join(dir, name)).catch(() => void 0);
18078
+ const stats = await fs.stat(path.join(dir, name)).catch(() => void 0);
17716
18079
  if (stats === void 0 || !stats.isFile()) continue;
17717
18080
  entries.push({
17718
18081
  path: path.join(dir, name),
@@ -17729,7 +18092,7 @@ async function pruneVideoStore(budgetBytes = VIDEO_STORE_BUDGET_BYTES) {
17729
18092
  let removed = 0;
17730
18093
  for (const entry of entries) {
17731
18094
  if (total <= budgetBytes) break;
17732
- await fsPromises.rm(entry.path, { force: true }).catch(() => void 0);
18095
+ await fs.rm(entry.path, { force: true }).catch(() => void 0);
17733
18096
  total -= entry.size;
17734
18097
  removed += 1;
17735
18098
  }
@@ -17753,7 +18116,7 @@ async function readVideoBytes(ref) {
17753
18116
  if (stored === void 0) throw new VideoIngestError("video reference is not one this store issued", "VIDEO_REFERENCE_INVALID");
17754
18117
  let data;
17755
18118
  try {
17756
- data = await fsPromises.readFile(stored);
18119
+ data = await fs.readFile(stored);
17757
18120
  } catch {
17758
18121
  throw new VideoIngestError("stored video is missing; attach it again", "VIDEO_MISSING");
17759
18122
  }
@@ -17814,14 +18177,14 @@ function humanBytes(bytes) {
17814
18177
  /** Read one local file, refusing anything that is not a regular file. */
17815
18178
  async function readLocalFile(input) {
17816
18179
  if (!path.isAbsolute(input)) throw new HarnessError(`video path must be absolute: ${input}`, "KIMI_VIDEO_PATH_NOT_ABSOLUTE");
17817
- const stats = await fsPromises.stat(input).catch(() => void 0);
18180
+ const stats = await fs.stat(input).catch(() => void 0);
17818
18181
  if (stats === void 0) throw new HarnessError(`video file not found: ${input}`, "KIMI_VIDEO_NOT_FOUND");
17819
18182
  if (!stats.isFile()) throw new HarnessError(`video path is not a regular file: ${input}`, "KIMI_VIDEO_NOT_A_FILE");
17820
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");
17821
18184
  const type = mediaTypeForPath(input);
17822
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");
17823
18186
  return {
17824
- data: await fsPromises.readFile(input),
18187
+ data: await fs.readFile(input),
17825
18188
  name: path.basename(input),
17826
18189
  type
17827
18190
  };
@@ -19302,16 +19665,16 @@ function credentialNeedsRefresh(credentials, now = Date.now()) {
19302
19665
  * leaves the destination untouched.
19303
19666
  */
19304
19667
  async function writeFileAtomic(filePath, contents) {
19305
- await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
19668
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
19306
19669
  const temporary = filePath + ".dsh-tmp-" + process.pid + "-" + Date.now();
19307
19670
  try {
19308
- await fsPromises.writeFile(temporary, contents, {
19671
+ await fs.writeFile(temporary, contents, {
19309
19672
  encoding: "utf8",
19310
19673
  mode: 384
19311
19674
  });
19312
- await fsPromises.rename(temporary, filePath);
19675
+ await fs.rename(temporary, filePath);
19313
19676
  } catch (error) {
19314
- await fsPromises.rm(temporary, { force: true }).catch(() => void 0);
19677
+ await fs.rm(temporary, { force: true }).catch(() => void 0);
19315
19678
  throw new Error("MiniMax Code credential write failed; the previous file was left untouched (" + (error instanceof Error ? error.message : String(error)) + ")");
19316
19679
  }
19317
19680
  }
@@ -19521,7 +19884,7 @@ async function readNativeDocument(region) {
19521
19884
  const filePath = authJsonPath(region);
19522
19885
  let raw;
19523
19886
  try {
19524
- raw = await fsPromises.readFile(filePath, "utf8");
19887
+ raw = await fs.readFile(filePath, "utf8");
19525
19888
  } catch {
19526
19889
  return null;
19527
19890
  }
@@ -19558,7 +19921,7 @@ async function syncAuthState(region, options) {
19558
19921
  const filePath = authStateJsonPath(region);
19559
19922
  let parsed;
19560
19923
  try {
19561
- parsed = JSON.parse(await fsPromises.readFile(filePath, "utf8"));
19924
+ parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
19562
19925
  } catch {
19563
19926
  return;
19564
19927
  }
@@ -19606,7 +19969,7 @@ var MinimaxCodeCredentialStore = class {
19606
19969
  async readPluginFile() {
19607
19970
  let raw;
19608
19971
  try {
19609
- raw = await fsPromises.readFile(pluginCredentialPath(), "utf8");
19972
+ raw = await fs.readFile(pluginCredentialPath(), "utf8");
19610
19973
  } catch {
19611
19974
  return null;
19612
19975
  }
@@ -19748,7 +20111,7 @@ var MinimaxCodeCredentialStore = class {
19748
20111
  */
19749
20112
  delete() {
19750
20113
  return serialize(pluginCredentialPath(), async () => {
19751
- await fsPromises.rm(pluginCredentialPath(), { force: true });
20114
+ await fs.rm(pluginCredentialPath(), { force: true });
19752
20115
  recentRead = null;
19753
20116
  });
19754
20117
  }
@@ -19847,17 +20210,17 @@ var MinimaxCodeModelSettingsStore = class {
19847
20210
  }
19848
20211
  async read() {
19849
20212
  try {
19850
- return parseMinimaxCodeModelSettings(JSON.parse(await fsPromises.readFile(this.filePath, "utf8")));
20213
+ return parseMinimaxCodeModelSettings(JSON.parse(await fs.readFile(this.filePath, "utf8")));
19851
20214
  } catch {
19852
20215
  return defaultMinimaxCodeSettings();
19853
20216
  }
19854
20217
  }
19855
20218
  async write(settings) {
19856
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
20219
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
19857
20220
  const temporary = this.filePath + ".tmp." + Date.now();
19858
- await fsPromises.writeFile(temporary, JSON.stringify(settings, null, 2) + "\n", "utf8");
19859
- await fsPromises.rename(temporary, this.filePath);
19860
- if (!isDeepStrictEqual(parseMinimaxCodeModelSettings(JSON.parse(await fsPromises.readFile(this.filePath, "utf8"))), settings)) throw new Error("MiniMax Code model settings write failed; the stored selection does not match what was written");
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");
19861
20224
  }
19862
20225
  /**
19863
20226
  * Read-modify-write under the same per-path lock the credential writes use.
@@ -23572,7 +23935,7 @@ function isCanonicalFile(name) {
23572
23935
  async function scanCredentials(dir = codeBuddyAuthDir()) {
23573
23936
  let names;
23574
23937
  try {
23575
- names = await fsPromises.readdir(dir);
23938
+ names = await fs.readdir(dir);
23576
23939
  } catch (error) {
23577
23940
  if (error.code === "ENOENT") return [];
23578
23941
  throw new Error(`WorkBuddy credential directory could not be read: ${dir}`);
@@ -23582,9 +23945,9 @@ async function scanCredentials(dir = codeBuddyAuthDir()) {
23582
23945
  if (!name.endsWith(".info")) continue;
23583
23946
  const file = path.join(dir, name);
23584
23947
  try {
23585
- const stats = await fsPromises.stat(file);
23948
+ const stats = await fs.stat(file);
23586
23949
  if (!stats.isFile()) continue;
23587
- const raw = await fsPromises.readFile(file, "utf8");
23950
+ const raw = await fs.readFile(file, "utf8");
23588
23951
  found.push(parseCredentialFile(JSON.parse(raw), file, stats.mtimeMs));
23589
23952
  } catch {
23590
23953
  continue;
@@ -23721,7 +24084,7 @@ var FileCredentialStore$4 = class {
23721
24084
  async isStillCurrent(credentials) {
23722
24085
  if (credentials.source === "managed") return true;
23723
24086
  try {
23724
- return (await fsPromises.stat(credentials.sourceFile)).mtimeMs === credentials.sourceMtimeMs;
24087
+ return (await fs.stat(credentials.sourceFile)).mtimeMs === credentials.sourceMtimeMs;
23725
24088
  } catch {
23726
24089
  return false;
23727
24090
  }
@@ -23761,9 +24124,9 @@ var FileCredentialStore$4 = class {
23761
24124
  */
23762
24125
  async readDesktopFile(filePath) {
23763
24126
  try {
23764
- const stats = await fsPromises.stat(filePath);
24127
+ const stats = await fs.stat(filePath);
23765
24128
  if (!stats.isFile()) return null;
23766
- const raw = await fsPromises.readFile(filePath, "utf8");
24129
+ const raw = await fs.readFile(filePath, "utf8");
23767
24130
  return parseCredentialFile(JSON.parse(raw), filePath, stats.mtimeMs);
23768
24131
  } catch {
23769
24132
  return null;
@@ -23822,25 +24185,25 @@ var FileCredentialStore$4 = class {
23822
24185
  */
23823
24186
  async writeBack(credentials) {
23824
24187
  try {
23825
- const raw = await fsPromises.readFile(credentials.sourceFile, "utf8");
24188
+ const raw = await fs.readFile(credentials.sourceFile, "utf8");
23826
24189
  const parsed = JSON.parse(raw);
23827
24190
  if (!isRecord$11(parsed) || !isRecord$11(parsed.auth)) return;
23828
24191
  parsed.auth.accessToken = credentials.accessToken;
23829
24192
  if (credentials.refreshToken !== "") parsed.auth.refreshToken = credentials.refreshToken;
23830
24193
  parsed.auth.expiresAt = credentials.expiresAt;
23831
24194
  parsed.auth.lastRefreshTime = Date.now();
23832
- const sourceMode = process.platform === "win32" ? void 0 : (await fsPromises.stat(credentials.sourceFile)).mode & 511;
24195
+ const sourceMode = process.platform === "win32" ? void 0 : (await fs.stat(credentials.sourceFile)).mode & 511;
23833
24196
  const tmp = `${credentials.sourceFile}.tmp.${process.pid}`;
23834
24197
  try {
23835
- await fsPromises.writeFile(tmp, JSON.stringify(parsed, null, 2), {
24198
+ await fs.writeFile(tmp, JSON.stringify(parsed, null, 2), {
23836
24199
  encoding: "utf8",
23837
24200
  ...sourceMode === void 0 ? {} : { mode: sourceMode }
23838
24201
  });
23839
- await fsPromises.rename(tmp, credentials.sourceFile);
24202
+ await fs.rename(tmp, credentials.sourceFile);
23840
24203
  } finally {
23841
- await fsPromises.unlink(tmp).catch(() => void 0);
24204
+ await fs.unlink(tmp).catch(() => void 0);
23842
24205
  }
23843
- credentials.sourceMtimeMs = (await fsPromises.stat(credentials.sourceFile)).mtimeMs;
24206
+ credentials.sourceMtimeMs = (await fs.stat(credentials.sourceFile)).mtimeMs;
23844
24207
  } catch {}
23845
24208
  }
23846
24209
  };
@@ -23855,7 +24218,7 @@ var FileModelSettingsStore$4 = class {
23855
24218
  }
23856
24219
  async read() {
23857
24220
  try {
23858
- const content = await fsPromises.readFile(this.filePath, "utf8");
24221
+ const content = await fs.readFile(this.filePath, "utf8");
23859
24222
  const parsed = JSON.parse(content);
23860
24223
  if (isRecord$11(parsed)) {
23861
24224
  const enabledModelIds = Array.isArray(parsed.enabledModelIds) ? parsed.enabledModelIds.filter((id) => typeof id === "string") : [...DEFAULT_ENABLED_MODEL_IDS$1];
@@ -23883,10 +24246,10 @@ var FileModelSettingsStore$4 = class {
23883
24246
  };
23884
24247
  }
23885
24248
  async write(settings) {
23886
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
24249
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
23887
24250
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
23888
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
23889
- await fsPromises.rename(tmp, this.filePath);
24251
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
24252
+ await fs.rename(tmp, this.filePath);
23890
24253
  }
23891
24254
  async updateSettings(patch) {
23892
24255
  const current = await this.read();
@@ -26510,6 +26873,25 @@ const CLAUDE_CODE_BETA = "claude-code-20250219";
26510
26873
  /** Lets thinking blocks interleave with tool use across a multi-turn loop. */
26511
26874
  const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
26512
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
+ /**
26513
26895
  * Licenses `thinking.block_binding` in a request body.
26514
26896
  *
26515
26897
  * The field is not optional to license: the vendor documents that a body
@@ -27478,6 +27860,7 @@ function defaultClaudeSettings() {
27478
27860
  enabledModelIds: [...DEFAULT_ENABLED_MODEL_IDS],
27479
27861
  contextWindowOverrides: {},
27480
27862
  defaultReasoningEffort: null,
27863
+ cacheTtl: null,
27481
27864
  selectedAccountId: null
27482
27865
  };
27483
27866
  }
@@ -27713,12 +28096,14 @@ function parseClaudeModelSettings(value) {
27713
28096
  for (const [model, window] of Object.entries(record.contextWindowOverrides)) if (typeof window === "number" && Number.isFinite(window) && window > 0) contextWindowOverrides[model] = window;
27714
28097
  }
27715
28098
  const defaultReasoningEffort = nonEmptyString$3(record.defaultReasoningEffort) ?? null;
28099
+ const cacheTtl = isClaudeCacheTtl$1(record.cacheTtl) ? record.cacheTtl : null;
27716
28100
  const selectedAccountId = nonEmptyString$3(record.selectedAccountId) ?? null;
27717
28101
  return {
27718
28102
  enabled: record.enabled !== false,
27719
28103
  enabledModelIds,
27720
28104
  contextWindowOverrides,
27721
28105
  defaultReasoningEffort,
28106
+ cacheTtl,
27722
28107
  selectedAccountId
27723
28108
  };
27724
28109
  }
@@ -27729,6 +28114,7 @@ function mergeClaudeSettings(current, patch) {
27729
28114
  enabledModelIds: patch.enabledModelIds !== void 0 ? [...patch.enabledModelIds] : current.enabledModelIds,
27730
28115
  contextWindowOverrides: patch.contextWindowOverrides !== void 0 ? mergeContextWindowOverrides(current.contextWindowOverrides, patch.contextWindowOverrides) : current.contextWindowOverrides,
27731
28116
  defaultReasoningEffort: patch.defaultReasoningEffort !== void 0 ? patch.defaultReasoningEffort : current.defaultReasoningEffort,
28117
+ cacheTtl: patch.cacheTtl !== void 0 ? patch.cacheTtl : current.cacheTtl,
27732
28118
  selectedAccountId: patch.selectedAccountId !== void 0 ? patch.selectedAccountId : current.selectedAccountId
27733
28119
  };
27734
28120
  }
@@ -27801,7 +28187,7 @@ var FileCredentialStore = class {
27801
28187
  }
27802
28188
  async removeLegacy() {
27803
28189
  try {
27804
- await fsPromises.unlink(this.filePath);
28190
+ await fs.unlink(this.filePath);
27805
28191
  } catch (error) {
27806
28192
  if (error.code !== "ENOENT") throw new Error("Claude legacy credential removal failed");
27807
28193
  }
@@ -27822,11 +28208,11 @@ var FileCredentialStore = class {
27822
28208
  }
27823
28209
  let legacy;
27824
28210
  try {
27825
- const stats = await fsPromises.lstat(this.filePath);
28211
+ const stats = await fs.lstat(this.filePath);
27826
28212
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("Invalid credential file");
27827
28213
  if (process.getuid && stats.uid !== process.getuid()) throw new Error("Invalid credential owner");
27828
- if (process.platform !== "win32") await fsPromises.chmod(this.filePath, 384);
27829
- legacy = await fsPromises.readFile(this.filePath, "utf8");
28214
+ if (process.platform !== "win32") await fs.chmod(this.filePath, 384);
28215
+ legacy = await fs.readFile(this.filePath, "utf8");
27830
28216
  } catch (error) {
27831
28217
  if (error.code === "ENOENT") return null;
27832
28218
  throw new Error("Claude legacy credential read failed");
@@ -27934,17 +28320,17 @@ var FileModelSettingsStore = class {
27934
28320
  }
27935
28321
  async read() {
27936
28322
  try {
27937
- const content = await fsPromises.readFile(this.filePath, "utf8");
28323
+ const content = await fs.readFile(this.filePath, "utf8");
27938
28324
  return parseClaudeModelSettings(JSON.parse(content));
27939
28325
  } catch {
27940
28326
  return defaultClaudeSettings();
27941
28327
  }
27942
28328
  }
27943
28329
  async write(settings) {
27944
- await fsPromises.mkdir(path.dirname(this.filePath), { recursive: true });
28330
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
27945
28331
  const tmp = this.filePath + ".tmp." + Date.now();
27946
- await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
27947
- await fsPromises.rename(tmp, this.filePath);
28332
+ await fs.writeFile(tmp, JSON.stringify(settings, null, 2), "utf8");
28333
+ await fs.rename(tmp, this.filePath);
27948
28334
  }
27949
28335
  /**
27950
28336
  * Read-modify-write under the same per-path lock the credentials use.
@@ -27986,6 +28372,11 @@ function registerClaudePreferenceStore(settings, fallbackStore = new FileModelSe
27986
28372
  enabledModelIds: z.array(z.string()).default([...DEFAULT_ENABLED_MODEL_IDS]),
27987
28373
  contextWindowOverrides: z.dict(z.number()).default({}),
27988
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),
27989
28380
  selectedAccountId: z.union([z.string(), z.const(null)]).default(null)
27990
28381
  }));
27991
28382
  return {
@@ -27996,6 +28387,7 @@ function registerClaudePreferenceStore(settings, fallbackStore = new FileModelSe
27996
28387
  enabledModelIds: value.enabledModelIds,
27997
28388
  contextWindowOverrides: value.contextWindowOverrides,
27998
28389
  defaultReasoningEffort: value.defaultReasoningEffort,
28390
+ cacheTtl: isClaudeCacheTtl$1(value.cacheTtl) ? value.cacheTtl : null,
27999
28391
  selectedAccountId: value.selectedAccountId ?? null
28000
28392
  };
28001
28393
  },
@@ -28191,6 +28583,7 @@ function claudeBetas(options = {}) {
28191
28583
  betas.push(OAUTH_BETA);
28192
28584
  if (options.thinking === true) betas.push(INTERLEAVED_THINKING_BETA);
28193
28585
  if (options.thinkingBinding === true) betas.push(THINKING_BINDING_CONTROLS_BETA);
28586
+ if (options.cacheTtl === "1h") betas.push(EXTENDED_CACHE_TTL_BETA);
28194
28587
  return betas;
28195
28588
  }
28196
28589
  /** The `User-Agent` value this line reports for one surface. */
@@ -29788,7 +30181,7 @@ function leadingSystemText(options) {
29788
30181
  *
29789
30182
  * @param cacheControl - false is the opt-OUT; the default requests caching.
29790
30183
  */
29791
- function buildClaudeSystemBlocks(options, cacheControl = true) {
30184
+ function buildClaudeSystemBlocks(options, cacheControl = true, ttl = "5m") {
29792
30185
  const blocks = [{
29793
30186
  type: "text",
29794
30187
  text: CLAUDE_CODE_IDENTITY_TEXT
@@ -29800,7 +30193,7 @@ function buildClaudeSystemBlocks(options, cacheControl = true) {
29800
30193
  });
29801
30194
  if (cacheControl) {
29802
30195
  const last = blocks[blocks.length - 1];
29803
- last.cache_control = { type: "ephemeral" };
30196
+ last.cache_control = cacheControlFor(ttl);
29804
30197
  }
29805
30198
  return blocks;
29806
30199
  }
@@ -29838,13 +30231,14 @@ function lastCacheableUserBlock(messages) {
29838
30231
  * and has not been handed to a caller yet.
29839
30232
  * @returns the number of breakpoints written.
29840
30233
  */
29841
- function markClaudeCacheBreakpoints(body) {
30234
+ function markClaudeCacheBreakpoints(body, ttl = "5m") {
29842
30235
  let marked = 0;
30236
+ const marker = cacheControlFor(ttl);
29843
30237
  const system = body.system;
29844
30238
  if (Array.isArray(system)) {
29845
30239
  const last = system[system.length - 1];
29846
30240
  if (last !== void 0) {
29847
- last.cache_control = { type: "ephemeral" };
30241
+ last.cache_control = { ...marker };
29848
30242
  marked += 1;
29849
30243
  }
29850
30244
  }
@@ -29852,7 +30246,7 @@ function markClaudeCacheBreakpoints(body) {
29852
30246
  if (Array.isArray(messages)) {
29853
30247
  const target = lastCacheableUserBlock(messages);
29854
30248
  if (target !== void 0) {
29855
- target.cache_control = { type: "ephemeral" };
30249
+ target.cache_control = { ...marker };
29856
30250
  marked += 1;
29857
30251
  }
29858
30252
  }
@@ -29860,7 +30254,7 @@ function markClaudeCacheBreakpoints(body) {
29860
30254
  if (Array.isArray(tools) && tools.length > 0) {
29861
30255
  const last = tools[tools.length - 1];
29862
30256
  if (last !== void 0) {
29863
- last.cache_control = { type: "ephemeral" };
30257
+ last.cache_control = { ...marker };
29864
30258
  marked += 1;
29865
30259
  }
29866
30260
  }
@@ -30094,11 +30488,12 @@ function buildClaudeRequestBody(options, images = NO_RESOLVED_IMAGES, request =
30094
30488
  const thinkingEnabled = thinking !== void 0 && thinking.type !== "disabled";
30095
30489
  const temperatureAllowed = claudeModelSupportsTemperature(options.model) && !thinkingEnabled;
30096
30490
  const caching = request.cacheControl !== false;
30491
+ const cacheTtl = request.cacheTtl ?? "5m";
30097
30492
  const body = {
30098
30493
  model: options.model,
30099
30494
  max_tokens: maxTokens,
30100
30495
  stream: true,
30101
- system: buildClaudeSystemBlocks(options, caching),
30496
+ system: buildClaudeSystemBlocks(options, caching, cacheTtl),
30102
30497
  messages: mergeClaudeMessages(entries),
30103
30498
  ...options.tools !== void 0 && options.tools.length > 0 ? { tools: options.tools.map((tool) => wireTool(tool, names)) } : {},
30104
30499
  ...thinking === void 0 ? {} : { thinking },
@@ -30106,7 +30501,7 @@ function buildClaudeRequestBody(options, images = NO_RESOLVED_IMAGES, request =
30106
30501
  ...temperatureAllowed && options.temperature !== void 0 ? { temperature: options.temperature } : {},
30107
30502
  ...options.stop !== void 0 && options.stop.length > 0 ? { stop_sequences: [...options.stop] } : {}
30108
30503
  };
30109
- if (caching) markClaudeCacheBreakpoints(body);
30504
+ if (caching) markClaudeCacheBreakpoints(body, cacheTtl);
30110
30505
  return body;
30111
30506
  }
30112
30507
  /**
@@ -31888,7 +32283,7 @@ var ClaudeAccountTokenStore = class {
31888
32283
  await this.store.saveAccount(credentials, { internalId: this.internalId });
31889
32284
  }
31890
32285
  };
31891
- var ClaudeAdapter = class extends LlmAdapter {
32286
+ var ClaudeAdapter = class ClaudeAdapter extends LlmAdapter {
31892
32287
  store;
31893
32288
  modelSettings;
31894
32289
  preferences;
@@ -32070,10 +32465,25 @@ var ClaudeAdapter = class extends LlmAdapter {
32070
32465
  * driven by {@link shouldRotateAccount}, which bypasses the pool ENTIRELY when
32071
32466
  * no credentials were resolved for it.
32072
32467
  */
32073
- async attemptRequest(credentials, requestOptions, images, toolNames, thinking, signal, fetchFn) {
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);
32074
32483
  const payload = buildClaudeRequestBody(requestOptions, images, {
32075
32484
  toolNames,
32076
- cacheControl: true
32485
+ cacheControl: true,
32486
+ cacheTtl
32077
32487
  });
32078
32488
  try {
32079
32489
  return await fetchFn("https://api.anthropic.com/v1/messages?beta=true", {
@@ -32082,6 +32492,7 @@ var ClaudeAdapter = class extends LlmAdapter {
32082
32492
  model: requestOptions.model,
32083
32493
  thinking,
32084
32494
  thinkingBinding: claudeBodyBindsThinking(payload),
32495
+ cacheTtl,
32085
32496
  method: "POST"
32086
32497
  }),
32087
32498
  body: JSON.stringify(payload),
@@ -32100,6 +32511,7 @@ var ClaudeAdapter = class extends LlmAdapter {
32100
32511
  assertClaudeCliVersionMeetsFloor(requestOptions.model, await this.catalogForRequest());
32101
32512
  const effort = requestOptions.reasoningEffort === void 0 || requestOptions.reasoningEffort === null ? void 0 : String(requestOptions.reasoningEffort);
32102
32513
  const thinking = claudeRequestThinks(requestOptions.model, effort);
32514
+ const settings = await this.settings().catch(() => null);
32103
32515
  const pool = this.accountPool;
32104
32516
  const tried = /* @__PURE__ */ new Set();
32105
32517
  let response;
@@ -32119,7 +32531,7 @@ var ClaudeAdapter = class extends LlmAdapter {
32119
32531
  let outputStarted = false;
32120
32532
  while (true) {
32121
32533
  const { credentials, accountId } = await this.resolveCredential(pool, tried, fetchFn, signal);
32122
- response = await this.attemptRequest(credentials, requestOptions, images, toolNames, thinking, signal, fetchFn);
32534
+ response = await this.attemptRequest(credentials, requestOptions, images, toolNames, thinking, settings, signal, fetchFn);
32123
32535
  if (response.ok) break;
32124
32536
  const detail = (await response.text().catch(() => "")).slice(0, 2e3);
32125
32537
  const failure = classifyFailure(response.status, detail, response.headers);
@@ -33589,6 +34001,16 @@ const CLAUDE_REASONING_EFFORTS = [
33589
34001
  function isClaudeReasoningEffort(value) {
33590
34002
  return typeof value === "string" && CLAUDE_REASONING_EFFORTS.includes(value);
33591
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
+ }
33592
34014
  //#endregion
33593
34015
  //#region src/host/claude/routes.ts
33594
34016
  /** Ceiling on one request body. Identical to every sibling line's. */
@@ -33780,6 +34202,7 @@ async function getClaudeWebStatus(store, modelSettings, preferences, options = {
33780
34202
  models,
33781
34203
  contextWindowOverrides: settings.contextWindowOverrides,
33782
34204
  defaultReasoningEffort: settings.defaultReasoningEffort,
34205
+ cacheTtl: settings.cacheTtl,
33783
34206
  selectedAccountId: settings.selectedAccountId,
33784
34207
  accounts: poolAccounts.map((summary) => summary),
33785
34208
  rotationStrategy: poolData?.rotationStrategy ?? "sequential",
@@ -34058,6 +34481,10 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
34058
34481
  const effort = body.defaultReasoningEffort;
34059
34482
  if (effort === null || isClaudeReasoningEffort(effort)) patch.defaultReasoningEffort = effort;
34060
34483
  }
34484
+ if (body.cacheTtl !== void 0) {
34485
+ const ttl = body.cacheTtl;
34486
+ if (ttl === null || isClaudeCacheTtl(ttl)) patch.cacheTtl = ttl;
34487
+ }
34061
34488
  if (body.selectedAccountId !== void 0) {
34062
34489
  const selected = body.selectedAccountId;
34063
34490
  if (selected !== null && typeof selected !== "string") return sendJson(response, 400, {
@@ -34593,7 +35020,7 @@ var WorkBuddyCheckinService = class {
34593
35020
  if (this.loaded) return;
34594
35021
  this.loaded = true;
34595
35022
  try {
34596
- const parsed = JSON.parse(await fsPromises.readFile(this.statePath, "utf8"));
35023
+ const parsed = JSON.parse(await fs.readFile(this.statePath, "utf8"));
34597
35024
  if (isRecord$2(parsed)) {
34598
35025
  const lastRunAt = asCount$1(parsed.lastRunAt);
34599
35026
  if (lastRunAt !== void 0) this.lastRunAt = lastRunAt;
@@ -34606,15 +35033,15 @@ var WorkBuddyCheckinService = class {
34606
35033
  }
34607
35034
  async save() {
34608
35035
  try {
34609
- await fsPromises.mkdir(path.dirname(this.statePath), { recursive: true });
35036
+ await fs.mkdir(path.dirname(this.statePath), { recursive: true });
34610
35037
  const tmp = `${this.statePath}.tmp.${process.pid}`;
34611
35038
  const document = {
34612
35039
  version: 2,
34613
35040
  lastRunAt: this.lastRunAt,
34614
35041
  accounts: this.state
34615
35042
  };
34616
- await fsPromises.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
34617
- await fsPromises.rename(tmp, this.statePath);
35043
+ await fs.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
35044
+ await fs.rename(tmp, this.statePath);
34618
35045
  } catch {}
34619
35046
  }
34620
35047
  async post(url, headers) {
@@ -35262,7 +35689,7 @@ var MinimaxCodeCheckinService = class {
35262
35689
  if (this.loaded) return;
35263
35690
  this.loaded = true;
35264
35691
  try {
35265
- const parsed = JSON.parse(await fsPromises.readFile(this.statePath, "utf8"));
35692
+ const parsed = JSON.parse(await fs.readFile(this.statePath, "utf8"));
35266
35693
  if (isRecord(parsed)) {
35267
35694
  const lastRunAt = asCount(parsed.lastRunAt);
35268
35695
  if (lastRunAt !== void 0) this.lastRunAt = lastRunAt;
@@ -35281,7 +35708,7 @@ var MinimaxCodeCheckinService = class {
35281
35708
  }
35282
35709
  async save() {
35283
35710
  try {
35284
- await fsPromises.mkdir(path.dirname(this.statePath), { recursive: true });
35711
+ await fs.mkdir(path.dirname(this.statePath), { recursive: true });
35285
35712
  const tmp = `${this.statePath}.tmp.${process.pid}`;
35286
35713
  const document = {
35287
35714
  version: 1,
@@ -35289,8 +35716,8 @@ var MinimaxCodeCheckinService = class {
35289
35716
  accounts: this.state,
35290
35717
  identities: this.identities
35291
35718
  };
35292
- await fsPromises.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
35293
- await fsPromises.rename(tmp, this.statePath);
35719
+ await fs.writeFile(tmp, JSON.stringify(document, null, 2), "utf8");
35720
+ await fs.rename(tmp, this.statePath);
35294
35721
  } catch {}
35295
35722
  }
35296
35723
  };
@@ -37682,7 +38109,10 @@ function apply(ctx, pluginConfig = {}) {
37682
38109
  outputVerbosity: () => preferences.status().outputVerbosity,
37683
38110
  fastMode: () => preferences.status().fastMode,
37684
38111
  reasoningSummary: () => preferences.status().reasoningSummary
37685
- }), preferences);
38112
+ }), preferences, (options) => oauth.credentials(void 0, { purpose: "tool" }).then((credentials) => loadCodexCatalog(credentials, {
38113
+ fetchFn: proxyFetch,
38114
+ signal: options?.signal
38115
+ })).catch(() => []));
37686
38116
  const searchSwitcher = new SearchProviderSwitcher(ctx.loader);
37687
38117
  const applyWebProviders = (current = preferences.status()) => {
37688
38118
  const pluginFetch = proxyManager.resolveActiveProxyUrl() !== null;