@askalf/dario 4.8.78 → 4.8.80

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "_version": "2.1.177",
2
+ "_version": "2.1.178",
3
3
  "_captured": "2026-06-11T20:43:54.573Z",
4
4
  "_source": "bundled",
5
5
  "_schemaVersion": 3,
@@ -1294,7 +1294,7 @@
1294
1294
  "anthropic_beta": "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advisor-tool-2026-03-01,effort-2025-11-24",
1295
1295
  "header_values": {
1296
1296
  "accept": "application/json",
1297
- "user-agent": "claude-cli/2.1.177 (external, sdk-cli)",
1297
+ "user-agent": "claude-cli/2.1.178 (external, sdk-cli)",
1298
1298
  "x-stainless-arch": "x64",
1299
1299
  "x-stainless-lang": "js",
1300
1300
  "x-stainless-os": "Linux",
@@ -1319,5 +1319,5 @@
1319
1319
  "output_config",
1320
1320
  "stream"
1321
1321
  ],
1322
- "_supportedMaxTested": "2.1.177"
1322
+ "_supportedMaxTested": "2.1.178"
1323
1323
  }
package/dist/proxy.d.ts CHANGED
@@ -98,6 +98,21 @@ export declare function parseEffortRejection(body: string): {
98
98
  rejected: string;
99
99
  supported: string[];
100
100
  } | null;
101
+ /**
102
+ * Detect upstream's HARD effort rejection — a model with NO output_config.effort
103
+ * support at all (it predates the parameter), distinct from the SOFT level
104
+ * rejection parsed by parseEffortRejection:
105
+ *
106
+ * 400 {"type":"invalid_request_error","message":"This model does not
107
+ * support the effort parameter."}
108
+ *
109
+ * Observed live 2026-06-16 on claude-opus-4-1-20250805 and
110
+ * claude-sonnet-4-5-20250929 — the autodetected catalog exposes pre-effort
111
+ * models, and the box's DARIO_EFFORT=max pin hard-400s them. There is no
112
+ * supported tier to clamp to here, so effort must be STRIPPED — modelled as
113
+ * an EMPTY supported set in effortSupportByModel. Exported for tests.
114
+ */
115
+ export declare function isEffortParamUnsupported(body: string): boolean;
101
116
  /**
102
117
  * Pick the strongest effort level a model says it supports. Preference is
103
118
  * descending capability — the caller asked for more than the model can do,
@@ -105,6 +120,20 @@ export declare function parseEffortRejection(body: string): {
105
120
  */
106
121
  export declare const EFFORT_PREFERENCE: readonly string[];
107
122
  export declare function bestSupportedEffort(supported: readonly string[]): string;
123
+ /**
124
+ * Parse upstream's max_tokens-cap rejection:
125
+ *
126
+ * 400 {"type":"invalid_request_error","message":"max_tokens: 64000 > 32000,
127
+ * which is the maximum allowed number of output tokens for
128
+ * claude-opus-4-1-20250805."}
129
+ *
130
+ * Observed live 2026-06-16 — dario pins DEFAULT_MAX_TOKENS (64000, CC 2.1.143's
131
+ * wire default), but the autodetected catalog exposes older models with a lower
132
+ * per-model output cap (e.g. opus-4-1: 32000). Returns the model's cap (the
133
+ * clamp target) or null for any other 400. Same learn-once-and-cache shape as
134
+ * parseEffortRejection. Exported for tests.
135
+ */
136
+ export declare function parseMaxTokensRejection(body: string): number | null;
108
137
  /**
109
138
  * Resolve an inbound API path to its upstream target + forwarding mode.
110
139
  * Allowlist semantics — anything unlisted is 403'd (prevents SSRF through
package/dist/proxy.js CHANGED
@@ -324,6 +324,23 @@ export function parseEffortRejection(body) {
324
324
  const supported = m[2].split(',').map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0);
325
325
  return supported.length > 0 ? { rejected: m[1], supported } : null;
326
326
  }
327
+ /**
328
+ * Detect upstream's HARD effort rejection — a model with NO output_config.effort
329
+ * support at all (it predates the parameter), distinct from the SOFT level
330
+ * rejection parsed by parseEffortRejection:
331
+ *
332
+ * 400 {"type":"invalid_request_error","message":"This model does not
333
+ * support the effort parameter."}
334
+ *
335
+ * Observed live 2026-06-16 on claude-opus-4-1-20250805 and
336
+ * claude-sonnet-4-5-20250929 — the autodetected catalog exposes pre-effort
337
+ * models, and the box's DARIO_EFFORT=max pin hard-400s them. There is no
338
+ * supported tier to clamp to here, so effort must be STRIPPED — modelled as
339
+ * an EMPTY supported set in effortSupportByModel. Exported for tests.
340
+ */
341
+ export function isEffortParamUnsupported(body) {
342
+ return /does not support the effort parameter/i.test(body);
343
+ }
327
344
  /**
328
345
  * Pick the strongest effort level a model says it supports. Preference is
329
346
  * descending capability — the caller asked for more than the model can do,
@@ -336,6 +353,26 @@ export function bestSupportedEffort(supported) {
336
353
  return e;
337
354
  return supported[0] ?? 'high';
338
355
  }
356
+ /**
357
+ * Parse upstream's max_tokens-cap rejection:
358
+ *
359
+ * 400 {"type":"invalid_request_error","message":"max_tokens: 64000 > 32000,
360
+ * which is the maximum allowed number of output tokens for
361
+ * claude-opus-4-1-20250805."}
362
+ *
363
+ * Observed live 2026-06-16 — dario pins DEFAULT_MAX_TOKENS (64000, CC 2.1.143's
364
+ * wire default), but the autodetected catalog exposes older models with a lower
365
+ * per-model output cap (e.g. opus-4-1: 32000). Returns the model's cap (the
366
+ * clamp target) or null for any other 400. Same learn-once-and-cache shape as
367
+ * parseEffortRejection. Exported for tests.
368
+ */
369
+ export function parseMaxTokensRejection(body) {
370
+ const m = body.match(/max_tokens:\s*\d+\s*>\s*(\d+),?\s*which is the maximum allowed/i);
371
+ if (!m)
372
+ return null;
373
+ const cap = Number(m[1]);
374
+ return Number.isFinite(cap) && cap > 0 ? cap : null;
375
+ }
339
376
  /**
340
377
  * Resolve an inbound API path to its upstream target + forwarding mode.
341
378
  * Allowlist semantics — anything unlisted is 403'd (prevents SSRF through
@@ -977,6 +1014,12 @@ export async function startProxy(opts = {}) {
977
1014
  // "does not support effort level" 400 (see parseEffortRejection); consulted
978
1015
  // up front at body-build time so capped models never re-pay the rejection.
979
1016
  const effortSupportByModel = new Map();
1017
+ // Per-model max_tokens cap cache — same pay-the-round-trip-once shape as
1018
+ // effortSupportByModel. Populated from upstream's "max_tokens: X > Y, which
1019
+ // is the maximum allowed" 400 (see parseMaxTokensRejection); consulted up
1020
+ // front so older models capped below dario's DEFAULT_MAX_TOKENS pin never
1021
+ // re-pay the rejection.
1022
+ const maxTokensCapByModel = new Map();
980
1023
  // Beta flag set — sourced from the live template when the capture recorded
981
1024
  // one (schema v2+), else falls back to the v2.1.104 bundled default. Same
982
1025
  // fallback string shim/runtime.cjs uses (kept in sync so proxy and shim
@@ -1763,7 +1806,25 @@ export async function startProxy(opts = {}) {
1763
1806
  const supportedEfforts = effortSupportByModel.get(r.model);
1764
1807
  const oc = r.output_config;
1765
1808
  if (supportedEfforts && oc && typeof oc.effort === 'string' && !supportedEfforts.includes(oc.effort)) {
1766
- oc.effort = bestSupportedEffort(supportedEfforts);
1809
+ if (supportedEfforts.length === 0) {
1810
+ // HARD rejection learned — this model has no effort support at
1811
+ // all; strip the field (no tier to clamp to). See the retry
1812
+ // branch below + isEffortParamUnsupported.
1813
+ delete oc.effort;
1814
+ if (Object.keys(oc).length === 0)
1815
+ delete r.output_config;
1816
+ }
1817
+ else {
1818
+ oc.effort = bestSupportedEffort(supportedEfforts);
1819
+ }
1820
+ }
1821
+ // max_tokens cap clamp — when a prior request taught us this model's
1822
+ // per-model output cap (older models cap below dario's
1823
+ // DEFAULT_MAX_TOKENS pin), clamp up front instead of re-paying the
1824
+ // 400. In-place value mutation: field order (a fingerprint) is kept.
1825
+ const maxTokensCap = maxTokensCapByModel.get(r.model);
1826
+ if (maxTokensCap !== undefined && typeof r.max_tokens === 'number' && r.max_tokens > maxTokensCap) {
1827
+ r.max_tokens = maxTokensCap;
1767
1828
  }
1768
1829
  }
1769
1830
  // dario#528: overwrite the random cch placeholder with the real
@@ -2117,6 +2178,122 @@ export async function startProxy(opts = {}) {
2117
2178
  return;
2118
2179
  }
2119
2180
  }
2181
+ else if (upstream.status === 400 && isEffortParamUnsupported(peekedBody) && finalBody) {
2182
+ // HARD effort rejection — this model has no output_config.effort
2183
+ // support at all (it predates the parameter; e.g. opus-4-1 /
2184
+ // sonnet-4-5 surfaced by the autodetected catalog under the box's
2185
+ // DARIO_EFFORT=max pin). Unlike the SOFT level rejection above there
2186
+ // is no supported tier to clamp to, so STRIP the field, retry once,
2187
+ // and cache an EMPTY supported set per model so the up-front consult
2188
+ // strips it on every later request without re-paying the round-trip.
2189
+ let retried = false;
2190
+ try {
2191
+ const rb = JSON.parse(finalBody.toString('utf8'));
2192
+ const wireModel = typeof rb.model === 'string' ? rb.model : '';
2193
+ const oc = rb.output_config;
2194
+ if (wireModel && oc && typeof oc.effort === 'string') {
2195
+ const firstRejection = !effortSupportByModel.has(wireModel);
2196
+ effortSupportByModel.set(wireModel, []); // empty set = no effort support → strip
2197
+ if (verbose && firstRejection)
2198
+ console.log(`[dario] #${requestCount} effort parameter unsupported by ${wireModel} — retrying without output_config.effort (stripped for this model going forward)`);
2199
+ delete oc.effort;
2200
+ if (Object.keys(oc).length === 0)
2201
+ delete rb.output_config;
2202
+ finalBody = Buffer.from(JSON.stringify(rb));
2203
+ const retry = await fetch(targetBase, {
2204
+ method: req.method ?? 'POST',
2205
+ headers: passthrough ? headers : orderHeadersForOutbound(headers),
2206
+ body: new Uint8Array(finalBody),
2207
+ signal: upstreamAbort.signal,
2208
+ });
2209
+ upstream = retry;
2210
+ peekedBody = null;
2211
+ retried = true;
2212
+ if (pool && poolAccount) {
2213
+ const retrySnapshot = parseRateLimits(upstream.headers);
2214
+ if (upstream.status === 429) {
2215
+ pool.markRejected(poolAccount.alias, retrySnapshot);
2216
+ }
2217
+ else {
2218
+ pool.updateRateLimits(poolAccount.alias, retrySnapshot);
2219
+ }
2220
+ }
2221
+ }
2222
+ }
2223
+ catch { /* body not JSON — forward the original 400 below */ }
2224
+ if (!retried) {
2225
+ // Couldn't rebuild the body — forward the upstream 400 (already consumed).
2226
+ const responseHeaders = {
2227
+ 'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
2228
+ 'Access-Control-Allow-Origin': corsOrigin,
2229
+ ...SECURITY_HEADERS,
2230
+ };
2231
+ for (const [key, value] of upstream.headers.entries()) {
2232
+ if (key === 'request-id')
2233
+ responseHeaders[key] = value;
2234
+ }
2235
+ requestCount++;
2236
+ res.writeHead(400, responseHeaders);
2237
+ res.end(peekedBody);
2238
+ return;
2239
+ }
2240
+ }
2241
+ else if (upstream.status === 400 && parseMaxTokensRejection(peekedBody) !== null && finalBody) {
2242
+ // max_tokens-cap rejection — dario's DEFAULT_MAX_TOKENS pin exceeds
2243
+ // this (older) model's per-model output cap (e.g. opus-4-1 caps at
2244
+ // 32000 where newer models allow 64000). Clamp max_tokens to the cap
2245
+ // the error reports, retry once, and cache per model so the up-front
2246
+ // consult clamps every later request without the round-trip.
2247
+ const cap = parseMaxTokensRejection(peekedBody);
2248
+ let retried = false;
2249
+ try {
2250
+ const rb = JSON.parse(finalBody.toString('utf8'));
2251
+ const wireModel = typeof rb.model === 'string' ? rb.model : '';
2252
+ if (wireModel && typeof rb.max_tokens === 'number' && rb.max_tokens > cap) {
2253
+ const firstRejection = !maxTokensCapByModel.has(wireModel);
2254
+ maxTokensCapByModel.set(wireModel, cap);
2255
+ if (verbose && firstRejection)
2256
+ console.log(`[dario] #${requestCount} max_tokens ${rb.max_tokens} exceeds ${wireModel} cap ${cap} — retrying clamped (cached per model)`);
2257
+ rb.max_tokens = cap; // in-place value mutation — field order untouched
2258
+ finalBody = Buffer.from(JSON.stringify(rb));
2259
+ const retry = await fetch(targetBase, {
2260
+ method: req.method ?? 'POST',
2261
+ headers: passthrough ? headers : orderHeadersForOutbound(headers),
2262
+ body: new Uint8Array(finalBody),
2263
+ signal: upstreamAbort.signal,
2264
+ });
2265
+ upstream = retry;
2266
+ peekedBody = null;
2267
+ retried = true;
2268
+ if (pool && poolAccount) {
2269
+ const retrySnapshot = parseRateLimits(upstream.headers);
2270
+ if (upstream.status === 429) {
2271
+ pool.markRejected(poolAccount.alias, retrySnapshot);
2272
+ }
2273
+ else {
2274
+ pool.updateRateLimits(poolAccount.alias, retrySnapshot);
2275
+ }
2276
+ }
2277
+ }
2278
+ }
2279
+ catch { /* body not JSON — forward the original 400 below */ }
2280
+ if (!retried) {
2281
+ // Couldn't rebuild the body — forward the upstream 400 (already consumed).
2282
+ const responseHeaders = {
2283
+ 'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
2284
+ 'Access-Control-Allow-Origin': corsOrigin,
2285
+ ...SECURITY_HEADERS,
2286
+ };
2287
+ for (const [key, value] of upstream.headers.entries()) {
2288
+ if (key === 'request-id')
2289
+ responseHeaders[key] = value;
2290
+ }
2291
+ requestCount++;
2292
+ res.writeHead(400, responseHeaders);
2293
+ res.end(peekedBody);
2294
+ return;
2295
+ }
2296
+ }
2120
2297
  else if (isLongContextError) {
2121
2298
  // Cache the rejection so future requests on this account skip
2122
2299
  // context-1m up front instead of re-paying the 400/429 round-trip.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.8.78",
3
+ "version": "4.8.80",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {