@askalf/dario 4.8.49 → 4.8.51
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/cc-template.js +15 -0
- package/dist/proxy.d.ts +17 -0
- package/dist/proxy.js +43 -13
- package/package.json +1 -1
package/dist/cc-template.js
CHANGED
|
@@ -1426,6 +1426,21 @@ export function buildCCRequest(clientBody, billingTag, cacheControl, identity, o
|
|
|
1426
1426
|
// wire footprint).
|
|
1427
1427
|
ccRequest.tools = CC_TOOL_DEFINITIONS;
|
|
1428
1428
|
}
|
|
1429
|
+
else if (model.toLowerCase().includes('fable')) {
|
|
1430
|
+
// Fable refuses tool-less CC-shaped MULTI-TURN requests (live replay
|
|
1431
|
+
// bisect 2026-06-09): scrubbed system + zero tools + an assistant turn
|
|
1432
|
+
// in history → 200 + stop_reason "refusal" with empty content on every
|
|
1433
|
+
// request, while the byte-identical body WITH CC's tool array answers.
|
|
1434
|
+
// Real CC always sends its tool array, so zero-tools is itself a
|
|
1435
|
+
// fingerprint divergence (see the merge-tools note above) — fable's
|
|
1436
|
+
// refusal layer is just the first model to punish it. Emit the CC base
|
|
1437
|
+
// array pinned with `tool_choice: none` so the model cannot call tools
|
|
1438
|
+
// the client never declared (without the pin it DOES — verified
|
|
1439
|
+
// spurious WebFetch on a weather prompt). Other families keep the
|
|
1440
|
+
// legacy tool-less shape, which they demonstrably accept.
|
|
1441
|
+
ccRequest.tools = CC_TOOL_DEFINITIONS;
|
|
1442
|
+
ccRequest.tool_choice = { type: 'none' };
|
|
1443
|
+
}
|
|
1429
1444
|
// Metadata
|
|
1430
1445
|
ccRequest.metadata = {
|
|
1431
1446
|
user_id: JSON.stringify({
|
package/dist/proxy.d.ts
CHANGED
|
@@ -37,6 +37,23 @@ export declare function betaForModel(base: string, model: string | null | undefi
|
|
|
37
37
|
* very end of the id. Exported for tests.
|
|
38
38
|
*/
|
|
39
39
|
export declare function stripContext1mTag(model: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve an inbound API path to its upstream target + forwarding mode.
|
|
42
|
+
* Allowlist semantics — anything unlisted is 403'd (prevents SSRF through
|
|
43
|
+
* the OAuth-bearing proxy).
|
|
44
|
+
*
|
|
45
|
+
* `thin: true` marks endpoints forwarded WITHOUT template injection —
|
|
46
|
+
* OAuth swap + model-id normalization only. `/v1/messages/count_tokens`
|
|
47
|
+
* is thin because the endpoint counts the CLIENT's own prompt: bolting on
|
|
48
|
+
* CC's system/tools/effort would distort the count (and `output_config`
|
|
49
|
+
* is not a count_tokens request field). `?beta=true` stays a /v1/messages
|
|
50
|
+
* affordance (billing classification) — not appended for count_tokens.
|
|
51
|
+
* Exported for tests.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveProxyTarget(urlPath: string, isOpenAI: boolean): {
|
|
54
|
+
target: string;
|
|
55
|
+
thin: boolean;
|
|
56
|
+
} | null;
|
|
40
57
|
export declare const ORCHESTRATION_TAG_NAMES: string[];
|
|
41
58
|
/**
|
|
42
59
|
* Build the regex list that actually strips orchestration tags.
|
package/dist/proxy.js
CHANGED
|
@@ -220,6 +220,29 @@ export function stripContext1mTag(model) {
|
|
|
220
220
|
return model;
|
|
221
221
|
return model.replace(/\[1m\]$/i, '');
|
|
222
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Resolve an inbound API path to its upstream target + forwarding mode.
|
|
225
|
+
* Allowlist semantics — anything unlisted is 403'd (prevents SSRF through
|
|
226
|
+
* the OAuth-bearing proxy).
|
|
227
|
+
*
|
|
228
|
+
* `thin: true` marks endpoints forwarded WITHOUT template injection —
|
|
229
|
+
* OAuth swap + model-id normalization only. `/v1/messages/count_tokens`
|
|
230
|
+
* is thin because the endpoint counts the CLIENT's own prompt: bolting on
|
|
231
|
+
* CC's system/tools/effort would distort the count (and `output_config`
|
|
232
|
+
* is not a count_tokens request field). `?beta=true` stays a /v1/messages
|
|
233
|
+
* affordance (billing classification) — not appended for count_tokens.
|
|
234
|
+
* Exported for tests.
|
|
235
|
+
*/
|
|
236
|
+
export function resolveProxyTarget(urlPath, isOpenAI) {
|
|
237
|
+
if (isOpenAI)
|
|
238
|
+
return { target: `${ANTHROPIC_API}/v1/messages?beta=true`, thin: false };
|
|
239
|
+
const allowed = {
|
|
240
|
+
'/v1/messages': { target: `${ANTHROPIC_API}/v1/messages?beta=true`, thin: false },
|
|
241
|
+
'/v1/messages/count_tokens': { target: `${ANTHROPIC_API}/v1/messages/count_tokens`, thin: true },
|
|
242
|
+
'/v1/complete': { target: `${ANTHROPIC_API}/v1/complete`, thin: false },
|
|
243
|
+
};
|
|
244
|
+
return allowed[urlPath] ?? null;
|
|
245
|
+
}
|
|
223
246
|
// Orchestration tags injected by agents (Aider, Cursor, OpenCode, etc.)
|
|
224
247
|
// that confuse Claude when passed through. Strip before forwarding.
|
|
225
248
|
export const ORCHESTRATION_TAG_NAMES = [
|
|
@@ -940,7 +963,7 @@ export async function startProxy(opts = {}) {
|
|
|
940
963
|
const JSON_HEADERS = { 'Content-Type': 'application/json', ...SECURITY_HEADERS };
|
|
941
964
|
const MODELS_JSON = JSON.stringify(OPENAI_MODELS_LIST);
|
|
942
965
|
const ERR_UNAUTH = JSON.stringify({ error: 'Unauthorized', message: 'Invalid or missing API key' });
|
|
943
|
-
const ERR_FORBIDDEN = JSON.stringify({ error: 'Forbidden', message: 'Path not allowed. Supported paths: POST /v1/messages, POST /v1/chat/completions, GET /v1/models' });
|
|
966
|
+
const ERR_FORBIDDEN = JSON.stringify({ error: 'Forbidden', message: 'Path not allowed. Supported paths: POST /v1/messages, POST /v1/messages/count_tokens, POST /v1/chat/completions, GET /v1/models' });
|
|
944
967
|
const ERR_METHOD = JSON.stringify({ error: 'Method not allowed' });
|
|
945
968
|
function checkAuth(req) {
|
|
946
969
|
return authenticateRequest(req.headers, apiKeyBuf);
|
|
@@ -1170,18 +1193,16 @@ export async function startProxy(opts = {}) {
|
|
|
1170
1193
|
}
|
|
1171
1194
|
// Detect OpenAI-format requests
|
|
1172
1195
|
const isOpenAI = urlPath === '/v1/chat/completions';
|
|
1173
|
-
// Allowlisted API paths — only these are proxied (prevents SSRF)
|
|
1174
|
-
//
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
'/v1/complete': `${ANTHROPIC_API}/v1/complete`,
|
|
1178
|
-
};
|
|
1179
|
-
const targetBase = isOpenAI ? `${ANTHROPIC_API}/v1/messages?beta=true` : allowedPaths[urlPath];
|
|
1180
|
-
if (!targetBase) {
|
|
1196
|
+
// Allowlisted API paths — only these are proxied (prevents SSRF).
|
|
1197
|
+
// count_tokens forwards thin (no template injection) — see resolveProxyTarget.
|
|
1198
|
+
const route = resolveProxyTarget(urlPath, isOpenAI);
|
|
1199
|
+
if (!route) {
|
|
1181
1200
|
res.writeHead(403, JSON_HEADERS);
|
|
1182
1201
|
res.end(ERR_FORBIDDEN);
|
|
1183
1202
|
return;
|
|
1184
1203
|
}
|
|
1204
|
+
const targetBase = route.target;
|
|
1205
|
+
const isCountTokens = route.thin;
|
|
1185
1206
|
if (req.method !== 'POST') {
|
|
1186
1207
|
res.writeHead(405, JSON_HEADERS);
|
|
1187
1208
|
res.end(ERR_METHOD);
|
|
@@ -1436,8 +1457,10 @@ export async function startProxy(opts = {}) {
|
|
|
1436
1457
|
const result = isOpenAI ? openaiToAnthropic(parsed, modelOverride) : (modelOverride ? { ...parsed, model: modelOverride } : parsed);
|
|
1437
1458
|
const r = result;
|
|
1438
1459
|
requestModel = (r.model || '').toLowerCase();
|
|
1439
|
-
// In passthrough mode, skip all Claude-specific injection — OAuth swap only
|
|
1440
|
-
|
|
1460
|
+
// In passthrough mode, skip all Claude-specific injection — OAuth swap only.
|
|
1461
|
+
// count_tokens also forwards thin (see resolveProxyTarget) — the endpoint
|
|
1462
|
+
// counts the CLIENT's own prompt, so template injection would distort it.
|
|
1463
|
+
if (!passthrough && !isCountTokens) {
|
|
1441
1464
|
// ── Template replay: replace the entire request with a CC template ──
|
|
1442
1465
|
// Instead of transforming signals one by one, we build a new request
|
|
1443
1466
|
// from CC's exact template and inject only the conversation content.
|
|
@@ -1563,6 +1586,12 @@ export async function startProxy(opts = {}) {
|
|
|
1563
1586
|
// logic (family buckets, fable beta/effort) sees the user intent.
|
|
1564
1587
|
r.model = stripContext1mTag(r.model);
|
|
1565
1588
|
}
|
|
1589
|
+
else if (isCountTokens && typeof r.model === 'string') {
|
|
1590
|
+
// Thin count_tokens forward still normalizes the model id —
|
|
1591
|
+
// the literal `[1m]` label 404s upstream here exactly as it
|
|
1592
|
+
// does on /v1/messages.
|
|
1593
|
+
r.model = stripContext1mTag(r.model);
|
|
1594
|
+
}
|
|
1566
1595
|
finalBody = Buffer.from(JSON.stringify(r));
|
|
1567
1596
|
}
|
|
1568
1597
|
catch { /* not JSON, send as-is */ }
|
|
@@ -1588,8 +1617,9 @@ export async function startProxy(opts = {}) {
|
|
|
1588
1617
|
// Beta headers
|
|
1589
1618
|
const clientBeta = req.headers['anthropic-beta'];
|
|
1590
1619
|
let beta;
|
|
1591
|
-
if (passthrough) {
|
|
1592
|
-
// Passthrough: only add oauth beta,
|
|
1620
|
+
if (passthrough || isCountTokens) {
|
|
1621
|
+
// Passthrough (and thin count_tokens): only add oauth beta,
|
|
1622
|
+
// forward client betas as-is — no template beta set.
|
|
1593
1623
|
beta = 'oauth-2025-04-20';
|
|
1594
1624
|
if (clientBeta)
|
|
1595
1625
|
beta += ',' + clientBeta;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.51",
|
|
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": {
|