@askalf/dario 4.8.50 → 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/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
- // ?beta=true matches native Claude Code behavior for billing classification
1175
- const allowedPaths = {
1176
- '/v1/messages': `${ANTHROPIC_API}/v1/messages?beta=true`,
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
- if (!passthrough) {
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, forward client betas as-is
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.50",
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": {