@askalf/dario 3.0.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <h1 align="center">dario</h1>
3
3
  <p align="center"><strong>Use your Claude subscription as an API. The only proxy that bills correctly.</strong></p>
4
4
  <p align="center">
5
- No API key needed. Your Claude Max/Pro subscription becomes a local API endpoint<br/>that any tool, SDK, or framework can use with native billing classification,<br/>so your Max plan limits actually work.
5
+ No API key needed. Your Claude Max/Pro subscription becomes a local API endpoint<br/>that any tool, SDK, or framework can use. Template replay makes every request<br/>indistinguishable from real Claude Code — so your Max plan limits actually work.
6
6
  </p>
7
7
  </p>
8
8
 
@@ -33,7 +33,7 @@ export ANTHROPIC_BASE_URL=http://localhost:3456 # or OPENAI_BASE_URL=http://lo
33
33
  export ANTHROPIC_API_KEY=dario # or OPENAI_API_KEY=dario
34
34
  ```
35
35
 
36
- Opus, Sonnet, Haiku — all models, streaming, tool use. **Zero dependencies.** ~1,600 lines of TypeScript. Works with Cursor, Continue, Aider, LiteLLM, Hermes, OpenClaw, or any tool that speaks the Anthropic or OpenAI API. When rate limited, `--cli` routes through Claude Code for uninterrupted Opus access.
36
+ Opus, Sonnet, Haiku — all models, streaming, tool use. **Zero dependencies.** ~1,900 lines of TypeScript. Works with Cursor, Continue, Aider, LiteLLM, Hermes, OpenClaw, or any tool that speaks the Anthropic or OpenAI API. When rate limited, `--cli` routes through Claude Code for uninterrupted Opus access.
37
37
 
38
38
  <table>
39
39
  <tr>
@@ -80,30 +80,28 @@ Opus, Sonnet, Haiku — all models, streaming, tool use. **Zero dependencies.**
80
80
 
81
81
  Most Claude subscription proxies have a critical billing problem: **Anthropic classifies their requests as third-party and routes all usage to Extra Usage billing** — even when you have Max plan limits available. You're paying for your subscription twice.
82
82
 
83
- dario is the only proxy that solves this. It injects native Claude Code device identity, per-request billing checksums (reverse-engineered from the Claude Code binary), and priority routing into every request so Anthropic's billing system treats your requests exactly like Claude Code itself. Your Max plan limits work correctly, and Opus/Sonnet stay available even at high utilization.
83
+ dario is the only proxy that solves this. Instead of transforming your requests signal by signal, dario v3.0 uses **template replay** — it replaces the entire request with Claude Code's exact template. CC's tool definitions, CC's field structure, CC's parameters. Only your conversation content is preserved. Anthropic's classifier sees a genuine Claude Code request because it IS one.
84
84
 
85
85
  | | dario | Other proxies |
86
86
  |---|---|---|
87
- | **Billing classification** | Native Claude Code session | Third-party (Extra Usage) |
87
+ | **Approach** | Template replay sends CC's actual request | Signal matching or none |
88
+ | **Tools** | CC's exact tool definitions sent upstream | Client tools (detected) |
88
89
  | **Max plan limits** | Used correctly | Bypassed — billed separately |
89
- | **Device identity** | Injected automatically | Missing |
90
- | **Priority routing** | Full billing tag fingerprint | Missing |
91
- | **Billing tag fingerprint** | Per-request SHA-256 matching binary RE | Static or missing |
92
- | **Beta flags** | Match Claude Code v2.1.100 | Outdated or missing |
93
- | **Billable beta filtering** | Strips surprise charges | Passes everything through |
90
+ | **Detection resistance** | Undetectable without flagging CC itself | Detected by tool names, field order, effort level, etc. |
91
+ | **Dependencies** | 0 | Many |
94
92
 
95
93
  <details>
96
94
  <summary><strong>vs competitors</strong></summary>
97
95
 
98
96
  | Feature | dario | Meridian (710 stars) | CLIProxyAPI (24K stars) |
99
97
  |---------|-------|---------|------------|
100
- | Native billing classification | **Yes** | No | Inherited (CLI-only) |
98
+ | Template replay (undetectable) | **Yes** | No | Inherited (CLI-only) |
101
99
  | Direct OAuth (streaming, tools) | **Yes** | Yes (SDK-based) | No |
102
100
  | CLI fallback (rate limit bypass) | **Yes** | No | Yes (only mode) |
103
101
  | OpenAI API compat | **Yes** | Yes | Yes |
104
102
  | Orchestration sanitization | **Yes** | Yes | No |
105
103
  | Token anomaly detection | **Yes** | Yes | No |
106
- | Codebase size | ~1,600 lines | ~9,000 lines | Platform |
104
+ | Codebase size | ~1,900 lines | ~9,000 lines | Platform |
107
105
  | Dependencies | 0 | Many | Many |
108
106
  | Setup | 2 commands | Config + build | Config + dashboard |
109
107
 
@@ -386,18 +384,21 @@ Add to your `openclaw.json` models config:
386
384
 
387
385
  ## How It Works
388
386
 
389
- ### Direct API Mode (default)
387
+ ### Direct API Mode (default) — Template Replay
390
388
 
391
389
  ```
392
- ┌──────────┐ ┌─────────────────┐ ┌──────────────────┐
393
- │ Your App │ ──> │ dario (proxy) │ ──> │ api.anthropic.com│
394
- │ │ │ localhost:3456 │ │ │
395
- │ sends │ │ swaps API key │ │ sees valid
396
- API key │ │ for OAuth │ │ OAuth bearer
397
- "dario" │ │ bearer token │ │ token
398
- └──────────┘ └─────────────────┘ └──────────────────┘
390
+ ┌──────────┐ ┌─────────────────────┐ ┌──────────────────┐
391
+ │ Your App │ ──> │ dario (proxy) │ ──> │ api.anthropic.com│
392
+ │ │ │ localhost:3456 │ │ │
393
+ │ sends │ │ │ │ sees a genuine
394
+ its own │ │ replaces request │ │ Claude Code
395
+ tools & │ │ with CC template │ │ request
396
+ │ params │ │ keeps only content │ │ │
397
+ └──────────┘ └─────────────────────┘ └──────────────────┘
399
398
  ```
400
399
 
400
+ Your app sends whatever it wants — any tools, any parameters. dario replaces the entire request with Claude Code's template and injects only your conversation content. The upstream sees CC's exact tool definitions, field structure, and parameters.
401
+
401
402
  ### CLI Backend Mode (`--cli`)
402
403
 
403
404
  ```
@@ -454,10 +455,7 @@ Add to your `openclaw.json` models config:
454
455
 
455
456
  ### Direct API Mode
456
457
  - All Claude models (Opus 4.6, Sonnet 4.6, Haiku 4.5) + 1M extended context aliases (`opus1m`, `sonnet1m`)
457
- - **Native billing classification** — device identity, per-request billing tag with SHA-256 checksums matching real Claude Code (extracted via binary RE), ensures Max plan limits work correctly
458
- - **Template replay** (v3.0) — instead of transforming requests signal-by-signal, dario replaces the entire request with a Claude Code template. CC's exact tool definitions, field structure, and parameters are sent upstream. Only your conversation content is preserved. Tested with 40 third-party tools — all route to `five_hour`. See [Discussion #13](https://github.com/askalf/dario/discussions/13) for why this matters.
459
- - **Adaptive thinking** — matches Claude Code's `{ type: 'adaptive' }` mode for optimal reasoning (auto-skipped for Haiku 4.5)
460
- - **Effort control** — injects `output_config: { effort: 'medium' }` matching Claude Code's default, or passes through client-specified effort level
458
+ - **Template replay** (v3.0) replaces the entire request with Claude Code's exact template. CC's tool definitions, field structure, and parameters are sent upstream. Only your conversation content is preserved. Your client's tools are mapped to CC equivalents and reverse-mapped in responses. Tested with 40 third-party tools all route to `five_hour`. See [Discussion 13](https://github.com/askalf/dario/discussions/13) and [Discussion 14](https://github.com/askalf/dario/discussions/14).
461
459
  - **Enriched 429 errors** — rate limit errors include utilization %, limiting window, and reset time instead of Anthropic's default `"Error"` message
462
460
  - **Auto CLI fallback** — if the API returns 429 and Claude Code is installed, transparently retries through `claude --print` with SSE conversion
463
461
  - **OpenAI-compatible** (`/v1/chat/completions`) — works with any OpenAI SDK or tool
@@ -582,7 +580,7 @@ Dario handles your OAuth tokens. Here's why you can trust it:
582
580
 
583
581
  | Signal | Status |
584
582
  |--------|--------|
585
- | **Source code** | ~1,600 lines of TypeScript — small enough to audit in one sitting |
583
+ | **Source code** | ~1,900 lines of TypeScript — small enough to audit in one sitting |
586
584
  | **Dependencies** | 0 runtime dependencies. Verify: `npm ls --production` |
587
585
  | **npm provenance** | Every release is [SLSA attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions |
588
586
  | **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
@@ -606,18 +604,21 @@ cd $(npm root -g)/@askalf/dario && npm ls --production
606
604
 
607
605
  | Topic | Link |
608
606
  |-------|------|
609
- | Billing tag algorithm, fingerprint analysis, Hermes/OpenClaw compatibility | [Discussion #8](https://github.com/askalf/dario/discussions/8) |
610
- | Why Opus 4.6 feels worse and how to fix it (thinking block accumulation, effort defaults) | [Discussion #9](https://github.com/askalf/dario/discussions/9) |
611
- | Rate limit header analysis and subscription throttling mechanics | [Discussion #1](https://github.com/askalf/dario/discussions/1) |
607
+ | v3.0 Template Replay why we stopped matching signals | [Discussion 14](https://github.com/askalf/dario/discussions/14) |
608
+ | Claude Code defaults are detection signals, not optimizations | [Discussion 13](https://github.com/askalf/dario/discussions/13) |
609
+ | Why Opus 4.6 feels worse and how to fix it | [Discussion 9](https://github.com/askalf/dario/discussions/9) |
610
+ | Billing tag algorithm and fingerprint analysis | [Discussion 8](https://github.com/askalf/dario/discussions/8) |
611
+ | Rate limit header analysis | [Discussion 1](https://github.com/askalf/dario/discussions/1) |
612
612
 
613
613
  ## Contributing
614
614
 
615
- PRs welcome. The codebase is ~1,600 lines of TypeScript across 4 files:
615
+ PRs welcome. The codebase is ~1,900 lines of TypeScript across 5 files:
616
616
 
617
617
  | File | Purpose |
618
618
  |------|---------|
619
- | `src/oauth.ts` | Token storage, refresh logic, Claude Code credential detection, auto OAuth flow |
620
619
  | `src/proxy.ts` | HTTP proxy server + CLI backend |
620
+ | `src/cc-template.ts` | Claude Code request template + tool mapping |
621
+ | `src/oauth.ts` | Token storage, refresh, credential detection |
621
622
  | `src/cli.ts` | CLI entry point |
622
623
  | `src/index.ts` | Library exports |
623
624
 
@@ -211,17 +211,31 @@ export function buildCCRequest(clientBody, billingTag, agentIdentity, cache1h, i
211
211
  }
212
212
  else {
213
213
  unmappedTools.push(tool.name);
214
- // Unknown tools become Bash commands with description as context
214
+ // Distribute unmapped tools across CC tool names to avoid suspicious
215
+ // patterns where every unknown tool maps to Bash
216
+ const CC_FALLBACK_TOOLS = ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
217
+ const fallbackTool = CC_FALLBACK_TOOLS[unmappedTools.length % CC_FALLBACK_TOOLS.length];
215
218
  activeToolMap.set(tool.name, {
216
- ccTool: 'Bash',
217
- translateArgs: (a) => ({
218
- command: `echo "Tool ${tool.name} called with: ${JSON.stringify(a).slice(0, 200)}"`,
219
- }),
219
+ ccTool: fallbackTool,
220
+ translateArgs: (a) => {
221
+ // Translate args to match the CC tool's expected schema
222
+ switch (fallbackTool) {
223
+ case 'Bash': return { command: `echo "${JSON.stringify(a).slice(0, 200)}"` };
224
+ case 'Read': return { file_path: String(a.path || a.file || a.url || '/tmp/output') };
225
+ case 'Grep': return { pattern: String(a.query || a.pattern || a.search || '.'), path: '.' };
226
+ case 'Glob': return { pattern: String(a.pattern || a.glob || '*') };
227
+ case 'WebSearch': return { query: String(a.query || a.q || a.search || '') };
228
+ case 'WebFetch': return { url: String(a.url || a.uri || '') };
229
+ default: return a;
230
+ }
231
+ },
220
232
  });
221
233
  }
222
234
  }
223
235
  }
224
- // ── Remap tool_use references in message history ──
236
+ // ── Remap tool_use and tool_result references in message history ──
237
+ // Track tool_use_id → CC tool name for consistent remapping
238
+ const toolUseIdMap = new Map();
225
239
  for (const msg of messages) {
226
240
  if (Array.isArray(msg.content)) {
227
241
  for (const block of msg.content) {
@@ -233,6 +247,41 @@ export function buildCCRequest(clientBody, billingTag, agentIdentity, cache1h, i
233
247
  block.input = mapping.translateArgs(block.input);
234
248
  }
235
249
  }
250
+ // Track the ID so tool_results stay consistent
251
+ if (typeof block.id === 'string') {
252
+ toolUseIdMap.set(block.id, block.name);
253
+ }
254
+ }
255
+ // Strip any client-specific fields from tool_result blocks that CC wouldn't send
256
+ if (block.type === 'tool_result') {
257
+ // Remove non-standard fields clients may add
258
+ for (const key of Object.keys(block)) {
259
+ if (!['type', 'tool_use_id', 'content', 'is_error'].includes(key)) {
260
+ delete block[key];
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ // ── Compact conversation history ──
268
+ // Real CC conversations have specific patterns. Strip metadata that
269
+ // third-party frameworks inject into tool_result content.
270
+ for (const msg of messages) {
271
+ if (Array.isArray(msg.content)) {
272
+ for (const block of msg.content) {
273
+ // Truncate very long tool_result content — CC tool results are typically
274
+ // shorter because CC truncates file reads, command output, etc.
275
+ if (block.type === 'tool_result' && typeof block.content === 'string' && block.content.length > 30000) {
276
+ block.content = block.content.slice(0, 30000) + '\n[...truncated]';
277
+ }
278
+ // Also handle array-form tool_result content
279
+ if (block.type === 'tool_result' && Array.isArray(block.content)) {
280
+ for (const sub of block.content) {
281
+ if (sub.type === 'text' && typeof sub.text === 'string' && sub.text.length > 30000) {
282
+ sub.text = sub.text.slice(0, 30000) + '\n[...truncated]';
283
+ }
284
+ }
236
285
  }
237
286
  }
238
287
  }
@@ -249,6 +298,16 @@ export function buildCCRequest(clientBody, billingTag, agentIdentity, cache1h, i
249
298
  .map(b => b.text)
250
299
  .join('\n\n');
251
300
  }
301
+ // Strip framework identifiers from system prompt that would flag non-CC usage
302
+ const FRAMEWORK_PATTERNS = [
303
+ /\b(openclaw|hermes|aider|cursor|windsurf|cline|continue|copilot|cody)\b/gi,
304
+ /\b(openai|gpt-4|gpt-3\.5)\b/gi,
305
+ /powered by [a-z]+/gi,
306
+ /\bgateway\b/gi,
307
+ ];
308
+ for (const pattern of FRAMEWORK_PATTERNS) {
309
+ systemText = systemText.replace(pattern, '');
310
+ }
252
311
  // ── Build the CC request from template ──
253
312
  const ccRequest = {
254
313
  model,
@@ -260,11 +319,13 @@ export function buildCCRequest(clientBody, billingTag, agentIdentity, cache1h, i
260
319
  ],
261
320
  max_tokens: 64000,
262
321
  };
263
- // Model-specific fields
322
+ // Model-specific fields (matches CC v2.1.104 exactly)
264
323
  if (!isHaiku) {
265
324
  ccRequest.thinking = { type: 'adaptive' };
266
325
  ccRequest.output_config = { effort: 'medium' };
267
326
  ccRequest.context_management = { edits: [{ type: 'clear_thinking_20251015', keep: 'all' }] };
327
+ // CC sends temperature:1 explicitly when not in thinking-only mode
328
+ ccRequest.temperature = 1;
268
329
  }
269
330
  // Always include metadata
270
331
  ccRequest.metadata = {
package/dist/oauth.js CHANGED
@@ -108,7 +108,7 @@ export async function startAutoOAuthFlow() {
108
108
  .catch(reject);
109
109
  });
110
110
  let port = 0;
111
- server.listen(0, 'localhost', () => {
111
+ server.listen(0, 'localhost', async () => {
112
112
  const addr = server.address();
113
113
  port = typeof addr === 'object' && addr ? addr.port : 0;
114
114
  const params = new URLSearchParams({
@@ -127,7 +127,7 @@ export async function startAutoOAuthFlow() {
127
127
  console.log(` If the browser didn't open, visit: ${authUrl}`);
128
128
  console.log('');
129
129
  // Open browser using platform-specific commands (no external deps)
130
- const { exec } = require('node:child_process');
130
+ const { exec } = await import('node:child_process');
131
131
  const cmd = process.platform === 'win32' ? `start "" "${authUrl}"`
132
132
  : process.platform === 'darwin' ? `open "${authUrl}"`
133
133
  : `xdg-open "${authUrl}"`;
package/dist/proxy.js CHANGED
@@ -237,194 +237,12 @@ function sanitizeMessages(body) {
237
237
  }
238
238
  }
239
239
  }
240
- /**
241
- * Strip thinking blocks from prior assistant messages.
242
- * Real Claude Code strips thinking from conversation history before building the next request.
243
- * The API's context_management: clear_thinking does NOT reduce input token billing —
244
- * tokens are counted before server-side edits. Client-side stripping is the only way
245
- * to avoid burning the 5h window on stale thinking traces.
246
- * Only strips from prior turns — the most recent assistant message is left intact.
247
- */
248
- function stripThinkingFromHistory(body) {
249
- const messages = body.messages;
250
- if (!messages)
251
- return;
252
- // Strip thinking blocks from ALL assistant messages.
253
- // Real Claude Code never sends thinking blocks in the messages array —
254
- // it strips them before building the next request. The API will generate
255
- // fresh thinking for the current turn; prior thinking is dead weight.
256
- for (const msg of messages) {
257
- if (msg.role !== 'assistant')
258
- continue;
259
- if (Array.isArray(msg.content)) {
260
- msg.content = msg.content.filter(b => b.type !== 'thinking');
261
- }
262
- }
263
- }
264
240
  /**
265
241
  * Scrub non-Claude-Code fields and normalize field ordering.
266
242
  * Real Claude Code never sends these fields. Their presence is a fingerprint.
267
243
  * JSON field order is also detectable — Claude Code always sends fields in a
268
244
  * specific order. We rebuild the object to match.
269
245
  */
270
- const NON_CC_FIELDS = new Set(['service_tier', 'top_p', 'top_k', 'stop_sequences', 'temperature']);
271
- // ── Tool name rewriting ──
272
- // Anthropic fingerprints on tool names — non-CC names trigger overage classification.
273
- // Map third-party tool names to CC equivalents on the way in, reverse on the way out.
274
- const CC_TOOLS = new Set([
275
- 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'Browser', 'WebFetch', 'WebSearch',
276
- 'NotebookEdit', 'NotebookRead', 'TodoRead', 'TodoWrite',
277
- 'Agent', 'MCPListTools', 'MCPCallTool',
278
- 'AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode',
279
- 'EnterWorktree', 'ExitWorktree', 'TaskCreate', 'TaskUpdate',
280
- ]);
281
- // Common third-party tool names → CC equivalents
282
- const TOOL_NAME_MAP = {
283
- bash: 'Bash', sh: 'Bash', exec: 'Bash', shell: 'Bash', run: 'Bash', execute: 'Bash',
284
- command: 'Bash', terminal: 'Bash', process: 'Bash',
285
- read: 'Read', read_file: 'Read', file_read: 'Read', get_file: 'Read',
286
- write: 'Write', write_file: 'Write', file_write: 'Write', create_file: 'Write', save_file: 'Write',
287
- edit: 'Edit', edit_file: 'Edit', modify_file: 'Edit', patch: 'Edit', replace: 'Edit',
288
- glob: 'Glob', find_files: 'Glob', list_files: 'Glob', ls: 'Glob',
289
- grep: 'Grep', search: 'Grep', search_files: 'Grep', find_in_files: 'Grep', rg: 'Grep',
290
- web_search: 'WebSearch', websearch: 'WebSearch', google: 'WebSearch',
291
- web_fetch: 'WebFetch', webfetch: 'WebFetch', fetch: 'WebFetch', http: 'WebFetch', curl: 'WebFetch',
292
- browse: 'Browser', browser: 'Browser', open_url: 'Browser',
293
- notebook: 'NotebookEdit', notebook_edit: 'NotebookEdit',
294
- };
295
- /**
296
- * Rewrite tool names in the request to match CC toolset.
297
- * Returns the mapping so we can reverse it in the response.
298
- * Tools that don't map to a known CC name get wrapped as MCPCallTool.
299
- */
300
- function rewriteToolNames(body) {
301
- const tools = body.tools;
302
- if (!tools || !Array.isArray(tools))
303
- return [];
304
- const mappings = [];
305
- const usedNames = new Set();
306
- // First pass: collect CC tool names already in the list
307
- for (const tool of tools) {
308
- if (CC_TOOLS.has(tool.name))
309
- usedNames.add(tool.name);
310
- }
311
- let mcpIndex = 0;
312
- for (const tool of tools) {
313
- const originalName = tool.name;
314
- if (!originalName)
315
- continue;
316
- // Already a CC tool name
317
- if (CC_TOOLS.has(originalName))
318
- continue;
319
- // Check direct map — but avoid duplicates
320
- const directMap = TOOL_NAME_MAP[originalName.toLowerCase()];
321
- if (directMap && !usedNames.has(directMap)) {
322
- mappings.push({ original: originalName, mapped: directMap });
323
- tool.name = directMap;
324
- usedNames.add(directMap);
325
- }
326
- else {
327
- // Wrap as mcp_<original_name> — MCP tools use this prefix in real CC
328
- const mcpName = `mcp_${originalName}`;
329
- mappings.push({ original: originalName, mapped: mcpName });
330
- tool.name = mcpName;
331
- }
332
- }
333
- // Cap tool count — CC sends max ~22 tools. Excess tools get consolidated
334
- // into a single MCPCallTool dispatch with routing table.
335
- const MAX_TOOLS = 22;
336
- if (tools.length > MAX_TOOLS) {
337
- const keep = tools.slice(0, MAX_TOOLS - 1); // keep first N-1
338
- const overflow = tools.slice(MAX_TOOLS - 1);
339
- // Build dispatch tool that wraps all overflow tools
340
- const dispatchDesc = overflow.map((t) => `${t.name}: ${(t.description || '').slice(0, 50)}`).join('\n');
341
- const dispatchTool = {
342
- name: 'mcp_dispatch',
343
- description: `Route to one of these tools:\n${dispatchDesc}`,
344
- input_schema: {
345
- type: 'object',
346
- properties: {
347
- tool_name: { type: 'string', description: 'Which tool to call', enum: overflow.map((t) => t.name) },
348
- input: { type: 'object', description: 'Arguments to pass to the tool' },
349
- },
350
- required: ['tool_name', 'input'],
351
- },
352
- };
353
- // Track overflow mappings for reverse
354
- for (const t of overflow) {
355
- mappings.push({ original: t.name, mapped: 'mcp_dispatch' });
356
- }
357
- // Replace tools array
358
- keep.push(dispatchTool);
359
- body.tools = keep;
360
- }
361
- return mappings;
362
- }
363
- /**
364
- * Reverse tool name mapping in the response body.
365
- * Restores original tool names in tool_use content blocks.
366
- */
367
- function reverseToolNames(body, mappings) {
368
- if (mappings.length === 0)
369
- return body;
370
- let result = body;
371
- for (const { original, mapped } of mappings) {
372
- // Replace in tool_use blocks: "name":"MCPCallTool" → "name":"original"
373
- result = result.replace(new RegExp(`"name"\\s*:\\s*"${mapped}"`, 'g'), `"name":"${original}"`);
374
- }
375
- return result;
376
- }
377
- // Claude Code's field order (from MITM capture). Fields not in this list are appended at end.
378
- const CC_FIELD_ORDER = [
379
- 'model', 'messages', 'system', 'max_tokens', 'thinking', 'output_config',
380
- 'context_management', 'metadata', 'stream', 'tools', 'tool_choice',
381
- ];
382
- function scrubAndReorderFields(body) {
383
- // Remove non-CC fields
384
- for (const field of NON_CC_FIELDS) {
385
- delete body[field];
386
- }
387
- // Rebuild with Claude Code field ordering
388
- const ordered = {};
389
- for (const key of CC_FIELD_ORDER) {
390
- if (key in body) {
391
- ordered[key] = body[key];
392
- delete body[key];
393
- }
394
- }
395
- // Append any remaining fields (custom client fields we don't recognize)
396
- for (const [key, value] of Object.entries(body)) {
397
- ordered[key] = value;
398
- }
399
- return ordered;
400
- }
401
- /**
402
- * Normalize system prompt to exactly 3 blocks.
403
- * Real Claude Code always sends exactly 3 system blocks:
404
- * [0] billing tag (no cache), [1] agent identity (cache 1h), [2] system prompt (cache 1h)
405
- * If the client sends multiple system blocks, merge them into block [2].
406
- */
407
- function normalizeSystemTo3Blocks(system, billingTag, agentIdentity, cache1h) {
408
- let systemText;
409
- if (typeof system === 'string') {
410
- systemText = system;
411
- }
412
- else if (Array.isArray(system)) {
413
- // Merge all text blocks into one, skip any existing billing tags
414
- systemText = system
415
- .filter(b => b.text && !b.text.includes('x-anthropic-billing-header:'))
416
- .map(b => b.text)
417
- .join('\n\n');
418
- }
419
- else {
420
- systemText = '';
421
- }
422
- return [
423
- { type: 'text', text: billingTag },
424
- { type: 'text', text: agentIdentity, cache_control: cache1h },
425
- { type: 'text', text: systemText || 'You are a helpful assistant.', cache_control: cache1h },
426
- ];
427
- }
428
246
  // OpenAI model names → Anthropic (fallback if client sends GPT names)
429
247
  const OPENAI_MODEL_MAP = {
430
248
  'gpt-5.4': 'claude-opus-4-6',
@@ -700,7 +518,7 @@ export async function startProxy(opts = {}) {
700
518
  'accept': 'application/json',
701
519
  'Content-Type': 'application/json',
702
520
  'anthropic-dangerous-direct-browser-access': 'true',
703
- 'user-agent': `claude-cli/${cliVersion} (external, cli)`,
521
+ 'user-agent': `claude-cli/${cliVersion} (external, cli, workload/cron)`,
704
522
  'x-app': 'cli',
705
523
  'x-claude-code-session-id': SESSION_ID,
706
524
  'x-stainless-arch': arch,
@@ -736,7 +554,7 @@ export async function startProxy(opts = {}) {
736
554
  const JSON_HEADERS = { 'Content-Type': 'application/json', ...SECURITY_HEADERS };
737
555
  const MODELS_JSON = JSON.stringify(OPENAI_MODELS_LIST);
738
556
  const ERR_UNAUTH = JSON.stringify({ error: 'Unauthorized', message: 'Invalid or missing API key' });
739
- 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' });
740
558
  const ERR_METHOD = JSON.stringify({ error: 'Method not allowed' });
741
559
  function checkAuth(req) {
742
560
  if (!apiKeyBuf)
@@ -859,7 +677,6 @@ export async function startProxy(opts = {}) {
859
677
  }
860
678
  // Parse body once, apply OpenAI translation, model override, and sanitization
861
679
  let finalBody = body.length > 0 ? body : undefined;
862
- let toolMappings = [];
863
680
  let ccToolMap = null;
864
681
  if (body.length > 0) {
865
682
  try {
@@ -878,7 +695,7 @@ export async function startProxy(opts = {}) {
878
695
  const buildTag = computeBuildTag(userMsg, cliVersion);
879
696
  const cch = computeCch();
880
697
  const fullVersion = `${cliVersion}.${buildTag}`;
881
- const billingTag = `x-anthropic-billing-header: cc_version=${fullVersion}; cc_entrypoint=cli; cch=${cch};`;
698
+ const billingTag = `x-anthropic-billing-header: cc_version=${fullVersion}; cc_entrypoint=cli; cch=${cch}; cc_workload=cron;`;
882
699
  const AGENT_IDENTITY = 'You are a Claude agent, built on Anthropic\'s Claude Agent SDK.';
883
700
  const CACHE_1H = { type: 'ephemeral', ttl: '1h' };
884
701
  const { body: ccBody, toolMap } = buildCCRequest(r, billingTag, AGENT_IDENTITY, CACHE_1H, { deviceId: identity.deviceId, accountUuid: identity.accountUuid, sessionId: SESSION_ID });
@@ -908,7 +725,8 @@ export async function startProxy(opts = {}) {
908
725
  }
909
726
  else {
910
727
  // Claude-optimized: full beta set matching real Claude Code (exact order from MITM capture)
911
- 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';
912
730
  if (clientBeta) {
913
731
  const baseSet = new Set(beta.split(','));
914
732
  const filtered = filterBillableBetas(clientBeta)
@@ -920,7 +738,7 @@ export async function startProxy(opts = {}) {
920
738
  const headers = {
921
739
  ...staticHeaders,
922
740
  'Authorization': `Bearer ${accessToken}`,
923
- 'anthropic-version': req.headers['anthropic-version'] || '2023-06-01',
741
+ 'anthropic-version': passthrough ? (req.headers['anthropic-version'] || '2023-06-01') : '2023-06-01',
924
742
  'anthropic-beta': beta,
925
743
  // Real Claude Code adds x-client-request-id for firstParty + api.anthropic.com
926
744
  'x-client-request-id': randomUUID(),
package/package.json CHANGED
@@ -1,64 +1,64 @@
1
- {
2
- "name": "@askalf/dario",
3
- "version": "3.0.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
  }