@askalf/dario 2.9.5 → 2.11.0

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.
Files changed (2) hide show
  1. package/dist/proxy.js +134 -6
  2. package/package.json +2 -2
package/dist/proxy.js CHANGED
@@ -267,6 +267,112 @@ function stripThinkingFromHistory(body) {
267
267
  * specific order. We rebuild the object to match.
268
268
  */
269
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
+ }
270
376
  // Claude Code's field order (from MITM capture). Fields not in this list are appended at end.
271
377
  const CC_FIELD_ORDER = [
272
378
  'model', 'messages', 'system', 'max_tokens', 'thinking', 'output_config',
@@ -752,6 +858,7 @@ export async function startProxy(opts = {}) {
752
858
  }
753
859
  // Parse body once, apply OpenAI translation, model override, and sanitization
754
860
  let finalBody = body.length > 0 ? body : undefined;
861
+ let toolMappings = [];
755
862
  if (body.length > 0) {
756
863
  try {
757
864
  const parsed = JSON.parse(body.toString());
@@ -766,9 +873,21 @@ export async function startProxy(opts = {}) {
766
873
  // context_management: clear_thinking does NOT reduce input token billing.
767
874
  // Real Claude Code strips thinking before building the next request.
768
875
  stripThinkingFromHistory(r);
769
- // 2. Scrub non-CC fields and normalize field ordering
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
770
890
  const reordered = scrubAndReorderFields(r);
771
- // Copy reordered keys back (r is a reference to result)
772
891
  for (const key of Object.keys(r))
773
892
  delete r[key];
774
893
  Object.assign(r, reordered);
@@ -793,7 +912,8 @@ export async function startProxy(opts = {}) {
793
912
  if (!r.max_tokens || r.max_tokens !== 64000) {
794
913
  r.max_tokens = 64000;
795
914
  }
796
- if (supportsThinking && !r.output_config) {
915
+ // Force effort to medium — CC default. Client 'high' is a fingerprint.
916
+ if (supportsThinking) {
797
917
  r.output_config = { effort: 'medium' };
798
918
  }
799
919
  if (supportsThinking && !r.context_management) {
@@ -941,7 +1061,14 @@ export async function startProxy(opts = {}) {
941
1061
  }
942
1062
  }
943
1063
  else {
944
- res.write(value);
1064
+ // Reverse tool names in streaming chunks
1065
+ if (toolMappings.length > 0) {
1066
+ const text = new TextDecoder().decode(value);
1067
+ res.write(reverseToolNames(text, toolMappings));
1068
+ }
1069
+ else {
1070
+ res.write(value);
1071
+ }
945
1072
  }
946
1073
  }
947
1074
  // Flush remaining buffer
@@ -959,9 +1086,10 @@ export async function startProxy(opts = {}) {
959
1086
  }
960
1087
  else {
961
1088
  // Buffer and forward
962
- const responseBody = await upstream.text();
1089
+ let responseBody = await upstream.text();
1090
+ // Reverse tool name mapping so client sees original names
1091
+ responseBody = reverseToolNames(responseBody, toolMappings);
963
1092
  if (isOpenAI && upstream.status >= 200 && upstream.status < 300) {
964
- // Translate Anthropic response → OpenAI format
965
1093
  try {
966
1094
  const parsed = JSON.parse(responseBody);
967
1095
  res.end(JSON.stringify(anthropicToOpenai(parsed)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "2.9.5",
3
+ "version": "2.11.0",
4
4
  "description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,4 +61,4 @@
61
61
  "tsx": "^4.19.0",
62
62
  "typescript": "^5.7.0"
63
63
  }
64
- }
64
+ }