@askalf/dario 3.9.4 → 3.9.6

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.
@@ -44,6 +44,14 @@ export interface ToolMapping {
44
44
  * into fields CC's schema doesn't carry. Unset in default mode.
45
45
  */
46
46
  clientFields?: string[];
47
+ /**
48
+ * Reverse-lookup priority for resolving collisions when multiple client
49
+ * tools map to the same CC tool. Higher wins. Default 10. Set lower for
50
+ * niche / lossy translations (e.g. OpenClaw's `process` action-discriminator
51
+ * tool loses most of its schema when flattened to Bash, so bash/exec
52
+ * should win the Bash reverse slot when both are declared — dario#37).
53
+ */
54
+ reverseScore?: number;
47
55
  }
48
56
  /**
49
57
  * Request context extracted once per incoming request. Source for
@@ -100,40 +100,58 @@ function injectContextFields(input, clientFields, ctx) {
100
100
  }
101
101
  const TOOL_MAP = {
102
102
  // Direct maps
103
+ // Note on translateBack field names: the vast majority of client bash-like
104
+ // tools use `command` (the Anthropic convention), not `cmd`. OpenClaw's
105
+ // `exec` tool takes `{command, workdir, env, ...}` (dario#36 triage).
106
+ // Hybrid mode overrides these with the actual client schema via clientFields,
107
+ // but default mode relies on these output names being the common case.
103
108
  bash: {
104
109
  ccTool: 'Bash',
105
110
  translateArgs: (a) => ({ command: a.cmd || a.command || a.c || '' }),
106
- translateBack: (a) => ({ cmd: a.command ?? '' }),
111
+ translateBack: (a) => ({ command: a.command ?? '' }),
107
112
  },
108
113
  exec: {
109
114
  ccTool: 'Bash',
110
115
  translateArgs: (a) => ({ command: a.cmd || a.command || a.c || '' }),
111
- translateBack: (a) => ({ cmd: a.command ?? '' }),
116
+ translateBack: (a) => ({ command: a.command ?? '' }),
112
117
  },
113
118
  shell: {
114
119
  ccTool: 'Bash',
115
120
  translateArgs: (a) => ({ command: a.cmd || a.command || a.c || '' }),
116
- translateBack: (a) => ({ cmd: a.command ?? '' }),
121
+ translateBack: (a) => ({ command: a.command ?? '' }),
117
122
  },
118
123
  run: {
119
124
  ccTool: 'Bash',
120
125
  translateArgs: (a) => ({ command: a.cmd || a.command || '' }),
121
- translateBack: (a) => ({ cmd: a.command ?? '' }),
126
+ translateBack: (a) => ({ command: a.command ?? '' }),
122
127
  },
123
128
  command: {
124
129
  ccTool: 'Bash',
125
130
  translateArgs: (a) => ({ command: a.cmd || a.command || '' }),
126
- translateBack: (a) => ({ cmd: a.command ?? '' }),
131
+ translateBack: (a) => ({ command: a.command ?? '' }),
127
132
  },
128
133
  terminal: {
129
134
  ccTool: 'Bash',
130
135
  translateArgs: (a) => ({ command: a.cmd || a.command || '' }),
131
- translateBack: (a) => ({ cmd: a.command ?? '' }),
136
+ translateBack: (a) => ({ command: a.command ?? '' }),
132
137
  },
138
+ // `process` is OpenClaw's session-manager tool — it's an action-discriminator
139
+ // shape {action: "list"|"poll"|"log"|..., sessionId?, ...}. Flattening it onto
140
+ // Bash.command loses all sibling fields (data, keys, hex, literal, text, ...),
141
+ // so the model upstream can't actually drive it. Kept mapped for fingerprint
142
+ // continuity but the reverse translation is inherently lossy — clients with a
143
+ // process-style tool should use --preserve-tools instead of --hybrid-tools.
144
+ //
145
+ // reverseScore: 1 makes sure that when a client declares BOTH `process` AND
146
+ // `exec`/`bash` (OpenClaw does — both are exported from bash-tools.ts), the
147
+ // reverse lookup picks the bash-family mapping for CC's Bash tool slot
148
+ // instead of routing CC tool calls through process's action-based shape
149
+ // and breaking every Bash call with "Unknown action" (dario#37).
133
150
  process: {
134
151
  ccTool: 'Bash',
135
152
  translateArgs: (a) => ({ command: a.action || a.cmd || '' }),
136
153
  translateBack: (a) => ({ action: a.command ?? '' }),
154
+ reverseScore: 1,
137
155
  },
138
156
  read: {
139
157
  ccTool: 'Read',
@@ -307,16 +325,34 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
307
325
  claimedCC.add(mapping.ccTool);
308
326
  }
309
327
  }
328
+ // Unmapped-tool handling differs by mode:
329
+ //
330
+ // - Default mode: round-robin to CC fallback tools. The model sees the CC
331
+ // tool set, any tool call is "something", and we best-effort relay it
332
+ // back to the client tool name. Broken-by-design for clients with rich
333
+ // discriminator tools (OpenClaw lobster/memory_get, dario#36), but
334
+ // preserves the old behavior for simple clients that don't have many
335
+ // unmapped tools.
336
+ //
337
+ // - Hybrid mode: DROP unmapped tools entirely. We can't forward them to
338
+ // the upstream (adding to CC_TOOL_DEFINITIONS breaks the fingerprint),
339
+ // and round-robin mapping produces nonsense shapes on the reverse path
340
+ // (lobster.translateBack(Glob.input) → {pattern: "..."} when lobster
341
+ // wants {action: "run"}). Better to let the model not see those tools
342
+ // than to pretend they exist and corrupt every call. Users needing
343
+ // every client tool to actually work must use --preserve-tools.
310
344
  const CC_FALLBACK_TOOLS = ['Bash', 'Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
311
345
  for (const tool of clientTools) {
312
346
  const name = (tool.name || '').toLowerCase();
313
347
  if (TOOL_MAP[name])
314
348
  continue;
315
349
  unmappedTools.push(tool.name);
316
- // Exclude CC tools the client already uses so we never create a
317
- // two-client-names-to-one-CC-tool collision. If every fallback is
318
- // claimed (rare: client already uses 6+ CC tools), fall back to the
319
- // full pool and accept the ambiguity.
350
+ if (opts.hybridTools)
351
+ continue; // dropped see comment above
352
+ // Default mode: round-robin distribution. Exclude CC tools the client
353
+ // already uses so we never create a two-client-names-to-one-CC-tool
354
+ // collision. If every fallback is claimed (rare: client already uses 6+
355
+ // CC tools), fall back to the full pool and accept the ambiguity.
320
356
  const pool = CC_FALLBACK_TOOLS.filter(t => !claimedCC.has(t));
321
357
  const fallbackPool = pool.length > 0 ? pool : CC_FALLBACK_TOOLS;
322
358
  const fallbackTool = fallbackPool[(unmappedTools.length - 1) % fallbackPool.length];
@@ -470,11 +506,22 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
470
506
  }
471
507
  /**
472
508
  * Build the CC-name → {clientName, mapping} reverse lookup used by both
473
- * the non-streaming and streaming reverse-mappers. Two-pass construction
474
- * preserves the original identity-protection rule: when a client sent a
475
- * tool with the literal CC name (e.g. `WebSearch`), that pairing claims
476
- * the CC slot first so a later unmapped-tool fallback that also lands
477
- * on `WebSearch` can't overwrite it.
509
+ * the non-streaming and streaming reverse-mappers.
510
+ *
511
+ * Two-pass construction preserves the original identity-protection rule:
512
+ * when a client sent a tool with the literal CC name (e.g. `WebSearch`),
513
+ * that pairing claims the CC slot first so a later unmapped-tool fallback
514
+ * that also lands on `WebSearch` can't overwrite it.
515
+ *
516
+ * Within the non-identity pass, collisions are broken by `reverseScore`
517
+ * (higher wins, default 10). This matters when a client declares two
518
+ * tools that both map to the same CC tool — OpenClaw declares both
519
+ * `exec` (bash-like, score 10) and `process` (action-discriminator,
520
+ * score 1) and both map to Bash. Pre-fix, insertion-order last-wins
521
+ * routed Bash tool calls through `process`, which interpreted the
522
+ * command string as an action and returned "Unknown action" for
523
+ * every call. `process` now has reverseScore: 1 so bash/exec wins
524
+ * (dario#37).
478
525
  */
479
526
  function buildReverseLookup(toolMap) {
480
527
  const reverseMap = new Map();
@@ -485,12 +532,17 @@ function buildReverseLookup(toolMap) {
485
532
  reverseMap.set(mapping.ccTool, { clientName, mapping });
486
533
  }
487
534
  }
535
+ // Score-based collision resolution in the non-identity pass.
536
+ const scoreOf = (m) => m.reverseScore ?? 10;
488
537
  for (const [clientName, mapping] of toolMap) {
489
538
  if (clientName.toLowerCase() === mapping.ccTool.toLowerCase())
490
539
  continue;
491
540
  if (identityClaimed.has(mapping.ccTool))
492
541
  continue;
493
- reverseMap.set(mapping.ccTool, { clientName, mapping });
542
+ const existing = reverseMap.get(mapping.ccTool);
543
+ if (!existing || scoreOf(mapping) > scoreOf(existing.mapping)) {
544
+ reverseMap.set(mapping.ccTool, { clientName, mapping });
545
+ }
494
546
  }
495
547
  return reverseMap;
496
548
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.9.4",
3
+ "version": "3.9.6",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {