@eddyskywalker/dsh-chatgpt-subscription 0.10.9 → 0.10.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -109,6 +109,16 @@ const CODEX_MODEL_CATALOG = [
109
109
  supportsReasoningSummary: true,
110
110
  maxTokens: GPT_6_MAX_TOKENS
111
111
  },
112
+ {
113
+ id: "gpt-6.1-sol",
114
+ name: "6.1 Sol",
115
+ contextWindow: GPT_6_DEFAULT_CONTEXT_WINDOW,
116
+ inputModalities: ["text", "image"],
117
+ defaultReasoningEffort: "medium",
118
+ reasoningProfile: "gpt-6",
119
+ supportsReasoningSummary: true,
120
+ maxTokens: GPT_6_MAX_TOKENS
121
+ },
112
122
  {
113
123
  id: "gpt-6-sol",
114
124
  name: "6 Sol",
@@ -189,6 +199,7 @@ const CODEX_MODEL_CATALOG = [
189
199
  ];
190
200
  const DEFAULT_VISIBLE_CODEX_MODEL_IDS = [
191
201
  "gpt-6-astra",
202
+ "gpt-6.1-sol",
192
203
  "gpt-6-sol",
193
204
  "gpt-6-luna",
194
205
  "gpt-5.6-sol",
@@ -6421,6 +6432,30 @@ function parseCatalogModels(data) {
6421
6432
  }
6422
6433
  return list;
6423
6434
  }
6435
+ /**
6436
+ * Merge a freshly discovered API catalog into the stored model selection.
6437
+ *
6438
+ * The API catalog is a *request-level* listing: its ids are runtime names
6439
+ * (`gemini-3.8-flash-tiered`, `claude-opus-4-6-thinking`, `gemini-3.1-pro-high`)
6440
+ * that share no vocabulary with the selectable plugin ids (`gemini-3.8-flash`,
6441
+ * `claude-opus-4-6`, `gemini-3.1-pro`). The picker and the adapter both iterate
6442
+ * the plugin table, so pruning the selection against the API ids kept nothing:
6443
+ * the first refresh after the shipped defaults were written dropped every
6444
+ * stored id, which left the card rendering every model unchecked and made a
6445
+ * deliberate pick look like it did not persist. Prune against the table the
6446
+ * selection actually indexes, and keep the API catalog as display metadata
6447
+ * only.
6448
+ *
6449
+ * @param stored - The selection currently persisted for the picker.
6450
+ * @param storedCatalog - The API catalog the previous refresh recorded.
6451
+ * @returns The selection to persist, drawn from the plugin model table.
6452
+ */
6453
+ function mergeCatalogSelection(stored, storedCatalog) {
6454
+ const shipped = MODELS.map((model) => model.id);
6455
+ const known = new Set(shipped);
6456
+ if (stored.length === 0 && storedCatalog.length === 0) return shipped;
6457
+ return stored.filter((id) => known.has(id));
6458
+ }
6424
6459
  async function fetchAccountQuota(store = new FileCredentialStore$2(), modelSettings, fetchFn = fetch, force = false) {
6425
6460
  if (!force && cachedQuota$1 && Date.now() - (cachedQuota$1.fetchedAt || 0) < 12e4) return cachedQuota$1;
6426
6461
  if (quotaFetchInFlight) return quotaFetchInFlight;
@@ -6461,9 +6496,7 @@ async function fetchAccountQuota(store = new FileCredentialStore$2(), modelSetti
6461
6496
  cachedQuota$1 = snapshot;
6462
6497
  if (modelSettings && catalogModels.length > 0) {
6463
6498
  const current = await modelSettings.read();
6464
- const isFirstTime = current.catalogModels.length === 0 && current.enabledModelIds.length === 0;
6465
- const catalogIds = new Set(catalogModels.map((m) => m.id));
6466
- const mergedEnabled = isFirstTime ? catalogModels.map((m) => m.id) : current.enabledModelIds.filter((id) => catalogIds.has(id));
6499
+ const mergedEnabled = mergeCatalogSelection(current.enabledModelIds, current.catalogModels);
6467
6500
  await modelSettings.setCatalogModels(catalogModels, { enabledModelIds: mergedEnabled });
6468
6501
  }
6469
6502
  return snapshot;
@@ -9606,18 +9639,36 @@ async function readCatalogSnapshot(name, parseModels) {
9606
9639
  return;
9607
9640
  }
9608
9641
  }
9642
+ /**
9643
+ * Snapshot writes still in flight.
9644
+ *
9645
+ * Callers fire-and-forget these writes, so nothing in production ever awaits
9646
+ * one. A test that deletes its private `storages` directory between cases
9647
+ * cannot know when that is safe, and a write that lands after the delete
9648
+ * recreates the file the next case asserts is absent — which is how a stale
9649
+ * listing leaked into a later test and failed it on CI. Registering each write
9650
+ * gives {@link flushCatalogSnapshots} a deterministic point to wait on.
9651
+ */
9652
+ const pendingSnapshotWrites = /* @__PURE__ */ new Set();
9609
9653
  /** Persist one snapshot; a failure is logged nowhere and never thrown. */
9610
- async function writeCatalogSnapshot(name, models, fetchedAt) {
9611
- try {
9612
- const filePath = catalogSnapshotPath(name);
9613
- await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
9614
- const tmp = `${filePath}.tmp.${Date.now()}`;
9615
- await fsPromises.writeFile(tmp, JSON.stringify({
9616
- fetchedAt,
9617
- models
9618
- }, null, 2), "utf8");
9619
- await fsPromises.rename(tmp, filePath);
9620
- } catch {}
9654
+ function writeCatalogSnapshot(name, models, fetchedAt) {
9655
+ const write = (async () => {
9656
+ try {
9657
+ const filePath = catalogSnapshotPath(name);
9658
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
9659
+ const tmp = `${filePath}.tmp.${Date.now()}`;
9660
+ await fsPromises.writeFile(tmp, JSON.stringify({
9661
+ fetchedAt,
9662
+ models
9663
+ }, null, 2), "utf8");
9664
+ await fsPromises.rename(tmp, filePath);
9665
+ } catch {}
9666
+ })();
9667
+ pendingSnapshotWrites.add(write);
9668
+ write.finally(() => {
9669
+ pendingSnapshotWrites.delete(write);
9670
+ });
9671
+ return write;
9621
9672
  }
9622
9673
  /**
9623
9674
  * Warm an in-memory cache from the persisted snapshot when it is fresher than
@@ -10191,6 +10242,8 @@ function extractUnlimited(payload) {
10191
10242
  return deepValue(payload, ["unlimited", "isUnlimited"]) === true;
10192
10243
  }
10193
10244
  let cachedCatalog$1;
10245
+ /** Bumped by every cache clear so a fetch already in flight cannot publish. */
10246
+ let catalogCacheEpoch$2 = 0;
10194
10247
  let catalogInFlight$2 = null;
10195
10248
  /**
10196
10249
  * Snapshot scopes already consulted in this process.
@@ -10282,7 +10335,9 @@ async function loadProviderModels(options = {}) {
10282
10335
  /** One in-flight, single-flighted fetch of the live catalog. */
10283
10336
  function refreshProviderModels$1(options = {}) {
10284
10337
  if (catalogInFlight$2) return catalogInFlight$2;
10338
+ const epoch = catalogCacheEpoch$2;
10285
10339
  const request = fetchProviderModels(options).then((models) => {
10340
+ if (epoch !== catalogCacheEpoch$2) return models.length > 0 ? models : cachedCatalog$1?.models ?? [];
10286
10341
  if (models.length > 0) {
10287
10342
  const fetchedAt = Date.now();
10288
10343
  cachedCatalog$1 = {
@@ -10299,6 +10354,7 @@ function refreshProviderModels$1(options = {}) {
10299
10354
  });
10300
10355
  }
10301
10356
  function clearCachedCatalog$3() {
10357
+ catalogCacheEpoch$2 += 1;
10302
10358
  cachedCatalog$1 = void 0;
10303
10359
  catalogInFlight$2 = null;
10304
10360
  catalogSnapshotScopesLoaded$2.clear();
@@ -13926,6 +13982,16 @@ async function modelRequestHeaders$1(accessToken, wire) {
13926
13982
  }
13927
13983
  let catalogCache$1 = null;
13928
13984
  /**
13985
+ * Bumped by every cache clear so a fetch already in flight cannot publish.
13986
+ *
13987
+ * A stale-while-revalidate refresh answers its caller from cache and keeps
13988
+ * fetching. Clearing the cache — a sign-out, a forced refresh, a test starting
13989
+ * over — must therefore also silence that refresh: without this it landed
13990
+ * afterwards and republished both the in-memory cache and the persisted
13991
+ * snapshot it was written to invalidate.
13992
+ */
13993
+ let catalogCacheEpoch$1 = 0;
13994
+ /**
13929
13995
  * Snapshot scopes already consulted in this process.
13930
13996
  *
13931
13997
  * One entry per region rather than a single flag: the persisted file is scoped
@@ -13935,6 +14001,7 @@ let catalogCache$1 = null;
13935
14001
  */
13936
14002
  const catalogSnapshotScopesLoaded$1 = /* @__PURE__ */ new Set();
13937
14003
  function clearCachedCatalog$2() {
14004
+ catalogCacheEpoch$1 += 1;
13938
14005
  catalogCache$1 = null;
13939
14006
  catalogSnapshotScopesLoaded$1.clear();
13940
14007
  catalogActiveRegions.clear();
@@ -14043,6 +14110,7 @@ function refreshProviderModels(options) {
14043
14110
  }
14044
14111
  async function performListing(options) {
14045
14112
  const region = options.region ?? await resolveRegion();
14113
+ const epoch = catalogCacheEpoch$1;
14046
14114
  let accessToken = options.accessToken;
14047
14115
  if (accessToken === void 0) {
14048
14116
  if (options.store === void 0) return [];
@@ -14067,6 +14135,7 @@ async function performListing(options) {
14067
14135
  const data = Array.isArray(payload) ? payload : asRecord$7(payload)?.data;
14068
14136
  if (!Array.isArray(data)) throw new Error("Kimi Code model listing was not in the documented shape.");
14069
14137
  const models = data.map(parseCatalogModel).filter((model) => model !== void 0);
14138
+ if (epoch !== catalogCacheEpoch$1) return models;
14070
14139
  rememberCatalogCredential(region, accessToken);
14071
14140
  catalogCache$1 = {
14072
14141
  fetchedAt: Date.now(),
@@ -23894,6 +23963,8 @@ function parseConfigModels(payload, region) {
23894
23963
  return models;
23895
23964
  }
23896
23965
  let cachedCatalog;
23966
+ /** Bumped by every cache clear so a fetch already in flight cannot publish. */
23967
+ let catalogCacheEpoch = 0;
23897
23968
  let catalogInFlight$1 = null;
23898
23969
  let catalogInFlightRegion = null;
23899
23970
  const CATALOG_CACHE_TTL_MS$1 = 1800 * 1e3;
@@ -23908,6 +23979,7 @@ const CATALOG_CACHE_TTL_MS$1 = 1800 * 1e3;
23908
23979
  const catalogSnapshotScopesLoaded = /* @__PURE__ */ new Set();
23909
23980
  function clearCachedCatalog$1() {
23910
23981
  cachedCatalog = void 0;
23982
+ catalogCacheEpoch += 1;
23911
23983
  catalogInFlight$1 = null;
23912
23984
  catalogInFlightRegion = null;
23913
23985
  catalogSnapshotScopesLoaded.clear();
@@ -23972,8 +24044,10 @@ async function loadConfigCatalog(credentials, options = {}) {
23972
24044
  /** One in-flight, single-flighted fetch of the live catalog. */
23973
24045
  function refreshConfigCatalog(credentials, options = {}) {
23974
24046
  if (catalogInFlight$1 && catalogInFlightRegion === credentials.region) return catalogInFlight$1;
24047
+ const epoch = catalogCacheEpoch;
23975
24048
  const sameRegionCache = () => cachedCatalog?.region === credentials.region ? cachedCatalog.models : [];
23976
24049
  const request = fetchConfigCatalog(credentials, options).then((models) => {
24050
+ if (epoch !== catalogCacheEpoch) return models.length > 0 ? models : sameRegionCache();
23977
24051
  if (models.length > 0) {
23978
24052
  const fetchedAt = Date.now();
23979
24053
  cachedCatalog = {
@@ -26277,6 +26351,17 @@ const OAUTH_BETA = "oauth-2025-04-20";
26277
26351
  const CLAUDE_CODE_BETA = "claude-code-20250219";
26278
26352
  /** Lets thinking blocks interleave with tool use across a multi-turn loop. */
26279
26353
  const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
26354
+ /**
26355
+ * Licenses `thinking.block_binding` in a request body.
26356
+ *
26357
+ * The field is not optional to license: the vendor documents that a body
26358
+ * carrying `block_binding` WITHOUT this beta is a 400 whose message ends
26359
+ * `block_binding: Extra inputs are not permitted` — every request, every turn.
26360
+ * The mid-convo thinking form always sends `block_binding`, so a request that
26361
+ * uses that form must carry this value. The reference sends it for exactly those
26362
+ * models (`THINKING_BINDING_CONTROLS_BETA`, pushed when `supportsMidConvoEffort`).
26363
+ */
26364
+ const THINKING_BINDING_CONTROLS_BETA = "thinking-binding-controls-2026-08-01";
26280
26365
  /** Anthropic protocol revision sent as `anthropic-version`. */
26281
26366
  const ANTHROPIC_VERSION = "2023-06-01";
26282
26367
  /**
@@ -26306,12 +26391,18 @@ const ANTHROPIC_VERSION = "2023-06-01";
26306
26391
  * trades a clear refusal for an unpredictable one. Note that npm's `stable`
26307
26392
  * dist-tag can itself sit below a model's floor, so `latest` is the tag to read.
26308
26393
  *
26394
+ * It must also be at or above the release that FIRST SHIPPED each model in the
26395
+ * table, whether or not a floor for that model has been observed: upstream has
26396
+ * so far gated a new model on the release that added it (Opus 5.5 arrived in
26397
+ * 2.1.280 and is refused below 2.1.280). Claude Sonnet 5.5 arrived in 2.1.284,
26398
+ * so 2.1.283 could not be relied on to reach it; 2.1.285 is npm `latest`.
26399
+ *
26309
26400
  * It is still not a literal that only moves when this package is republished: a
26310
26401
  * user whose installed baseline has aged past the server's floor has to be able
26311
26402
  * to raise it without waiting for a release. Read the effective value with
26312
26403
  * {@link claudeCliVersion}, pin one with {@link setClaudeCliVersion}.
26313
26404
  */
26314
- const CLAUDE_CLI_VERSION = "2.1.283";
26405
+ const CLAUDE_CLI_VERSION = "2.1.285";
26315
26406
  /**
26316
26407
  * Loopback callback port the subscription flow defaults to.
26317
26408
  *
@@ -26669,8 +26760,10 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
26669
26760
  * LOCAL ADDITIONS — the rows the reference snapshot predates
26670
26761
  * ---------------------------------------------------------------------------
26671
26762
  *
26672
- * `claude-opus-5-5` is the only such row today. The mirror list in the test is
26673
- * `LOCALLY_CURATED_MODEL_IDS`; the two must be changed together.
26763
+ * `claude-opus-5-5` and `claude-sonnet-5-5` are the two such rows today. The
26764
+ * mirror list in the test is `LOCALLY_CURATED_MODEL_IDS`; the two must be
26765
+ * changed together. Sonnet 5.5's own reasons are recorded at its row; what
26766
+ * follows below is Opus 5.5's.
26674
26767
  *
26675
26768
  * It is CURATED rather than transcribed because the snapshot this table copies
26676
26769
  * has no entry for it, and the one thing a transcription table must never do is
@@ -26722,8 +26815,8 @@ function loopbackRedirectUri(port = DEFAULT_CALLBACK_PORT) {
26722
26815
  * the user has to reach.
26723
26816
  */
26724
26817
  /**
26725
- * Fifteen rows: the snapshot's own 14, in the snapshot's own declaration order,
26726
- * followed by the curated row the snapshot predates (see LOCAL ADDITIONS above).
26818
+ * Sixteen rows: the snapshot's own 14, in the snapshot's own declaration order,
26819
+ * followed by the curated rows the snapshot predates (see LOCAL ADDITIONS above).
26727
26820
  *
26728
26821
  * The order matters and is not cosmetic. The test asserts the snapshot's ids
26729
26822
  * appear in the table as a SUBSEQUENCE, so a curated row may be appended or
@@ -26991,7 +27084,26 @@ const CLAUDE_MODELS = Object.freeze([
26991
27084
  "max"
26992
27085
  ],
26993
27086
  canDisableThinking: false,
26994
- minCliVersion: "2.1.280"
27087
+ minCliVersion: "2.1.280",
27088
+ bindsThinkingToPrefix: true
27089
+ },
27090
+ {
27091
+ id: "claude-sonnet-5-5",
27092
+ name: "Claude Sonnet 5.5",
27093
+ contextWindow: 1e6,
27094
+ maxTokens: 128e3,
27095
+ supportsImage: true,
27096
+ supportsTemperature: false,
27097
+ thinkingMode: "mid-convo",
27098
+ reasoningEfforts: [
27099
+ "low",
27100
+ "medium",
27101
+ "high",
27102
+ "xhigh",
27103
+ "max"
27104
+ ],
27105
+ canDisableThinking: false,
27106
+ bindsThinkingToPrefix: true
26995
27107
  }
26996
27108
  ]);
26997
27109
  /** Every id the fallback table knows, in declaration order. */
@@ -27091,6 +27203,10 @@ function claudeThinkingMode(modelId, catalog) {
27091
27203
  function claudeModelSupportsTemperature(modelId, catalog) {
27092
27204
  return resolveClaudeModel(modelId, catalog ?? CLAUDE_MODELS).supportsTemperature;
27093
27205
  }
27206
+ /** Whether one model runs the documented preserved-thinking prefix check. */
27207
+ function claudeModelBindsThinkingToPrefix(modelId, catalog) {
27208
+ return resolveClaudeModel(modelId, catalog ?? CLAUDE_MODELS).bindsThinkingToPrefix === true;
27209
+ }
27094
27210
  /** Whether thinking can be turned off for one model. */
27095
27211
  function claudeModelCanDisableThinking(modelId, catalog) {
27096
27212
  return resolveClaudeModel(modelId, catalog ?? CLAUDE_MODELS).canDisableThinking;
@@ -27916,6 +28032,7 @@ function claudeBetas(options = {}) {
27916
28032
  if (options.model === void 0 || !isHaikuModel(options.model)) betas.push(CLAUDE_CODE_BETA);
27917
28033
  betas.push(OAUTH_BETA);
27918
28034
  if (options.thinking === true) betas.push(INTERLEAVED_THINKING_BETA);
28035
+ if (options.thinkingBinding === true) betas.push(THINKING_BINDING_CONTROLS_BETA);
27919
28036
  return betas;
27920
28037
  }
27921
28038
  /** The `User-Agent` value this line reports for one surface. */
@@ -28062,7 +28179,7 @@ async function performCatalogLoad(credentials, options) {
28062
28179
  let models;
28063
28180
  let live = false;
28064
28181
  try {
28065
- const response = await fetchFn("https://api.anthropic.com/v1/models", {
28182
+ const response = await fetchFn("https://api.anthropic.com/v1/models?limit=1000", {
28066
28183
  method: "GET",
28067
28184
  headers: buildClaudeHeaders(credentials.accessToken, { method: "GET" }),
28068
28185
  signal: timeoutSignal(options.signal, DISCOVERY_TIMEOUT_MS)
@@ -28529,8 +28646,15 @@ function classifyFailure(status, bodyText, headers = {}) {
28529
28646
  retryAfterMs: retryAfter
28530
28647
  };
28531
28648
  switch (envelope.type) {
28532
- case ERROR_TYPE.AUTHENTICATION:
28533
28649
  case ERROR_TYPE.PERMISSION: return {
28650
+ ...base,
28651
+ kind: "request",
28652
+ message: envelope.message ?? "This Claude account is not permitted to use the requested resource.",
28653
+ retryable: false,
28654
+ accountScoped: false,
28655
+ resetsAt: null
28656
+ };
28657
+ case ERROR_TYPE.AUTHENTICATION: return {
28534
28658
  ...base,
28535
28659
  kind: "credential",
28536
28660
  message: envelope.message ?? "Claude rejected the stored credential. Sign in again.",
@@ -28688,7 +28812,9 @@ function classifyFailure(status, bodyText, headers = {}) {
28688
28812
  * everything, including a caller asking for thinking off: the
28689
28813
  * block_binding exists so a prefix mismatch is dropped instead of
28690
28814
  * surfacing as a persistent 400, and suppressing it is exactly
28691
- * how that 400 comes back.
28815
+ * how that 400 comes back. The field is itself a 400 unless the
28816
+ * request carries the thinking-binding-controls beta, so the
28817
+ * adapter reads claudeBodyBindsThinking(body) to add it.
28692
28818
  * adaptive -> { type: 'adaptive', display: 'summarized' }, plus
28693
28819
  * output_config = { effort } when an effort was named. The budget
28694
28820
  * form is rejected by these models.
@@ -28781,11 +28907,12 @@ function classifyFailure(status, bodyText, headers = {}) {
28781
28907
  * 'error' event THROWS, because a severed reply flushed as a clean stop is a
28782
28908
  * wrong answer rather than a visible failure.
28783
28909
  *
28784
- * Reasoning deltas are BUFFERED and emitted as one block at content_block_stop
28785
- * (CHOICE, see emitPendingThinking): the signature that decides whether the
28786
- * block may be replayed at all arrives at the END of the block, so a signed
28787
- * block cannot be recognized before then, and a reasoning block that cannot be
28788
- * replayed is not content the caller should be told it received.
28910
+ * Reasoning deltas are STREAMED as they arrive (see openThinkingBlock); only the
28911
+ * replay entry waits for content_block_stop, because the signature that decides
28912
+ * whether the block may be replayed arrives at the END of the block. They used
28913
+ * to be buffered until the stop, which made DSH start the step's first-token
28914
+ * clock after all thinking was done - so TPS divided every thinking token by the
28915
+ * answer's time alone, and TTFT absorbed the whole thinking phase.
28789
28916
  *
28790
28917
  * ---------------------------------------------------------------------------
28791
28918
  * 7. PROMPT CACHING IS ON BY DEFAULT, AND IT IS THE REQUEST THAT TURNS IT ON
@@ -28884,9 +29011,17 @@ function jsonSafeValue(value, seen = /* @__PURE__ */ new WeakSet()) {
28884
29011
  seen.delete(value);
28885
29012
  }
28886
29013
  }
28887
- /** Strip a NUL that would truncate the string inside a JSON encoder. */
29014
+ /**
29015
+ * Strip what the wire cannot carry: a NUL, which would truncate the string
29016
+ * inside a JSON encoder, and an UNPAIRED surrogate. JSON.stringify writes the
29017
+ * latter as a lone `\uD83D` escape, which upstream rejects as invalid JSON ("no
29018
+ * low surrogate in string") - and since the text sits in history, every later
29019
+ * request fails the same way. A tool output sliced mid-emoji is enough to
29020
+ * produce one. Paired surrogates (emoji) are untouched. Same rule as the
29021
+ * reference's sanitizeSurrogates.
29022
+ */
28888
29023
  function sanitizeText(text) {
28889
- return text.replace(/\0/g, "");
29024
+ return text.replace(/\0|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
28890
29025
  }
28891
29026
  /**
28892
29027
  * Whether one failure is a cancellation rather than a real read failure.
@@ -29101,7 +29236,8 @@ function claudeThinking(model, effort, level, maxTokens, customBudgets) {
29101
29236
  if (mode === "adaptive") return {
29102
29237
  thinking: {
29103
29238
  type: "adaptive",
29104
- display: "summarized"
29239
+ display: "summarized",
29240
+ ...claudeModelBindsThinkingToPrefix(model) ? { block_binding: { prefix_mismatch_behavior: "drop_block" } } : {}
29105
29241
  },
29106
29242
  ...namedEffort === void 0 ? {} : { outputConfig: { effort: namedEffort } }
29107
29243
  };
@@ -29664,9 +29800,10 @@ function toolResultContent(blocks, images) {
29664
29800
  for (const block of blocks) {
29665
29801
  if (!isRecord$5(block)) continue;
29666
29802
  if (block.type === "text" && typeof block.text === "string") {
29667
- out.push({
29803
+ const text = sanitizeText(block.text);
29804
+ if (text !== "") out.push({
29668
29805
  type: "text",
29669
- text: sanitizeText(block.text)
29806
+ text
29670
29807
  });
29671
29808
  continue;
29672
29809
  }
@@ -29686,7 +29823,7 @@ function toolResultContent(blocks, images) {
29686
29823
  if (!hasImage) return void 0;
29687
29824
  if (out[0]?.type !== "text") out.unshift({
29688
29825
  type: "text",
29689
- text: ""
29826
+ text: "(see attached image)"
29690
29827
  });
29691
29828
  return out;
29692
29829
  }
@@ -29815,6 +29952,17 @@ function buildClaudeRequestBody(options, images = NO_RESOLVED_IMAGES, request =
29815
29952
  return body;
29816
29953
  }
29817
29954
  /**
29955
+ * Whether one built body carries `thinking.block_binding`.
29956
+ *
29957
+ * Read off the BODY rather than re-derived from the catalog, because the header
29958
+ * this answers for (THINKING_BINDING_CONTROLS_BETA) exists to license that one
29959
+ * field: sent without it the field is a 400 on every request, so the header must
29960
+ * follow the bytes, not a second copy of the rule that produced them.
29961
+ */
29962
+ function claudeBodyBindsThinking(body) {
29963
+ return isRecord$5(body.thinking) && body.thinking.block_binding !== void 0;
29964
+ }
29965
+ /**
29818
29966
  * Fresh stream state.
29819
29967
  *
29820
29968
  * @param toolNames - the SAME ClaudeToolNames the request was built with, so a
@@ -29839,6 +29987,7 @@ function createStreamState$1(toolNames) {
29839
29987
  cacheReadTokens: 0,
29840
29988
  cacheWriteTokens: 0,
29841
29989
  reasoningTokens: 0,
29990
+ reportedReasoningTokens: void 0,
29842
29991
  sawUsage: false,
29843
29992
  finished: false
29844
29993
  };
@@ -29935,14 +30084,15 @@ function closeToolCall(state, contentIndex) {
29935
30084
  }];
29936
30085
  }
29937
30086
  /**
29938
- * Emit one buffered thinking block.
30087
+ * Close one thinking block: end it for the caller and record its replay entry.
29939
30088
  *
29940
- * A signed block is emitted as DSH reasoning and replayed verbatim afterwards.
29941
- * An unsigned one is emitted WITHOUT a replay entry: it is still shown to the
29942
- * caller (the model did think, and hiding it would misreport the turn) but it
29943
- * cannot be sent back, and the request builder drops it on the next turn.
30089
+ * Its text already reached the caller as it streamed (see openThinkingBlock);
30090
+ * what waits for the close is the replay entry, because the signature arrives
30091
+ * last. A signed block is replayed verbatim afterwards. An unsigned one is
30092
+ * still shown (the model did think) but the request builder drops it on the
30093
+ * next turn, since it cannot be sent back.
29944
30094
  */
29945
- function emitPendingThinking(state, contentIndex) {
30095
+ function closeThinkingBlock(state, contentIndex) {
29946
30096
  const pending = state.pending.get(contentIndex);
29947
30097
  if (pending === void 0) return [];
29948
30098
  state.pending.delete(contentIndex);
@@ -29962,20 +30112,45 @@ function emitPendingThinking(state, contentIndex) {
29962
30112
  ...pending.signature === "" ? {} : { signature: pending.signature }
29963
30113
  };
29964
30114
  state.reasoningTokens += estimateReasoningTokens(text);
30115
+ return [{
30116
+ type: "block-end",
30117
+ index: pending.index,
30118
+ block
30119
+ }];
30120
+ }
30121
+ /**
30122
+ * Open one thinking block and stream whatever text its start already carries.
30123
+ *
30124
+ * STREAMED, not held to content_block_stop: DSH times a step's first token from
30125
+ * the first non-empty delta, and the usage it divides by that window counts every
30126
+ * thinking token. Holding the text until the block closed started the clock
30127
+ * after all the thinking was done - inflating TPS and TTFT alike - and kept the
30128
+ * UI blank for the whole thinking phase. Only the replay entry waits for the
30129
+ * close, because the signature arrives last.
30130
+ */
30131
+ function openThinkingBlock(state, contentIndex, block, kind) {
30132
+ const index = state.blocks.length;
30133
+ state.blocks.push({
30134
+ type: "reasoning",
30135
+ text: ""
30136
+ });
30137
+ const seed = kind === "thinking" ? asString$2(block.thinking) ?? "" : "";
30138
+ state.pending.set(contentIndex, {
30139
+ index,
30140
+ kind,
30141
+ text: seed,
30142
+ signature: asString$2(block.signature) ?? "",
30143
+ data: asString$2(block.data) ?? ""
30144
+ });
29965
30145
  const out = [{
29966
30146
  type: "block-start",
29967
- index: pending.index,
30147
+ index,
29968
30148
  blockType: "reasoning"
29969
30149
  }];
29970
- if (text !== "") out.push({
30150
+ if (seed !== "") out.push({
29971
30151
  type: "reasoning-delta",
29972
- index: pending.index,
29973
- text
29974
- });
29975
- out.push({
29976
- type: "block-end",
29977
- index: pending.index,
29978
- block
30152
+ index,
30153
+ text: seed
29979
30154
  });
29980
30155
  return out;
29981
30156
  }
@@ -29992,7 +30167,7 @@ function flushOpenBlocks(state) {
29992
30167
  });
29993
30168
  for (const [contentIndex, pending] of state.pending) closers.push({
29994
30169
  index: pending.index,
29995
- close: () => emitPendingThinking(state, contentIndex)
30170
+ close: () => closeThinkingBlock(state, contentIndex)
29996
30171
  });
29997
30172
  closers.sort((left, right) => left.index - right.index);
29998
30173
  const out = [];
@@ -30011,17 +30186,25 @@ function flushOpenBlocks(state) {
30011
30186
  * the billed-input total unaccountable.
30012
30187
  */
30013
30188
  function tokenUsage(state) {
30189
+ const reasoningTokens = Math.min(state.reportedReasoningTokens ?? state.reasoningTokens, state.outputTokens);
30014
30190
  return {
30015
30191
  inputTokens: state.inputTokens,
30016
30192
  outputTokens: state.outputTokens,
30017
30193
  ...state.cacheReadTokens > 0 ? { cacheReadTokens: state.cacheReadTokens } : {},
30018
30194
  ...state.cacheWriteTokens > 0 ? { cacheWriteTokens: state.cacheWriteTokens } : {},
30019
- ...state.reasoningTokens > 0 ? { reasoningTokens: state.reasoningTokens } : {}
30195
+ ...reasoningTokens > 0 ? { reasoningTokens } : {}
30020
30196
  };
30021
30197
  }
30022
30198
  function finishReasonFor(state) {
30023
30199
  const reason = state.finishReason ?? "";
30024
- if (reason === "max_tokens") return { kind: "max-tokens" };
30200
+ if (reason === "refusal" || reason === "sensitive") return {
30201
+ kind: "error",
30202
+ failure: {
30203
+ message: state.stopExplanation ?? "Claude declined to respond (stop_reason: " + reason + ").",
30204
+ code: "PROVIDER_ERROR"
30205
+ }
30206
+ };
30207
+ if (reason === "max_tokens" || reason === "model_context_window_exceeded") return { kind: "max-tokens" };
30025
30208
  if (state.hasToolCall || reason === "tool_use") return { kind: "tool-calls" };
30026
30209
  return { kind: "stop" };
30027
30210
  }
@@ -30069,6 +30252,8 @@ function applyUsage(state, usage, cumulativeOutput) {
30069
30252
  if (usage.cache_creation_input_tokens !== void 0) state.cacheWriteTokens = numberOr(usage.cache_creation_input_tokens, state.cacheWriteTokens);
30070
30253
  if (usage.cache_read_input_tokens !== void 0) state.cacheReadTokens = numberOr(usage.cache_read_input_tokens, state.cacheReadTokens);
30071
30254
  if (usage.output_tokens !== void 0) state.outputTokens = cumulativeOutput ? numberOr(usage.output_tokens, state.outputTokens) : state.outputTokens + numberOr(usage.output_tokens, 0);
30255
+ const details = isRecord$5(usage.output_tokens_details) ? usage.output_tokens_details : void 0;
30256
+ if (details !== void 0 && details.thinking_tokens !== void 0) state.reportedReasoningTokens = numberOr(details.thinking_tokens, state.reportedReasoningTokens ?? 0);
30072
30257
  }
30073
30258
  /**
30074
30259
  * Feed one SSE line of a Messages response into the state.
@@ -30139,19 +30324,8 @@ function processStreamLine(line, state) {
30139
30324
  return out;
30140
30325
  }
30141
30326
  if (blockType === "thinking" || blockType === "redacted_thinking") {
30142
- if (state.pending.get(contentIndex) !== void 0) out.push(...emitPendingThinking(state, contentIndex));
30143
- const index = state.blocks.length;
30144
- state.blocks.push({
30145
- type: "reasoning",
30146
- text: ""
30147
- });
30148
- state.pending.set(contentIndex, {
30149
- index,
30150
- kind: blockType === "redacted_thinking" ? "redacted_thinking" : "thinking",
30151
- text: asString$2(block.thinking) ?? "",
30152
- signature: asString$2(block.signature) ?? "",
30153
- data: asString$2(block.data) ?? ""
30154
- });
30327
+ out.push(...closeThinkingBlock(state, contentIndex));
30328
+ out.push(...openThinkingBlock(state, contentIndex, block, blockType));
30155
30329
  return out;
30156
30330
  }
30157
30331
  out.push(...openTextAt(state, contentIndex).chunks);
@@ -30184,8 +30358,14 @@ function processStreamLine(line, state) {
30184
30358
  }
30185
30359
  if (deltaType === "thinking_delta") {
30186
30360
  const pending = state.pending.get(contentIndex);
30187
- const text = asString$2(delta.thinking) ?? "";
30188
- if (pending !== void 0 && text !== "") pending.text += sanitizeText(text);
30361
+ const text = sanitizeText(asString$2(delta.thinking) ?? "");
30362
+ if (pending === void 0 || pending.kind !== "thinking" || text === "") return out;
30363
+ pending.text += text;
30364
+ out.push({
30365
+ type: "reasoning-delta",
30366
+ index: pending.index,
30367
+ text
30368
+ });
30189
30369
  return out;
30190
30370
  }
30191
30371
  const text = asString$2(delta.text) ?? "";
@@ -30204,13 +30384,16 @@ function processStreamLine(line, state) {
30204
30384
  if (type === "content_block_stop") {
30205
30385
  const contentIndex = numberOr(event.index, -1);
30206
30386
  if (state.toolCalls.has(contentIndex)) return closeToolCall(state, contentIndex);
30207
- if (state.pending.has(contentIndex)) return emitPendingThinking(state, contentIndex);
30387
+ if (state.pending.has(contentIndex)) return closeThinkingBlock(state, contentIndex);
30208
30388
  return closeTextBlock(state, contentIndex);
30209
30389
  }
30210
30390
  if (type === "message_delta") {
30211
30391
  const delta = isRecord$5(event.delta) ? event.delta : void 0;
30212
30392
  const stop = delta === void 0 ? void 0 : asString$2(delta.stop_reason);
30213
30393
  if (stop !== void 0 && stop !== "") state.finishReason = stop;
30394
+ const details = delta !== void 0 && isRecord$5(delta.stop_details) ? delta.stop_details : void 0;
30395
+ const explanation = details === void 0 ? void 0 : asString$2(details.explanation);
30396
+ if (explanation !== void 0 && explanation !== "") state.stopExplanation = explanation;
30214
30397
  const usage = isRecord$5(event.usage) ? event.usage : void 0;
30215
30398
  if (usage !== void 0) applyUsage(state, usage, true);
30216
30399
  return out;
@@ -30645,7 +30828,7 @@ async function refreshAccessToken(refreshToken, options = {}) {
30645
30828
  try {
30646
30829
  return await postTokenGrant(body, options, false, [refreshToken]);
30647
30830
  } catch (error) {
30648
- if (error instanceof ClaudeUnauthorizedError) throw error;
30831
+ if (!(error instanceof ClaudeRetryableError)) throw error;
30649
30832
  lastError = error;
30650
30833
  }
30651
30834
  if (attempt < REFRESH_MAX_ATTEMPTS - 1) await sleep(REFRESH_BACKOFF_BASE_MS * 2 ** attempt, options.signal);
@@ -31730,19 +31913,20 @@ var ClaudeAdapter = class extends LlmAdapter {
31730
31913
  * no credentials were resolved for it.
31731
31914
  */
31732
31915
  async attemptRequest(credentials, requestOptions, images, toolNames, thinking, signal, fetchFn) {
31733
- const body = JSON.stringify(buildClaudeRequestBody(requestOptions, images, {
31916
+ const payload = buildClaudeRequestBody(requestOptions, images, {
31734
31917
  toolNames,
31735
31918
  cacheControl: true
31736
- }));
31919
+ });
31737
31920
  try {
31738
31921
  return await fetchFn("https://api.anthropic.com/v1/messages?beta=true", {
31739
31922
  method: "POST",
31740
31923
  headers: buildClaudeHeaders(credentials.accessToken, {
31741
31924
  model: requestOptions.model,
31742
31925
  thinking,
31926
+ thinkingBinding: claudeBodyBindsThinking(payload),
31743
31927
  method: "POST"
31744
31928
  }),
31745
- body,
31929
+ body: JSON.stringify(payload),
31746
31930
  signal
31747
31931
  });
31748
31932
  } catch (error) {
@@ -33512,24 +33696,18 @@ function registerClaudeRoutes(ctx, store, modelSettings, preferences, options =
33512
33696
  * is addressed at THAT account, which is what makes a re-login repair the row
33513
33697
  * the user clicked instead of adding a second one.
33514
33698
  */
33515
- const loginStore = (accountId) => {
33516
- const base = pool !== void 0 && accountId !== void 0 ? pool.credentialStoreFor(accountId) : {
33517
- read: async () => (await activeAccount()).credentials ?? null,
33518
- write: async (credentials) => {
33519
- if (pool !== void 0) await pool.addAccount(credentials);
33520
- else await store.saveAccount(credentials);
33521
- }
33522
- };
33523
- return {
33524
- read: () => base.read(),
33525
- write: async (credentials) => {
33526
- await base.write(credentials);
33527
- if (pool !== void 0 && accountId !== void 0) await pool.clearAuthFailed(accountId).catch(() => void 0);
33528
- clearCachedQuota$1();
33529
- clearCachedCatalog();
33699
+ const loginStore = (accountId) => ({
33700
+ read: async () => pool !== void 0 && accountId !== void 0 ? pool.credentialStoreFor(accountId).read() : (await activeAccount()).credentials ?? null,
33701
+ write: async (credentials) => {
33702
+ if (pool === void 0) await store.saveAccount(credentials);
33703
+ else {
33704
+ const landed = await pool.addAccount(credentials);
33705
+ if (accountId !== void 0 && landed.id === accountId) await pool.clearAuthFailed(accountId).catch(() => void 0);
33530
33706
  }
33531
- };
33532
- };
33707
+ clearCachedQuota$1();
33708
+ clearCachedCatalog();
33709
+ }
33710
+ });
33533
33711
  const readStatus = (quotaError = null) => activeAccount().then((active) => getClaudeWebStatus(store, modelSettings, preferences, options, quotaError, active));
33534
33712
  return ctx.webServer.register({
33535
33713
  kind: "prefix",
@@ -35133,6 +35311,40 @@ function inheritedRouteOf(agent) {
35133
35311
  model: asString$1(agent?.options?.model)
35134
35312
  };
35135
35313
  }
35314
+ /**
35315
+ * Resolve the provider/model pair a call actually names, tolerating a model
35316
+ * value that already carries its provider prefix.
35317
+ *
35318
+ * The discovery tool that hands a model to the model renders each route as
35319
+ * `provider/model` (`minimax-code/MiniMax-M3.1-Flash-Preview`), and the
35320
+ * `model` parameter is documented as "Model id interpreted by provider". A
35321
+ * model that copies that line into `model` while naming `provider` separately
35322
+ * therefore sends `provider` = `minimax-code` and `model` =
35323
+ * `minimax-code/MiniMax-M3.1-Flash-Preview`. That is the single most likely way
35324
+ * a correct-looking call is formed, and treating the doubled text as an opaque
35325
+ * model id both denies it and — had it been authorized — resolved a model the
35326
+ * provider does not have. The prefix is stripped only when the remainder is a
35327
+ * non-empty model id.
35328
+ *
35329
+ * @param provider - Provider the call named, when it named one.
35330
+ * @param model - Model the call named, when it named one.
35331
+ * @returns the exact pair, or undefined when the call named no complete pair.
35332
+ */
35333
+ function explicitRoutePair(provider, model) {
35334
+ const providerId = asString$1(provider);
35335
+ const modelId = asString$1(model);
35336
+ if (providerId === void 0 || modelId === void 0) return void 0;
35337
+ if (providerId.length === 0 || modelId.length === 0) return void 0;
35338
+ const prefix = providerId + "/";
35339
+ if (modelId.startsWith(prefix) && modelId.length > prefix.length) return {
35340
+ provider: providerId,
35341
+ model: modelId.slice(prefix.length)
35342
+ };
35343
+ return {
35344
+ provider: providerId,
35345
+ model: modelId
35346
+ };
35347
+ }
35136
35348
  /** Whether an exact route is authorized by a allowlist. */
35137
35349
  function routesInclude(allowed, provider, model) {
35138
35350
  return allowed.some((route) => route.provider === provider && route.model === model);
@@ -35177,6 +35389,32 @@ function unauthorizedRouteReason(provider, model, allowed, explicit) {
35177
35389
  return `subagent model selection: ${explicit ? "the model this call selected is not on the Session allowlist" : "the route this call would inherit from the parent is not on the Session allowlist"} (${route}). Provide an authorized provider and model — ${authorizedRoutesText(allowed)} — using list_subagent_models to inspect their reasoning efforts.`;
35178
35390
  }
35179
35391
  /**
35392
+ * Build the denial reason for a call that spelled an authorized route the way
35393
+ * the discovery tool prints it.
35394
+ *
35395
+ * `list_subagent_models` renders every route as `provider/model`, and the
35396
+ * delegation tool documents `model` as "Model id interpreted by provider". A
35397
+ * model that copies the printed line into `model` while also naming `provider`
35398
+ * therefore sends a doubled id — the single most likely way a correct-looking
35399
+ * call is formed, and the reason a delegation is refused even though the route
35400
+ * the user picked is authorized.
35401
+ *
35402
+ * The id is deliberately NOT accepted here. The guard is the plugin plane, so
35403
+ * it cannot rewrite the arguments the delegation tool executes: that tool hands
35404
+ * `model` to the adapter verbatim, and the adapter matches it against its own
35405
+ * table, where `minimax-code/MiniMax-M3.1-Flash-Preview` is not a model. A
35406
+ * guard that accepted the doubled text would turn this clear refusal into a
35407
+ * downstream `NO_ADAPTER`/unknown-model failure. Naming the two fields
35408
+ * separately is what actually unblocks the call.
35409
+ *
35410
+ * @param pair - The authorized pair the call described as one string.
35411
+ * @param allowed - Routes the calling Session authorizes.
35412
+ * @returns the corrective reason handed back to the model.
35413
+ */
35414
+ function prefixedModelReason(pair, allowed) {
35415
+ return `subagent model selection: this call passed a whole route as \`model\`. Pass \`provider\` and \`model\` as two separate fields — provider "${pair.provider}", model "${pair.model}" — because \`provider/model\` is only how list_subagent_models prints a route, not a model id. Authorized routes: ${authorizedRoutesText(allowed)}.`;
35416
+ }
35417
+ /**
35180
35418
  * Build the denial reason for an inherit-mode delegation that named a route.
35181
35419
  * The fork backend accepts no route parameters, so a named pair is not a
35182
35420
  * partial override the backend would ignore — it is a request to change the
@@ -35250,6 +35488,8 @@ function delegationDenialReason(agent, toolName, args, preference, sessions, too
35250
35488
  if (requestedProvider === void 0) return requestedModel === void 0 ? missingRouteReason(allowed) : partialRouteReason("provider", requestedModel, allowed);
35251
35489
  if (requestedModel === void 0) return partialRouteReason("model", requestedProvider, allowed);
35252
35490
  if (routesInclude(allowed, requestedProvider, requestedModel)) return void 0;
35491
+ const pair = explicitRoutePair(requestedProvider, requestedModel);
35492
+ if (pair !== void 0 && pair.model !== requestedModel && routesInclude(allowed, pair.provider, pair.model)) return prefixedModelReason(pair, allowed);
35253
35493
  return unauthorizedRouteReason(requestedProvider, requestedModel, allowed, true);
35254
35494
  }
35255
35495
  /**