@askalf/dario 4.8.79 → 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.
- package/dist/proxy.d.ts +29 -0
- package/dist/proxy.js +178 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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": {
|