@askalf/dario 2.11.0 → 3.0.2

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.js CHANGED
@@ -6,6 +6,7 @@ import { join } from 'node:path';
6
6
  import { homedir, tmpdir } from 'node:os';
7
7
  import { arch, platform } from 'node:process';
8
8
  import { getAccessToken, getStatus } from './oauth.js';
9
+ import { buildCCRequest, reverseMapResponse } from './cc-template.js';
9
10
  const ANTHROPIC_API = 'https://api.anthropic.com';
10
11
  const DEFAULT_PORT = 3456;
11
12
  const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
@@ -236,194 +237,12 @@ function sanitizeMessages(body) {
236
237
  }
237
238
  }
238
239
  }
239
- /**
240
- * Strip thinking blocks from prior assistant messages.
241
- * Real Claude Code strips thinking from conversation history before building the next request.
242
- * The API's context_management: clear_thinking does NOT reduce input token billing —
243
- * tokens are counted before server-side edits. Client-side stripping is the only way
244
- * to avoid burning the 5h window on stale thinking traces.
245
- * Only strips from prior turns — the most recent assistant message is left intact.
246
- */
247
- function stripThinkingFromHistory(body) {
248
- const messages = body.messages;
249
- if (!messages)
250
- return;
251
- // Strip thinking blocks from ALL assistant messages.
252
- // Real Claude Code never sends thinking blocks in the messages array —
253
- // it strips them before building the next request. The API will generate
254
- // fresh thinking for the current turn; prior thinking is dead weight.
255
- for (const msg of messages) {
256
- if (msg.role !== 'assistant')
257
- continue;
258
- if (Array.isArray(msg.content)) {
259
- msg.content = msg.content.filter(b => b.type !== 'thinking');
260
- }
261
- }
262
- }
263
240
  /**
264
241
  * Scrub non-Claude-Code fields and normalize field ordering.
265
242
  * Real Claude Code never sends these fields. Their presence is a fingerprint.
266
243
  * JSON field order is also detectable — Claude Code always sends fields in a
267
244
  * specific order. We rebuild the object to match.
268
245
  */
269
- const NON_CC_FIELDS = new Set(['service_tier', 'top_p', 'top_k', 'stop_sequences', 'temperature']);
270
- // ── Tool name rewriting ──
271
- // Anthropic fingerprints on tool names — non-CC names trigger overage classification.
272
- // Map third-party tool names to CC equivalents on the way in, reverse on the way out.
273
- const CC_TOOLS = new Set([
274
- 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'Browser', 'WebFetch', 'WebSearch',
275
- 'NotebookEdit', 'NotebookRead', 'TodoRead', 'TodoWrite',
276
- 'Agent', 'MCPListTools', 'MCPCallTool',
277
- 'AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode',
278
- 'EnterWorktree', 'ExitWorktree', 'TaskCreate', 'TaskUpdate',
279
- ]);
280
- // Common third-party tool names → CC equivalents
281
- const TOOL_NAME_MAP = {
282
- bash: 'Bash', sh: 'Bash', exec: 'Bash', shell: 'Bash', run: 'Bash', execute: 'Bash',
283
- command: 'Bash', terminal: 'Bash', process: 'Bash',
284
- read: 'Read', read_file: 'Read', file_read: 'Read', get_file: 'Read',
285
- write: 'Write', write_file: 'Write', file_write: 'Write', create_file: 'Write', save_file: 'Write',
286
- edit: 'Edit', edit_file: 'Edit', modify_file: 'Edit', patch: 'Edit', replace: 'Edit',
287
- glob: 'Glob', find_files: 'Glob', list_files: 'Glob', ls: 'Glob',
288
- grep: 'Grep', search: 'Grep', search_files: 'Grep', find_in_files: 'Grep', rg: 'Grep',
289
- web_search: 'WebSearch', websearch: 'WebSearch', google: 'WebSearch',
290
- web_fetch: 'WebFetch', webfetch: 'WebFetch', fetch: 'WebFetch', http: 'WebFetch', curl: 'WebFetch',
291
- browse: 'Browser', browser: 'Browser', open_url: 'Browser',
292
- notebook: 'NotebookEdit', notebook_edit: 'NotebookEdit',
293
- };
294
- /**
295
- * Rewrite tool names in the request to match CC toolset.
296
- * Returns the mapping so we can reverse it in the response.
297
- * Tools that don't map to a known CC name get wrapped as MCPCallTool.
298
- */
299
- function rewriteToolNames(body) {
300
- const tools = body.tools;
301
- if (!tools || !Array.isArray(tools))
302
- return [];
303
- const mappings = [];
304
- const usedNames = new Set();
305
- // First pass: collect CC tool names already in the list
306
- for (const tool of tools) {
307
- if (CC_TOOLS.has(tool.name))
308
- usedNames.add(tool.name);
309
- }
310
- let mcpIndex = 0;
311
- for (const tool of tools) {
312
- const originalName = tool.name;
313
- if (!originalName)
314
- continue;
315
- // Already a CC tool name
316
- if (CC_TOOLS.has(originalName))
317
- continue;
318
- // Check direct map — but avoid duplicates
319
- const directMap = TOOL_NAME_MAP[originalName.toLowerCase()];
320
- if (directMap && !usedNames.has(directMap)) {
321
- mappings.push({ original: originalName, mapped: directMap });
322
- tool.name = directMap;
323
- usedNames.add(directMap);
324
- }
325
- else {
326
- // Wrap as mcp_<original_name> — MCP tools use this prefix in real CC
327
- const mcpName = `mcp_${originalName}`;
328
- mappings.push({ original: originalName, mapped: mcpName });
329
- tool.name = mcpName;
330
- }
331
- }
332
- // Cap tool count — CC sends max ~22 tools. Excess tools get consolidated
333
- // into a single MCPCallTool dispatch with routing table.
334
- const MAX_TOOLS = 22;
335
- if (tools.length > MAX_TOOLS) {
336
- const keep = tools.slice(0, MAX_TOOLS - 1); // keep first N-1
337
- const overflow = tools.slice(MAX_TOOLS - 1);
338
- // Build dispatch tool that wraps all overflow tools
339
- const dispatchDesc = overflow.map((t) => `${t.name}: ${(t.description || '').slice(0, 50)}`).join('\n');
340
- const dispatchTool = {
341
- name: 'mcp_dispatch',
342
- description: `Route to one of these tools:\n${dispatchDesc}`,
343
- input_schema: {
344
- type: 'object',
345
- properties: {
346
- tool_name: { type: 'string', description: 'Which tool to call', enum: overflow.map((t) => t.name) },
347
- input: { type: 'object', description: 'Arguments to pass to the tool' },
348
- },
349
- required: ['tool_name', 'input'],
350
- },
351
- };
352
- // Track overflow mappings for reverse
353
- for (const t of overflow) {
354
- mappings.push({ original: t.name, mapped: 'mcp_dispatch' });
355
- }
356
- // Replace tools array
357
- keep.push(dispatchTool);
358
- body.tools = keep;
359
- }
360
- return mappings;
361
- }
362
- /**
363
- * Reverse tool name mapping in the response body.
364
- * Restores original tool names in tool_use content blocks.
365
- */
366
- function reverseToolNames(body, mappings) {
367
- if (mappings.length === 0)
368
- return body;
369
- let result = body;
370
- for (const { original, mapped } of mappings) {
371
- // Replace in tool_use blocks: "name":"MCPCallTool" → "name":"original"
372
- result = result.replace(new RegExp(`"name"\\s*:\\s*"${mapped}"`, 'g'), `"name":"${original}"`);
373
- }
374
- return result;
375
- }
376
- // Claude Code's field order (from MITM capture). Fields not in this list are appended at end.
377
- const CC_FIELD_ORDER = [
378
- 'model', 'messages', 'system', 'max_tokens', 'thinking', 'output_config',
379
- 'context_management', 'metadata', 'stream', 'tools', 'tool_choice',
380
- ];
381
- function scrubAndReorderFields(body) {
382
- // Remove non-CC fields
383
- for (const field of NON_CC_FIELDS) {
384
- delete body[field];
385
- }
386
- // Rebuild with Claude Code field ordering
387
- const ordered = {};
388
- for (const key of CC_FIELD_ORDER) {
389
- if (key in body) {
390
- ordered[key] = body[key];
391
- delete body[key];
392
- }
393
- }
394
- // Append any remaining fields (custom client fields we don't recognize)
395
- for (const [key, value] of Object.entries(body)) {
396
- ordered[key] = value;
397
- }
398
- return ordered;
399
- }
400
- /**
401
- * Normalize system prompt to exactly 3 blocks.
402
- * Real Claude Code always sends exactly 3 system blocks:
403
- * [0] billing tag (no cache), [1] agent identity (cache 1h), [2] system prompt (cache 1h)
404
- * If the client sends multiple system blocks, merge them into block [2].
405
- */
406
- function normalizeSystemTo3Blocks(system, billingTag, agentIdentity, cache1h) {
407
- let systemText;
408
- if (typeof system === 'string') {
409
- systemText = system;
410
- }
411
- else if (Array.isArray(system)) {
412
- // Merge all text blocks into one, skip any existing billing tags
413
- systemText = system
414
- .filter(b => b.text && !b.text.includes('x-anthropic-billing-header:'))
415
- .map(b => b.text)
416
- .join('\n\n');
417
- }
418
- else {
419
- systemText = '';
420
- }
421
- return [
422
- { type: 'text', text: billingTag },
423
- { type: 'text', text: agentIdentity, cache_control: cache1h },
424
- { type: 'text', text: systemText || 'You are a helpful assistant.', cache_control: cache1h },
425
- ];
426
- }
427
246
  // OpenAI model names → Anthropic (fallback if client sends GPT names)
428
247
  const OPENAI_MODEL_MAP = {
429
248
  'gpt-5.4': 'claude-opus-4-6',
@@ -699,7 +518,7 @@ export async function startProxy(opts = {}) {
699
518
  'accept': 'application/json',
700
519
  'Content-Type': 'application/json',
701
520
  'anthropic-dangerous-direct-browser-access': 'true',
702
- 'user-agent': `claude-cli/${cliVersion} (external, cli)`,
521
+ 'user-agent': `claude-cli/${cliVersion} (external, cli, workload/cron)`,
703
522
  'x-app': 'cli',
704
523
  'x-claude-code-session-id': SESSION_ID,
705
524
  'x-stainless-arch': arch,
@@ -735,7 +554,7 @@ export async function startProxy(opts = {}) {
735
554
  const JSON_HEADERS = { 'Content-Type': 'application/json', ...SECURITY_HEADERS };
736
555
  const MODELS_JSON = JSON.stringify(OPENAI_MODELS_LIST);
737
556
  const ERR_UNAUTH = JSON.stringify({ error: 'Unauthorized', message: 'Invalid or missing API key' });
738
- const ERR_FORBIDDEN = JSON.stringify({ error: 'Forbidden', message: 'Path not allowed' });
557
+ const ERR_FORBIDDEN = JSON.stringify({ error: 'Forbidden', message: 'Path not allowed. Supported paths: POST /v1/messages, POST /v1/chat/completions, GET /v1/models' });
739
558
  const ERR_METHOD = JSON.stringify({ error: 'Method not allowed' });
740
559
  function checkAuth(req) {
741
560
  if (!apiKeyBuf)
@@ -858,7 +677,7 @@ export async function startProxy(opts = {}) {
858
677
  }
859
678
  // Parse body once, apply OpenAI translation, model override, and sanitization
860
679
  let finalBody = body.length > 0 ? body : undefined;
861
- let toolMappings = [];
680
+ let ccToolMap = null;
862
681
  if (body.length > 0) {
863
682
  try {
864
683
  const parsed = JSON.parse(body.toString());
@@ -868,67 +687,24 @@ export async function startProxy(opts = {}) {
868
687
  const r = result;
869
688
  // In passthrough mode, skip all Claude-specific injection — OAuth swap only
870
689
  if (!passthrough) {
871
- // ── Stealth layer: make request indistinguishable from real Claude Code ──
872
- // 1. Strip thinking blocks from prior assistant turns (client-side).
873
- // context_management: clear_thinking does NOT reduce input token billing.
874
- // Real Claude Code strips thinking before building the next request.
875
- stripThinkingFromHistory(r);
876
- // 2. Strip client cache_control from messages (prevents overflow — max 4 breakpoints)
877
- const msgs = r.messages;
878
- if (msgs) {
879
- for (const msg of msgs) {
880
- if (Array.isArray(msg.content)) {
881
- for (const block of msg.content) {
882
- delete block.cache_control;
883
- }
884
- }
885
- }
886
- }
887
- // 3. Rewrite tool names to CC equivalents (Anthropic fingerprints on tool names)
888
- toolMappings = rewriteToolNames(r);
889
- // 3. Scrub non-CC fields and normalize field ordering
890
- const reordered = scrubAndReorderFields(r);
891
- for (const key of Object.keys(r))
892
- delete r[key];
893
- Object.assign(r, reordered);
894
- // 3. Inject device identity metadata for session tracking
895
- if (identity.deviceId) {
896
- r.metadata = {
897
- user_id: JSON.stringify({
898
- device_id: identity.deviceId,
899
- account_uuid: identity.accountUuid,
900
- session_id: SESSION_ID,
901
- }),
902
- };
903
- }
904
- // 4. Model-aware defaults matching Claude Code behavior
905
- const modelName = (r.model || '').toLowerCase();
906
- const supportsThinking = !modelName.includes('haiku');
907
- if (supportsThinking && !r.thinking) {
908
- r.thinking = { type: 'adaptive' };
909
- }
910
- // Claude Code always sends max_tokens: 64000. Values above this
911
- // are a fingerprint — cap to match real CC behavior.
912
- if (!r.max_tokens || r.max_tokens !== 64000) {
913
- r.max_tokens = 64000;
914
- }
915
- // Force effort to medium — CC default. Client 'high' is a fingerprint.
916
- if (supportsThinking) {
917
- r.output_config = { effort: 'medium' };
918
- }
919
- if (supportsThinking && !r.context_management) {
920
- r.context_management = { edits: [{ type: 'clear_thinking_20251015', keep: 'all' }] };
921
- }
922
- // 5. Build per-request billing tag matching Claude Code binary (Oz$ algorithm)
690
+ // ── Template replay: replace the entire request with a CC template ──
691
+ // Instead of transforming signals one by one, we build a new request
692
+ // from CC's exact template and inject only the conversation content.
693
+ // The upstream sees a genuine CC request structure.
923
694
  const userMsg = extractFirstUserMessage(r);
924
695
  const buildTag = computeBuildTag(userMsg, cliVersion);
925
696
  const cch = computeCch();
926
697
  const fullVersion = `${cliVersion}.${buildTag}`;
927
- const billingTag = `x-anthropic-billing-header: cc_version=${fullVersion}; cc_entrypoint=cli; cch=${cch};`;
928
- // 6. Normalize system prompt to exactly 3 blocks (real Claude Code always sends 3)
698
+ const billingTag = `x-anthropic-billing-header: cc_version=${fullVersion}; cc_entrypoint=cli; cch=${cch}; cc_workload=cron;`;
929
699
  const AGENT_IDENTITY = 'You are a Claude agent, built on Anthropic\'s Claude Agent SDK.';
930
700
  const CACHE_1H = { type: 'ephemeral', ttl: '1h' };
931
- r.system = normalizeSystemTo3Blocks(r.system, billingTag, AGENT_IDENTITY, CACHE_1H);
701
+ const { body: ccBody, toolMap } = buildCCRequest(r, billingTag, AGENT_IDENTITY, CACHE_1H, { deviceId: identity.deviceId, accountUuid: identity.accountUuid, sessionId: SESSION_ID });
702
+ // Store tool map for response reverse-mapping
703
+ ccToolMap = toolMap;
704
+ // Replace request body entirely with CC template
705
+ for (const key of Object.keys(r))
706
+ delete r[key];
707
+ Object.assign(r, ccBody);
932
708
  }
933
709
  finalBody = Buffer.from(JSON.stringify(r));
934
710
  }
@@ -949,7 +725,8 @@ export async function startProxy(opts = {}) {
949
725
  }
950
726
  else {
951
727
  // Claude-optimized: full beta set matching real Claude Code (exact order from MITM capture)
952
- beta = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,effort-2025-11-24,fast-mode-2026-02-01';
728
+ // Beta set from CC v2.1.104 binary RE — some are CC-internal/gated, only include publicly accepted ones
729
+ beta = 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,effort-2025-11-24,fast-mode-2026-02-01,redact-thinking-2026-02-12,context-1m-2025-08-07,web-search-2025-03-05,advanced-tool-use-2025-11-20,tool-search-tool-2025-10-19';
953
730
  if (clientBeta) {
954
731
  const baseSet = new Set(beta.split(','));
955
732
  const filtered = filterBillableBetas(clientBeta)
@@ -961,7 +738,7 @@ export async function startProxy(opts = {}) {
961
738
  const headers = {
962
739
  ...staticHeaders,
963
740
  'Authorization': `Bearer ${accessToken}`,
964
- 'anthropic-version': req.headers['anthropic-version'] || '2023-06-01',
741
+ 'anthropic-version': passthrough ? (req.headers['anthropic-version'] || '2023-06-01') : '2023-06-01',
965
742
  'anthropic-beta': beta,
966
743
  // Real Claude Code adds x-client-request-id for firstParty + api.anthropic.com
967
744
  'x-client-request-id': randomUUID(),
@@ -1062,9 +839,9 @@ export async function startProxy(opts = {}) {
1062
839
  }
1063
840
  else {
1064
841
  // Reverse tool names in streaming chunks
1065
- if (toolMappings.length > 0) {
842
+ if (ccToolMap && ccToolMap.size > 0) {
1066
843
  const text = new TextDecoder().decode(value);
1067
- res.write(reverseToolNames(text, toolMappings));
844
+ res.write(reverseMapResponse(text, ccToolMap));
1068
845
  }
1069
846
  else {
1070
847
  res.write(value);
@@ -1088,7 +865,8 @@ export async function startProxy(opts = {}) {
1088
865
  // Buffer and forward
1089
866
  let responseBody = await upstream.text();
1090
867
  // Reverse tool name mapping so client sees original names
1091
- responseBody = reverseToolNames(responseBody, toolMappings);
868
+ if (ccToolMap)
869
+ responseBody = reverseMapResponse(responseBody, ccToolMap);
1092
870
  if (isOpenAI && upstream.status >= 200 && upstream.status < 300) {
1093
871
  try {
1094
872
  const parsed = JSON.parse(responseBody);
package/package.json CHANGED
@@ -1,64 +1,64 @@
1
- {
2
- "name": "@askalf/dario",
3
- "version": "2.11.0",
4
- "description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
5
- "type": "module",
6
- "bin": {
7
- "dario": "./dist/cli.js"
8
- },
9
- "main": "./dist/index.js",
10
- "types": "./dist/index.d.ts",
11
- "exports": {
12
- ".": {
13
- "import": "./dist/index.js",
14
- "types": "./dist/index.d.ts"
15
- }
16
- },
17
- "files": [
18
- "dist",
19
- "README.md",
20
- "LICENSE"
21
- ],
22
- "scripts": {
23
- "build": "tsc",
24
- "audit": "npm audit --production --audit-level=high",
25
- "prepublishOnly": "npm run build",
26
- "start": "node dist/cli.js",
27
- "dev": "tsx src/cli.ts",
28
- "e2e": "node test/e2e.mjs",
29
- "compat": "node test/compat.mjs"
30
- },
31
- "keywords": [
32
- "claude",
33
- "anthropic",
34
- "oauth",
35
- "proxy",
36
- "api",
37
- "bridge",
38
- "subscription",
39
- "claude-max",
40
- "claude-pro",
41
- "llm",
42
- "ai",
43
- "cli",
44
- "developer-tools"
45
- ],
46
- "author": "askalf (https://github.com/askalf)",
47
- "license": "MIT",
48
- "repository": {
49
- "type": "git",
50
- "url": "https://github.com/askalf/dario.git"
51
- },
52
- "homepage": "https://github.com/askalf/dario",
53
- "bugs": {
54
- "url": "https://github.com/askalf/dario/issues"
55
- },
56
- "engines": {
57
- "node": ">=18.0.0"
58
- },
59
- "devDependencies": {
60
- "@types/node": "^22.0.0",
61
- "tsx": "^4.19.0",
62
- "typescript": "^5.7.0"
63
- }
1
+ {
2
+ "name": "@askalf/dario",
3
+ "version": "3.0.2",
4
+ "description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
5
+ "type": "module",
6
+ "bin": {
7
+ "dario": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "audit": "npm audit --production --audit-level=high",
25
+ "prepublishOnly": "npm run build",
26
+ "start": "node dist/cli.js",
27
+ "dev": "tsx src/cli.ts",
28
+ "e2e": "node test/e2e.mjs",
29
+ "compat": "node test/compat.mjs"
30
+ },
31
+ "keywords": [
32
+ "claude",
33
+ "anthropic",
34
+ "oauth",
35
+ "proxy",
36
+ "api",
37
+ "bridge",
38
+ "subscription",
39
+ "claude-max",
40
+ "claude-pro",
41
+ "llm",
42
+ "ai",
43
+ "cli",
44
+ "developer-tools"
45
+ ],
46
+ "author": "askalf (https://github.com/askalf)",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/askalf/dario.git"
51
+ },
52
+ "homepage": "https://github.com/askalf/dario",
53
+ "bugs": {
54
+ "url": "https://github.com/askalf/dario/issues"
55
+ },
56
+ "engines": {
57
+ "node": ">=18.0.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.0.0",
61
+ "tsx": "^4.19.0",
62
+ "typescript": "^5.7.0"
63
+ }
64
64
  }