@eddyskywalker/dsh-chatgpt-subscription 0.11.5 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +36 -2
  3. package/lib/client.js +182 -59
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +465 -25
  6. package/lib/types/client/CodexSubscriptionSection.d.ts +18 -0
  7. package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
  8. package/lib/types/client/antigravity/styles.d.ts.map +1 -1
  9. package/lib/types/client/claude/styles.d.ts.map +1 -1
  10. package/lib/types/client/common/plugin-style.d.ts +23 -0
  11. package/lib/types/client/common/plugin-style.d.ts.map +1 -0
  12. package/lib/types/client/common/styles.d.ts.map +1 -1
  13. package/lib/types/client/kimi-code/styles.d.ts.map +1 -1
  14. package/lib/types/client/locales.d.ts +45 -5
  15. package/lib/types/client/locales.d.ts.map +1 -1
  16. package/lib/types/client/mermaid/styles.d.ts.map +1 -1
  17. package/lib/types/client/minimax-code/styles.d.ts.map +1 -1
  18. package/lib/types/client/styles.d.ts +5 -0
  19. package/lib/types/client/styles.d.ts.map +1 -1
  20. package/lib/types/client/workbuddy/styles.d.ts.map +1 -1
  21. package/lib/types/host/antigravity/mapper.d.ts +3 -2
  22. package/lib/types/host/antigravity/mapper.d.ts.map +1 -1
  23. package/lib/types/host/antigravity/tool-schema.d.ts +13 -1
  24. package/lib/types/host/antigravity/tool-schema.d.ts.map +1 -1
  25. package/lib/types/host/claude/adapter.d.ts.map +1 -1
  26. package/lib/types/host/claude/mapper.d.ts +57 -3
  27. package/lib/types/host/claude/mapper.d.ts.map +1 -1
  28. package/lib/types/host/codex-fetch.d.ts.map +1 -1
  29. package/lib/types/host/command-code/adapter.d.ts.map +1 -1
  30. package/lib/types/host/common/context-budget-fetch.d.ts +7 -0
  31. package/lib/types/host/common/context-budget-fetch.d.ts.map +1 -0
  32. package/lib/types/host/fetch-address-policy.d.ts +2 -1
  33. package/lib/types/host/fetch-address-policy.d.ts.map +1 -1
  34. package/lib/types/host/fetch-configuration.d.ts +6 -0
  35. package/lib/types/host/fetch-configuration.d.ts.map +1 -0
  36. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  37. package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
  38. package/lib/types/host/minimax-code/adapter.d.ts.map +1 -1
  39. package/lib/types/host/routes.d.ts +2 -2
  40. package/lib/types/host/routes.d.ts.map +1 -1
  41. package/lib/types/host/search-provider-switcher.d.ts +2 -0
  42. package/lib/types/host/search-provider-switcher.d.ts.map +1 -1
  43. package/lib/types/host/workbuddy/adapter.d.ts.map +1 -1
  44. package/lib/types/index.d.ts +7 -0
  45. package/lib/types/index.d.ts.map +1 -1
  46. package/lib/types/shared/contracts.d.ts +11 -0
  47. package/lib/types/shared/contracts.d.ts.map +1 -1
  48. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -2984,6 +2984,24 @@ var CodexAccountPool = class extends AccountPoolCore {
2984
2984
  }
2985
2985
  };
2986
2986
  //#endregion
2987
+ //#region src/host/fetch-configuration.ts
2988
+ const DEFAULT_FETCH_MAX_BODY_CHARS = 1e5;
2989
+ const DEFAULT_FETCH_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
2990
+ function positiveFetchLimit(value, fallback, name) {
2991
+ const resolved = value === void 0 ? fallback : value;
2992
+ if (!Number.isSafeInteger(resolved) || resolved < 1) throw new Error(`${name} must be a positive safe integer.`);
2993
+ return resolved;
2994
+ }
2995
+ function resolveFetchConfiguration(config) {
2996
+ const fetchProvider = config.fetchProvider ?? "auto";
2997
+ if (fetchProvider !== "auto" && fetchProvider !== "plugin" && fetchProvider !== "dsh") throw new Error("fetchProvider must be auto, plugin, or dsh.");
2998
+ return {
2999
+ fetchProvider,
3000
+ fetchMaxBodyChars: positiveFetchLimit(config.fetchMaxBodyChars, DEFAULT_FETCH_MAX_BODY_CHARS, "fetchMaxBodyChars"),
3001
+ fetchMaxResponseBytes: positiveFetchLimit(config.fetchMaxResponseBytes, DEFAULT_FETCH_MAX_RESPONSE_BYTES, "fetchMaxResponseBytes")
3002
+ };
3003
+ }
3004
+ //#endregion
2987
3005
  //#region src/host/fetch-address-policy.ts
2988
3006
  /**
2989
3007
  * Destination policy for this plugin's fetch provider.
@@ -3001,7 +3019,8 @@ var CodexAccountPool = class extends AccountPoolCore {
3001
3019
  * proxy on this machine must not become a path into loopback or a LAN — and it
3002
3020
  * also refuses a name this machine resolves into private space. Only the proxy's
3003
3021
  * own fake-ip answers are accepted, since that is what the proxy's name
3004
- * resolution looks like from here; the URL hostname still reaches the proxy
3022
+ * resolution looks like from here: the IPv4 one, and the IPv6 one only beside it
3023
+ * (see assertPublicFetchTarget). The URL hostname still reaches the proxy
3005
3024
  * intact, so the proxy decides where the name really goes.
3006
3025
  *
3007
3026
  * @module dsh-chatgpt-subscription/fetch-address-policy
@@ -3114,11 +3133,22 @@ function isNonPublicIpLiteral(hostname) {
3114
3133
  */
3115
3134
  function assertPublicFetchTarget(hostname, addresses) {
3116
3135
  if (isNonPublicIpLiteral(hostname)) throw new WebError(`URL hostname "${hostname}" is not a public IP address`, "WEB_BLOCKED_URL");
3136
+ const claimedByProxy = addresses.some(isProxyFakeIpAddress);
3117
3137
  for (const address of addresses) {
3118
3138
  if (isPublicIpAddress(address) || isProxyFakeIpAddress(address)) continue;
3139
+ if (claimedByProxy && isUnassignedUniqueLocal(address)) continue;
3119
3140
  throw new WebError(`URL hostname "${hostname}" resolves to a non-public IP address`, "WEB_BLOCKED_URL");
3120
3141
  }
3121
3142
  }
3143
+ /**
3144
+ * Whether an IPv6 address is inside fc00::/8: the unique-local half RFC 4193
3145
+ * leaves unassigned (real unique-local networks are numbered from fd00::/8), and
3146
+ * where proxies place their IPv6 fake-ip range.
3147
+ */
3148
+ function isUnassignedUniqueLocal(address) {
3149
+ const words = ipv6Words(unbracket(address));
3150
+ return words !== void 0 && (words[0] ?? 0) >> 8 === 252;
3151
+ }
3122
3152
  /** Whether a dotted quad is public, using {@link NON_PUBLIC_IPV4}. */
3123
3153
  function isPublicIpv4(address) {
3124
3154
  const value = ipv4Value(address);
@@ -3204,12 +3234,10 @@ function unbracket(hostname) {
3204
3234
  }
3205
3235
  //#endregion
3206
3236
  //#region src/host/codex-fetch.ts
3207
- const DEFAULT_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
3208
- const DEFAULT_MAX_BODY_CHARS = 1e5;
3209
3237
  function createCodexFetchProvider(options = {}) {
3210
3238
  const fetchFn = options.fetchFn ?? fetch;
3211
- const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
3212
- const maxBodyChars = options.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS;
3239
+ const maxResponseBytes = positiveFetchLimit(options.maxResponseBytes, DEFAULT_FETCH_MAX_RESPONSE_BYTES, "maxResponseBytes");
3240
+ const maxBodyChars = positiveFetchLimit(options.maxBodyChars, DEFAULT_FETCH_MAX_BODY_CHARS, "maxBodyChars");
3213
3241
  const resolveHostAddresses = options.resolveHostAddresses ?? lookupHostAddresses;
3214
3242
  return {
3215
3243
  id: CODEX_FETCH_PROVIDER_ID,
@@ -7363,7 +7391,7 @@ function identityKey(credentials) {
7363
7391
  //#endregion
7364
7392
  //#region src/host/routes.ts
7365
7393
  const MAX_BODY_BYTES$6 = 64 * 1024;
7366
- function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, routeAudit, accountPool) {
7394
+ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, routeAudit, accountPool, fetchConfiguration) {
7367
7395
  const handler = async (request, response) => {
7368
7396
  const url = new URL(request.url ?? "/", "http://dsh.local");
7369
7397
  if (request.method === "GET" && url.pathname === `/api/dsh-chatgpt-subscription/status`) {
@@ -7378,7 +7406,8 @@ function registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwit
7378
7406
  preferences: preferences.status(),
7379
7407
  detectedProxy: proxyManager?.getSystemProxy() ?? null,
7380
7408
  activeProxy: proxyManager?.resolveActiveProxyUrl() ?? null,
7381
- switcher: searchSwitcher?.status() ?? null
7409
+ switcher: searchSwitcher?.status() ?? null,
7410
+ fetchConfiguration
7382
7411
  }
7383
7412
  });
7384
7413
  return;
@@ -7790,10 +7819,13 @@ var SearchProviderSwitcher = class {
7790
7819
  }
7791
7820
  const codexSelected = preference === SEARCH_PROVIDER_CODEX;
7792
7821
  const nextSearch = codexSelected ? CODEX_SEARCH_PROVIDER_ID : this.originalSearchProvider;
7793
- const nextFetch = codexSelected || options.pluginFetch === true ? CODEX_FETCH_PROVIDER_ID : this.originalFetchProvider;
7822
+ const nextFetch = options.fetchProvider === "plugin" || options.fetchProvider !== "dsh" && (codexSelected || options.pluginFetch === true) ? CODEX_FETCH_PROVIDER_ID : this.originalFetchProvider;
7794
7823
  const configured = config.searchProvider === nextSearch && config.fetchProvider === nextFetch;
7795
7824
  const applied = running === void 0 || running.searchProvider === nextSearch && running.fetchProvider === nextFetch;
7796
- if (configured && applied) return;
7825
+ if (configured && applied) {
7826
+ this.state = "applied";
7827
+ return;
7828
+ }
7797
7829
  const nextConfig = { ...config };
7798
7830
  if (nextSearch === void 0) delete nextConfig.searchProvider;
7799
7831
  else nextConfig.searchProvider = nextSearch;
@@ -8562,7 +8594,170 @@ function mergeSchemas(left, right) {
8562
8594
  if (Array.isArray(left.required) && Array.isArray(right.required)) merged.required = [.../* @__PURE__ */ new Set([...left.required, ...right.required])];
8563
8595
  return merged;
8564
8596
  }
8565
- function toAntigravityToolSchema(schema) {
8597
+ /** Value constraints a branch summary reports, in a stable order. */
8598
+ const constraintFields = [
8599
+ "format",
8600
+ "pattern",
8601
+ "minimum",
8602
+ "maximum",
8603
+ "minLength",
8604
+ "maxLength",
8605
+ "minItems",
8606
+ "maxItems",
8607
+ "minProperties",
8608
+ "maxProperties"
8609
+ ];
8610
+ /** Keys the same-type union merge below represents rather than drops. */
8611
+ const mergedTypeKeys = /* @__PURE__ */ new Set([
8612
+ "type",
8613
+ "enum",
8614
+ "nullable",
8615
+ "description",
8616
+ ...constraintFields
8617
+ ]);
8618
+ /**
8619
+ * One alternative in the words a model can act on.
8620
+ *
8621
+ * Folding removes the branch from the schema itself, so this text carries the
8622
+ * branch description and its constraints too: the type alone would leave the
8623
+ * model with strictly less than the schema it started from.
8624
+ */
8625
+ function describeBranch(branch) {
8626
+ const type = typeof branch.type === "string" ? branch.type : isRecord$22(branch.properties) ? "object" : isRecord$22(branch.items) ? "array" : "value";
8627
+ const details = [];
8628
+ if (isRecord$22(branch.properties)) {
8629
+ const names = Object.keys(branch.properties);
8630
+ if (names.length > 0) details.push(`{${names.join(",")}}`);
8631
+ }
8632
+ if (Array.isArray(branch.required) && branch.required.length > 0) details.push(`requires ${branch.required.map(String).join(",")}`);
8633
+ if (Array.isArray(branch.enum)) details.push(`enum[${branch.enum.map((value) => JSON.stringify(value)).join(",")}]`);
8634
+ for (const field of constraintFields) if (branch[field] !== void 0) details.push(`${field}=${JSON.stringify(branch[field])}`);
8635
+ if (branch.nullable === true) details.push("or null");
8636
+ const summary = details.length > 0 ? `${type} ${details.join(" ")}` : type;
8637
+ const description = typeof branch.description === "string" ? branch.description.trim() : "";
8638
+ return description === "" ? summary : `${summary} — "${description}"`;
8639
+ }
8640
+ /** How well one branch can stand in for a whole union; a named object shape wins. */
8641
+ function branchRank(branch) {
8642
+ if (isRecord$22(branch.properties)) return 3;
8643
+ if (branch.type === "object") return 2;
8644
+ if (branch.type === "array") return 1;
8645
+ return 0;
8646
+ }
8647
+ /**
8648
+ * The one JSON Schema type a literal set declares, when it declares exactly one.
8649
+ *
8650
+ * Read from the caller's literals, never from the wire's stringified `enum`: a
8651
+ * `{ const: 5 }` branch is a number, and `String(5)` alone cannot say so.
8652
+ */
8653
+ function literalType(values) {
8654
+ const kinds = new Set(values.map((value) => typeof value));
8655
+ if (kinds.size !== 1) return void 0;
8656
+ const [kind] = kinds;
8657
+ return kind === "string" || kind === "number" || kind === "boolean" ? kind : void 0;
8658
+ }
8659
+ /**
8660
+ * The single form the wire can declare for a set of union branches.
8661
+ *
8662
+ * `folded` is false only when the declared form carries every constraint the
8663
+ * branches between them declared; once a constraint is only in the description,
8664
+ * the fold says so instead of passing a partial restatement off as the union.
8665
+ */
8666
+ function unionShape(branches) {
8667
+ if (branches.length === 1) return {
8668
+ shape: branches[0],
8669
+ folded: false
8670
+ };
8671
+ if (new Set(branches.map((branch) => branch.type)).size === 1 && branches.every((branch) => !isRecord$22(branch.properties) && !isRecord$22(branch.items))) {
8672
+ const shape = { type: branches[0].type };
8673
+ if (branches.every((branch) => Array.isArray(branch.enum))) {
8674
+ const values = [...new Set(branches.flatMap((branch) => branch.enum))];
8675
+ if (values.length > 0) shape.enum = values;
8676
+ }
8677
+ if (branches.some((branch) => branch.nullable === true)) shape.nullable = true;
8678
+ return {
8679
+ shape,
8680
+ folded: branches.some((branch) => Object.entries(branch).some(([key, value]) => !mergedTypeKeys.has(key) && !Object.is(shape[key], value)))
8681
+ };
8682
+ }
8683
+ return {
8684
+ shape: branches.reduce((best, branch) => branchRank(branch) > branchRank(best) ? branch : best),
8685
+ folded: true
8686
+ };
8687
+ }
8688
+ /** Append the folded union to whatever description the node already carried. */
8689
+ function withUnionHint(description, hint) {
8690
+ const base = typeof description === "string" ? description.trim() : "";
8691
+ return base === "" ? hint : `${base}\n\n${hint}`;
8692
+ }
8693
+ function unionHint(alternatives, declared) {
8694
+ const text = `Union of accepted forms: ${alternatives.map(describeBranch).join(" | ")}.`;
8695
+ return declared === void 0 ? text : `${text} Send "${describeBranch(declared)}": this gateway validates the declared form only, and the alternatives describe the original tool contract.`;
8696
+ }
8697
+ /**
8698
+ * One node's own fields, children folded, composition keys set aside.
8699
+ *
8700
+ * `anyOf`, `oneOf` and `allOf` are keywords only in these positions: a parameter
8701
+ * NAMED `anyOf` is data, so recursion goes through `properties` by name and never
8702
+ * reads a property key as a keyword.
8703
+ */
8704
+ function foldOwnFields(node) {
8705
+ const out = {};
8706
+ for (const [key, value] of Object.entries(node)) {
8707
+ if (key === "anyOf" || key === "oneOf" || key === "allOf") continue;
8708
+ if (key === "properties" && isRecord$22(value)) {
8709
+ out.properties = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, foldSchemaUnions(child)]));
8710
+ continue;
8711
+ }
8712
+ if (key === "items") {
8713
+ out.items = foldSchemaUnions(value);
8714
+ continue;
8715
+ }
8716
+ out[key] = value;
8717
+ }
8718
+ return out;
8719
+ }
8720
+ /**
8721
+ * Replace every composition keyword in one already-converted schema with a
8722
+ * single declared form, moving what folding drops into `description`.
8723
+ *
8724
+ * This runs after {@link toAntigravityToolSchema} resolved references and merged
8725
+ * `allOf`, so it is one pass over the wire shape and no second sanitizer can
8726
+ * strip the hint it just wrote.
8727
+ *
8728
+ * Folding NARROWS the wire contract: preferring the object branch leaves the
8729
+ * schema declaring an object, so a caller that would have sent the string form
8730
+ * now sends against a schema that does not mention it. That is the only way
8731
+ * past a gateway that refuses a composed schema outright, and the description
8732
+ * keeps the dropped forms visible as the original tool contract rather than
8733
+ * silently erasing them. DSH's own tool validator is the real gate: an
8734
+ * argument the model sends anyway fails there, naming the actual problem.
8735
+ */
8736
+ function foldSchemaUnions(node) {
8737
+ if (!isRecord$22(node)) return node;
8738
+ let own = foldOwnFields(node);
8739
+ if (Array.isArray(node.allOf)) for (const branch of node.allOf) {
8740
+ const folded = foldSchemaUnions(branch);
8741
+ if (isRecord$22(folded)) own = mergeSchemas(own, folded);
8742
+ }
8743
+ const alternatives = Array.isArray(node.anyOf) ? node.anyOf : Array.isArray(node.oneOf) ? node.oneOf : [];
8744
+ if (alternatives.length === 0) return own;
8745
+ const branches = alternatives.map((branch) => foldSchemaUnions(branch)).filter(isRecord$22);
8746
+ if (branches.length === 0) return own;
8747
+ const shapes = branches.filter((branch) => branch.type !== "null");
8748
+ const acceptsNull = branches.some((branch) => branch.type === "null");
8749
+ if (shapes.length === 0) {
8750
+ const onlyNull = mergeSchemas({ nullable: true }, own);
8751
+ onlyNull.description = withUnionHint(own.description, unionHint(branches, { nullable: true }));
8752
+ return onlyNull;
8753
+ }
8754
+ const { shape, folded } = unionShape(shapes);
8755
+ const merged = mergeSchemas(shape, own);
8756
+ if (acceptsNull) merged.nullable = true;
8757
+ if (branches.length > 1) merged.description = withUnionHint(merged.description, unionHint(branches, folded ? shape : void 0));
8758
+ return merged;
8759
+ }
8760
+ function toAntigravityToolSchema(schema, options = {}) {
8566
8761
  if (!isRecord$22(schema)) return schema;
8567
8762
  const root = schema;
8568
8763
  function resolveReference(reference) {
@@ -8605,9 +8800,17 @@ function toAntigravityToolSchema(schema) {
8605
8800
  "boolean"
8606
8801
  ].includes(typeof node.const)) out.enum = [String(node.const)];
8607
8802
  } else if (Array.isArray(node.enum)) out.enum = node.enum.map((value) => String(value));
8803
+ if (options.foldUnions && node.type === void 0 && !Array.isArray(node.type)) {
8804
+ const values = Object.hasOwn(node, "const") ? [node.const] : node.enum;
8805
+ if (Array.isArray(values) && values.length > 0) {
8806
+ const literal = literalType(values);
8807
+ if (literal !== void 0) out.type = literal;
8808
+ }
8809
+ }
8608
8810
  return mergeSchemas(inherited, out);
8609
8811
  }
8610
- return convert(root, /* @__PURE__ */ new Set());
8812
+ const converted = convert(root, /* @__PURE__ */ new Set());
8813
+ return options.foldUnions === true ? foldSchemaUnions(converted) : converted;
8611
8814
  }
8612
8815
  //#endregion
8613
8816
  //#region src/host/antigravity/mapper.ts
@@ -9018,15 +9221,27 @@ function convertMessages(options, model, runtimeModel, images = NO_RESOLVED_IMAG
9018
9221
  }
9019
9222
  return contents;
9020
9223
  }
9021
- function stripMetaSchema$3(schema) {
9022
- return toAntigravityToolSchema(schema);
9224
+ function stripMetaSchema$3(schema, options) {
9225
+ return toAntigravityToolSchema(schema, options);
9226
+ }
9227
+ /**
9228
+ * Whether this request's tool declarations must declare one form per parameter.
9229
+ *
9230
+ * Antigravity routes a Claude-family runtime model to Vertex Anthropic, whose
9231
+ * `input_schema` validation rejects a request whose tool schema still composes
9232
+ * itself with `anyOf`/`oneOf`/`allOf` (#39). Gemini ignores `anyOf`, so its
9233
+ * declarations keep the composed form they always sent.
9234
+ */
9235
+ function foldsToolSchemaUnions(modelId, runtimeModel) {
9236
+ return modelId.startsWith("claude-") || runtimeModel.startsWith("claude-");
9023
9237
  }
9024
- function convertTools(tools) {
9238
+ function convertTools(tools, modelId = "", runtimeModel = "") {
9025
9239
  if (!tools || tools.length === 0) return void 0;
9240
+ const foldUnions = foldsToolSchemaUnions(modelId, runtimeModel);
9026
9241
  return [{ functionDeclarations: tools.map((tool) => ({
9027
9242
  name: tool.name,
9028
9243
  description: tool.description || "",
9029
- parameters: stripMetaSchema$3(tool.parameters) || {
9244
+ parameters: stripMetaSchema$3(tool.parameters, { foldUnions }) || {
9030
9245
  type: "object",
9031
9246
  properties: {}
9032
9247
  }
@@ -9087,7 +9302,7 @@ function buildRequest$2(options, model, projectId, runtimeModel, effort, images
9087
9302
  generationConfig.maxOutputTokens = options.maxTokens !== void 0 ? Math.min(options.maxTokens, maxAllowed) : maxAllowed;
9088
9303
  request.generationConfig = generationConfig;
9089
9304
  const toolChoice = options.toolChoice;
9090
- const tools = convertTools(options.tools);
9305
+ const tools = convertTools(options.tools, model.id, runtimeModel);
9091
9306
  if (tools) {
9092
9307
  request.tools = tools;
9093
9308
  if (toolChoice) request.toolConfig = { functionCallingConfig: { mode: mapToolChoiceMode(toolChoice) } };
@@ -9979,6 +10194,103 @@ function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetch
9979
10194
  });
9980
10195
  }
9981
10196
  //#endregion
10197
+ //#region src/host/common/context-budget-fetch.ts
10198
+ /**
10199
+ * Recover a rejected input+output budget without dropping model-visible history.
10200
+ * Only explicit wire output controls and provider-confirmed counts qualify.
10201
+ * HTTP success/SSE, auth, quota, and byte-limit failures retain their old paths.
10202
+ */
10203
+ function fetchWithContextBudgetRecovery(fetchFn) {
10204
+ return async (input, init) => {
10205
+ const response = await fetchFn(input, init);
10206
+ if (response.status !== 400 && response.status !== 422) return response;
10207
+ if (typeof init?.body !== "string") return response;
10208
+ let body;
10209
+ try {
10210
+ const parsed = JSON.parse(init.body);
10211
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return response;
10212
+ body = parsed;
10213
+ } catch {
10214
+ return response;
10215
+ }
10216
+ const keys = [
10217
+ "max_tokens",
10218
+ "max_completion_tokens",
10219
+ "max_output_tokens"
10220
+ ].filter((key) => key in body);
10221
+ if (keys.length !== 1) return response;
10222
+ const key = keys[0];
10223
+ const requested = body[key];
10224
+ if (typeof requested !== "number" || !Number.isSafeInteger(requested) || requested <= 0) return response;
10225
+ const reduced = reducedOutputBudget(await boundedDiagnostic(response), requested);
10226
+ if (reduced === void 0) return response;
10227
+ const thinking = body.thinking;
10228
+ if (typeof thinking === "object" && thinking !== null) {
10229
+ const budget = thinking.budget_tokens;
10230
+ if (typeof budget === "number" && reduced <= budget) return response;
10231
+ }
10232
+ init.signal?.throwIfAborted();
10233
+ await response.body?.cancel().catch(() => void 0);
10234
+ return fetchFn(input, {
10235
+ ...init,
10236
+ body: JSON.stringify({
10237
+ ...body,
10238
+ [key]: reduced
10239
+ })
10240
+ });
10241
+ };
10242
+ }
10243
+ /** Read a bounded clone so a non-recoverable refusal remains readable by its owner. */
10244
+ async function boundedDiagnostic(response) {
10245
+ const reader = response.clone().body?.getReader();
10246
+ if (reader === void 0) return "";
10247
+ const decoder = new TextDecoder();
10248
+ let size = 0;
10249
+ let text = "";
10250
+ try {
10251
+ for (;;) {
10252
+ const { done, value } = await reader.read();
10253
+ if (done) return text + decoder.decode();
10254
+ size += value.byteLength;
10255
+ if (size > 16384) return "";
10256
+ text += decoder.decode(value, { stream: true });
10257
+ }
10258
+ } catch {
10259
+ return "";
10260
+ } finally {
10261
+ reader.cancel().catch(() => void 0);
10262
+ }
10263
+ }
10264
+ function diagnosticMessage(value, depth = 0) {
10265
+ if (depth > 4) return void 0;
10266
+ if (typeof value === "string") return value;
10267
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
10268
+ const fields = value;
10269
+ return diagnosticMessage(fields.error, depth + 1) ?? diagnosticMessage(fields.message, depth + 1);
10270
+ }
10271
+ /** Never infer capacity from a model name or from an echoed request. */
10272
+ function reducedOutputBudget(detail, requested) {
10273
+ let message;
10274
+ try {
10275
+ message = diagnosticMessage(JSON.parse(detail));
10276
+ } catch {
10277
+ message = detail;
10278
+ }
10279
+ if (message === void 0) return void 0;
10280
+ const match = /maximum context length is ([\d,]+) tokens[\s\S]*?requested ([\d,]+) tokens[\s\S]*?\(([\d,]+) in the messages, ([\d,]+) in the completion\)/i.exec(message);
10281
+ if (match === null) return void 0;
10282
+ const [window, total, prompt, completion] = match.slice(1).map((value) => Number(value.replaceAll(",", "")));
10283
+ if (![
10284
+ window,
10285
+ total,
10286
+ prompt,
10287
+ completion
10288
+ ].every((value) => Number.isSafeInteger(value) && value > 0)) return void 0;
10289
+ if (completion !== requested || prompt + completion !== total || total <= window) return void 0;
10290
+ const available = window - prompt - 1024;
10291
+ return available >= 1024 && available < requested ? available : void 0;
10292
+ }
10293
+ //#endregion
9982
10294
  //#region src/host/command-code/model-catalog.ts
9983
10295
  /** Every model the Command Code registry describes, in registry order. */
9984
10296
  const COMMAND_CODE_MODELS = [
@@ -14092,7 +14404,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
14092
14404
  "anthropic-version": "2023-06-01"
14093
14405
  } : baseHeaders;
14094
14406
  try {
14095
- response = await fetchFn(endpoint, {
14407
+ response = await fetchWithContextBudgetRecovery(fetchFn)(endpoint, {
14096
14408
  method: "POST",
14097
14409
  headers,
14098
14410
  body,
@@ -19923,7 +20235,20 @@ function estimatedInputTokens(options) {
19923
20235
  characters += JSON.stringify(tool.parameters).length;
19924
20236
  } catch {}
19925
20237
  }
19926
- for (const message of options.messages) characters += textOf$3(message.content).length;
20238
+ const contentCharacters = (content) => {
20239
+ if (!Array.isArray(content)) return typeof content === "string" ? content.length : 0;
20240
+ return content.reduce((total, block) => {
20241
+ if (!isRecord$16(block)) return total;
20242
+ if ((block.type === "text" || block.type === "reasoning") && typeof block.text === "string") return total + block.text.length;
20243
+ if (block.type === "tool-result") return total + contentCharacters(block.content);
20244
+ if (block.type === "tool-call") {
20245
+ const args = toolCallArguments$3(block.arguments);
20246
+ return total + String(block.name ?? "").length + args.length;
20247
+ }
20248
+ return total;
20249
+ }, 0);
20250
+ };
20251
+ for (const message of options.messages) characters += contentCharacters(message.content);
19927
20252
  if (characters === 0) return void 0;
19928
20253
  return Math.ceil(characters / 4);
19929
20254
  }
@@ -21082,7 +21407,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
21082
21407
  const endpoint = wire === "anthropic" ? `${base}/v1/messages?beta=true` : `${base}/v1/chat/completions`;
21083
21408
  const headers = await modelRequestHeaders$1(apiCredentials.accessToken, wire);
21084
21409
  try {
21085
- response = await fetchFn(endpoint, {
21410
+ response = await fetchWithContextBudgetRecovery(fetchFn)(endpoint, {
21086
21411
  method: "POST",
21087
21412
  headers,
21088
21413
  body,
@@ -25799,7 +26124,7 @@ var MinimaxCodeAdapter = class extends LlmAdapter {
25799
26124
  let response;
25800
26125
  for (let attempt = 0;; attempt += 1) {
25801
26126
  try {
25802
- response = await fetchFn(messagesUrl(credentials.region), {
26127
+ response = await fetchWithContextBudgetRecovery(fetchFn)(messagesUrl(credentials.region), {
25803
26128
  method: "POST",
25804
26129
  headers: modelRequestHeaders(credentials.accessToken),
25805
26130
  body,
@@ -30539,7 +30864,7 @@ var WorkBuddyAdapter = class extends LlmAdapter {
30539
30864
  credentials = effective.credentials;
30540
30865
  }
30541
30866
  try {
30542
- response = await fetchFn(`${credentials.backend}${CHAT_PATH}`, {
30867
+ response = await fetchWithContextBudgetRecovery(fetchFn)(`${credentials.backend}${CHAT_PATH}`, {
30543
30868
  method: "POST",
30544
30869
  headers: workBuddyHeaders(credentials, { accept: "text/event-stream" }),
30545
30870
  body,
@@ -34202,14 +34527,94 @@ function offloadOldestRequestImages(options) {
34202
34527
  };
34203
34528
  }
34204
34529
  /**
34530
+ * Longest side, in pixels, the wire accepts for one image in any request.
34531
+ * TRANSCRIBED from the API's vision limits. The harness normalizes stored
34532
+ * images to an 8192 px long edge, so a stored image can still exceed this.
34533
+ */
34534
+ const MAX_IMAGE_EDGE = 8e3;
34535
+ /**
34536
+ * Longest side, in pixels, the wire accepts for every image once one request
34537
+ * carries more than {@link MANY_IMAGE_THRESHOLD} images. TRANSCRIBED: past that
34538
+ * count the request is rejected with "At least one of the image dimensions
34539
+ * exceed max allowed size for many-image requests: 2000 pixels".
34540
+ *
34541
+ * Rejection is a 400 for the whole request. History keeps every image, so a
34542
+ * long session can only add images. Once it holds more than twenty and one is
34543
+ * wider than 2000 px, which is common for side-by-side screenshots, every later
34544
+ * turn fails. The session cannot recover by itself.
34545
+ */
34546
+ const MANY_IMAGE_EDGE = 2e3;
34547
+ /**
34548
+ * Encoded-byte target for one downscaled request version.
34549
+ *
34550
+ * CHOICE: 3.5 MiB of raw bytes is about 4.9 MB of base64. That stays under the
34551
+ * 5 MB per-image ceiling whichever form the server measures. When no quality
34552
+ * level meets the target, the harness keeps its smallest output.
34553
+ */
34554
+ const REQUEST_IMAGE_VERSION_MAX_BYTES = 3.5 * 1024 * 1024;
34555
+ function countRequestImages(content) {
34556
+ if (!Array.isArray(content)) return 0;
34557
+ let count = 0;
34558
+ for (const block of content) {
34559
+ if (!isRecord$5(block)) continue;
34560
+ if (block.type === "image") count += 1;
34561
+ else if (block.type === "tool-result") count += countRequestImages(block.content);
34562
+ }
34563
+ return count;
34564
+ }
34565
+ /**
34566
+ * Long-edge ceiling every image of this request must meet.
34567
+ *
34568
+ * The API counts image blocks, so a repeated attachment counts every time it
34569
+ * appears. The count is taken before images are resolved: a block that later
34570
+ * degrades to placeholder text still counts. Overcounting can only lower the
34571
+ * ceiling, and a lower ceiling never causes a rejection.
34572
+ */
34573
+ function requestImageEdgeLimit(options) {
34574
+ let count = 0;
34575
+ for (const message of options.messages) count += countRequestImages(message.content);
34576
+ return count > 20 ? MANY_IMAGE_EDGE : MAX_IMAGE_EDGE;
34577
+ }
34578
+ /**
34579
+ * Request-version target for one stored image whose long edge is over the
34580
+ * ceiling, or undefined when it fits. The short edge rounds to the nearest
34581
+ * pixel, matching the harness's long-edge resize. A reference without usable
34582
+ * dimensions is sent as stored.
34583
+ */
34584
+ function requestImageEdgeTarget(ref, edge) {
34585
+ const { width, height } = ref;
34586
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return void 0;
34587
+ if (Math.max(width, height) <= edge) return void 0;
34588
+ const maxPixels = Math.max(1, Math.floor(edge * edge * (Math.min(width, height) / Math.max(width, height))));
34589
+ return width >= height ? {
34590
+ width: edge,
34591
+ height: Math.max(1, Math.round(edge * height / width)),
34592
+ maxPixels,
34593
+ maxBytes: REQUEST_IMAGE_VERSION_MAX_BYTES
34594
+ } : {
34595
+ width: Math.max(1, Math.round(edge * width / height)),
34596
+ height: edge,
34597
+ maxPixels,
34598
+ maxBytes: REQUEST_IMAGE_VERSION_MAX_BYTES
34599
+ };
34600
+ }
34601
+ /**
34205
34602
  * Read every durable { type: 'image', attachment } block one request carries.
34206
34603
  * An unreadable image resolves to 'unavailable' rather than disappearing, so
34207
34604
  * the model is told the picture is missing instead of answering about a blank.
34605
+ *
34606
+ * An image whose long edge is over this request's ceiling is sent as the
34607
+ * harness's downscaled request version (see {@link requestImageEdgeLimit}). The
34608
+ * stored image and durable history do not change, and an image that already
34609
+ * fits is sent byte-for-byte as stored. If a downscaled version cannot be
34610
+ * produced, the image resolves to 'unavailable'. A visible placeholder is better
34611
+ * than sending the original, which would make the whole request fail.
34208
34612
  */
34209
34613
  async function resolveRequestImages(options, attachments, signal) {
34210
34614
  const refs = /* @__PURE__ */ new Map();
34211
34615
  for (const message of options.messages) collectImageRefs(message.content, refs);
34212
34616
  if (refs.size === 0) return NO_RESOLVED_IMAGES;
34617
+ const edge = requestImageEdgeLimit(options);
34213
34618
  const resolved = /* @__PURE__ */ new Map();
34214
34619
  await Promise.all([...refs].map(async ([attachmentId, ref]) => {
34215
34620
  if (!attachments) {
@@ -34217,6 +34622,16 @@ async function resolveRequestImages(options, attachments, signal) {
34217
34622
  return;
34218
34623
  }
34219
34624
  try {
34625
+ const target = requestImageEdgeTarget(ref, edge);
34626
+ if (target !== void 0) {
34627
+ const version = await attachments.readImageRequest(ref, target, signal);
34628
+ resolved.set(attachmentId, Math.max(version.width, version.height) > edge ? { kind: "unavailable" } : {
34629
+ kind: "inline",
34630
+ mediaType: version.mediaType,
34631
+ data: Buffer.from(version.data).toString("base64")
34632
+ });
34633
+ return;
34634
+ }
34220
34635
  const stored = await attachments.readImage(ref, signal);
34221
34636
  resolved.set(attachmentId, {
34222
34637
  kind: "inline",
@@ -36687,7 +37102,7 @@ var ClaudeAdapter = class ClaudeAdapter extends LlmAdapter {
36687
37102
  cacheTtl
36688
37103
  });
36689
37104
  try {
36690
- return await fetchFn("https://api.anthropic.com/v1/messages?beta=true", {
37105
+ return await fetchWithContextBudgetRecovery(fetchFn)("https://api.anthropic.com/v1/messages?beta=true", {
36691
37106
  method: "POST",
36692
37107
  headers: buildClaudeHeaders(credentials.accessToken, {
36693
37108
  model: requestOptions.model,
@@ -42055,6 +42470,13 @@ function releaseListener(handle) {
42055
42470
  //#endregion
42056
42471
  //#region src/index.ts
42057
42472
  const Config = z.object({
42473
+ fetchProvider: z.union([
42474
+ z.const("auto"),
42475
+ z.const("plugin"),
42476
+ z.const("dsh")
42477
+ ]).default("auto"),
42478
+ fetchMaxBodyChars: z.number().min(1).max(Number.MAX_SAFE_INTEGER).step(1).default(DEFAULT_FETCH_MAX_BODY_CHARS),
42479
+ fetchMaxResponseBytes: z.number().min(1).max(Number.MAX_SAFE_INTEGER).step(1).default(DEFAULT_FETCH_MAX_RESPONSE_BYTES),
42058
42480
  syncAgentPresets: z.boolean().default(true),
42059
42481
  subagentModelAuthorization: z.boolean().default(true),
42060
42482
  subagentModelTools: z.array(z.string()).default([]),
@@ -42071,6 +42493,7 @@ const inject = [
42071
42493
  "loader"
42072
42494
  ];
42073
42495
  function apply(ctx, pluginConfig = {}) {
42496
+ const fetchConfiguration = resolveFetchConfiguration(pluginConfig);
42074
42497
  if (pluginConfig.syncAgentPresets !== false) ctx.effect(() => installBundledPresets(ctx, installDispatchPreset(ctx)), "dsh-chatgpt-subscription: agent preset sync");
42075
42498
  const store = createPlatformTokenStore();
42076
42499
  const preferences = registerPreferenceStore(ctx.settings);
@@ -42407,9 +42830,22 @@ function apply(ctx, pluginConfig = {}) {
42407
42830
  signal: options?.signal
42408
42831
  })).catch(() => []));
42409
42832
  const searchSwitcher = new SearchProviderSwitcher(ctx.loader);
42833
+ let lastFetchDiagnostic;
42410
42834
  const applyWebProviders = (current = preferences.status()) => {
42411
42835
  const pluginFetch = proxyManager.resolveActiveProxyUrl() !== null;
42412
- searchSwitcher.select(current.searchProvider, { pluginFetch }).catch((error) => {
42836
+ searchSwitcher.select(current.searchProvider, {
42837
+ pluginFetch,
42838
+ fetchProvider: fetchConfiguration.fetchProvider
42839
+ }).then(() => {
42840
+ const status = searchSwitcher.status();
42841
+ if (status.state === "missing" || status.state === "failed") return;
42842
+ const reason = fetchConfiguration.fetchProvider !== "auto" ? "explicit configuration" : current.searchProvider === "codex" ? "Codex search selected" : pluginFetch ? "active proxy detected" : "no proxy or Codex search";
42843
+ const diagnostic = `${status.configuredFetchProvider ?? "DSH default"} (mode=${fetchConfiguration.fetchProvider}; ${reason}; plugin limits=${fetchConfiguration.fetchMaxBodyChars} chars/${fetchConfiguration.fetchMaxResponseBytes} bytes)`;
42844
+ if (diagnostic !== lastFetchDiagnostic) {
42845
+ lastFetchDiagnostic = diagnostic;
42846
+ ctx.logger.info(`[dsh-chatgpt-subscription] Web fetch provider configured: ${diagnostic}`);
42847
+ }
42848
+ }).catch((error) => {
42413
42849
  ctx.logger.warn(`[dsh-chatgpt-subscription] Web provider selection could not be applied: ${error instanceof Error ? error.message : String(error)}`);
42414
42850
  });
42415
42851
  };
@@ -42435,13 +42871,17 @@ function apply(ctx, pluginConfig = {}) {
42435
42871
  }))
42436
42872
  });
42437
42873
  };
42438
- const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, readRouteAudit, codexAccountPool);
42874
+ const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences, proxyManager, searchSwitcher, readRouteAudit, codexAccountPool, fetchConfiguration);
42439
42875
  const disposeAdapter = ctx.llm.registerAdapter([PROVIDER_ID$7], adapter);
42440
42876
  const disposeImageTool = ctx.tools.register(createCodexImageTool(oauth, ctx.attachments, { fetchFn: proxyFetch }));
42441
42877
  const disposeVideoTool = ctx.tools.register(createKimiVideoTool(ctx, { fetchFn: proxyFetch }));
42442
42878
  ctx.inject(["web"], (ctx) => {
42443
42879
  ctx.web.registerSearchProvider(createCodexSearchProvider(oauth, { fetchFn: proxyFetch }));
42444
- ctx.web.registerFetchProvider(createCodexFetchProvider({ fetchFn: proxyFetch }));
42880
+ ctx.web.registerFetchProvider(createCodexFetchProvider({
42881
+ fetchFn: proxyFetch,
42882
+ maxBodyChars: fetchConfiguration.fetchMaxBodyChars,
42883
+ maxResponseBytes: fetchConfiguration.fetchMaxResponseBytes
42884
+ }));
42445
42885
  setTimeout(applyWebProviders, 0);
42446
42886
  });
42447
42887
  const disposePreferenceWatch = preferences.watch((next) => applyWebProviders(next));