@askalf/dario 3.8.0 → 3.9.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.
- package/README.md +48 -1
- package/dist/cc-template.d.ts +22 -2
- package/dist/cc-template.js +77 -9
- package/dist/cli.js +10 -1
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +20 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -245,7 +245,8 @@ curl http://localhost:3456/analytics # per-account / per-model stats, burn ra
|
|
|
245
245
|
| Flag / env | Description | Default |
|
|
246
246
|
|---|---|---|
|
|
247
247
|
| `--passthrough` / `--thin` | Thin proxy for the Claude backend — OAuth swap only, no template injection | off |
|
|
248
|
-
| `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC tools (
|
|
248
|
+
| `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC's `Bash/Read/Grep/Glob/WebSearch/WebFetch`. Required for clients whose tools have fields CC doesn't (`sessionId`, custom ids, etc.) — see [Custom tool schemas](#custom-tool-schemas). Trade-off: drops the CC request fingerprint. | off |
|
|
249
|
+
| `--hybrid-tools` / `--context-inject` | Remap to CC tools **and** inject request-context values (`sessionId`, `requestId`, `channelId`, `userId`, `timestamp`) into client-declared fields CC's schema doesn't carry. Preserves the CC fingerprint while keeping custom schemas functional — see [Hybrid tool mode](#hybrid-tool-mode). Mutually exclusive with `--preserve-tools`. | off |
|
|
249
250
|
| `--model=<name>` | Force a model (`opus`, `sonnet`, `haiku`, or full ID). Applies to the Claude backend. | passthrough |
|
|
250
251
|
| `--port=<n>` | Port to listen on | `3456` |
|
|
251
252
|
| `--host=<addr>` / `DARIO_HOST` | Bind address. Use `0.0.0.0` for LAN, or a specific IP (e.g. a Tailscale interface). When non-loopback, also set `DARIO_API_KEY`. | `127.0.0.1` |
|
|
@@ -347,6 +348,50 @@ curl http://localhost:3456/v1/chat/completions \
|
|
|
347
348
|
|
|
348
349
|
All supported. Claude backend: full Anthropic SSE format plus OpenAI-SSE translation for tool_use streaming. OpenAI-compat backend: streaming body forwarded byte-for-byte.
|
|
349
350
|
|
|
351
|
+
### Custom tool schemas
|
|
352
|
+
|
|
353
|
+
By default, on the Claude backend, dario replaces your client's tool definitions with the real Claude Code tools (`Bash`, `Read`, `Grep`, `Glob`, `WebSearch`, `WebFetch`) and translates parameters back and forth. That's how dario looks like CC on the wire, which is what lets your request bill against your Claude subscription instead of API pricing.
|
|
354
|
+
|
|
355
|
+
The trade-off: if your client's tools carry fields CC's schema doesn't have — a `sessionId`, a custom request id, a channel-bound context token, anything — those fields don't survive the round trip. The model only ever sees `Bash({command})`, responds with `Bash({command})`, and dario's reverse map rebuilds your tool call without the fields the model never saw. Your validator then rejects the call for a missing required field.
|
|
356
|
+
|
|
357
|
+
Symptom: your tool calls come back looking stripped-down, or your runtime complains about a required field being absent *only when routed through dario's Claude backend*, while the same tools work fine against a direct API key or the OpenAI-compat backend.
|
|
358
|
+
|
|
359
|
+
Fix: run dario with `--preserve-tools` (or `--keep-tools`). That skips the CC tool remap entirely, passes your client's tool definitions through to the model unchanged, and lets the model populate every field your schema expects.
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
dario proxy --preserve-tools
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The cost: requests no longer look like CC on the wire, so the CC subscription fingerprint is gone. On a Max/Pro plan, that means the request may be counted against your API usage rather than your subscription quota. If you're on API-key billing already, `--preserve-tools` is free; if you're using dario specifically to route against a subscription, the [hybrid tool mode](#hybrid-tool-mode) below is the compromise that keeps both.
|
|
366
|
+
|
|
367
|
+
The openai-compat backend (OpenRouter, OpenAI, Groq, local LiteLLM, etc.) is unaffected — it forwards tool definitions byte-for-byte and doesn't need this flag.
|
|
368
|
+
|
|
369
|
+
### Hybrid tool mode
|
|
370
|
+
|
|
371
|
+
For the very common case where the "missing" fields on your client's tool are **request context** — `sessionId`, `requestId`, `channelId`, `userId`, `timestamp` — dario can remap to CC tools *and* inject those values on the reverse path. The fingerprint stays intact, the model still sees only CC's tools (so subscription billing still routes), and your validator still sees the fields it requires because dario fills them from request headers on the way back.
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
dario proxy --hybrid-tools
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**How it works.** On each request, dario builds a `RequestContext` from headers (`x-session-id`, `x-request-id`, `x-channel-id`, `x-user-id`) plus its own generated ids and the current timestamp. After `translateBack` produces the client-shaped tool call on the response path, any field declared on the client's tool schema whose name matches a known context field (`sessionId`/`session_id`, `requestId`/`request_id`, `channelId`/`channel_id`, `userId`/`user_id`, `timestamp`/`created_at`/`createdAt`) and isn't already populated gets filled from the context. Fields the model genuinely populated via `translateBack` are never overwritten.
|
|
378
|
+
|
|
379
|
+
**When to use which flag.**
|
|
380
|
+
|
|
381
|
+
| Your situation | Flag | Why |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| Your custom fields are request context (session/request/channel/user ids, timestamps) | `--hybrid-tools` | Keeps the CC fingerprint *and* your validator is satisfied. |
|
|
384
|
+
| Your custom fields need the model's reasoning (e.g. `confidence`, `reasoning_trace`, `tool_selection_rationale`) | `--preserve-tools` | The model has to see the real schema to populate these. Accept the fingerprint loss. |
|
|
385
|
+
| Your client's tools are already a subset of CC's `Bash/Read/Grep/Glob/WebSearch/WebFetch` | *(neither)* | Default mode works as-is. |
|
|
386
|
+
|
|
387
|
+
**Limitations of hybrid mode.**
|
|
388
|
+
|
|
389
|
+
- Top-level fields only. If your custom field is nested (e.g. `meta: {sessionId: ...}`), v1 doesn't reach into the nested object. Tracked in [#33](https://github.com/askalf/dario/issues/33).
|
|
390
|
+
- The field-to-context mapping is a fixed list. If you need arbitrary fields (e.g. an internal `tenant_id`) pulled from headers, file an issue and we'll extend the map.
|
|
391
|
+
- No type coercion beyond string. If your schema requires a numeric `sessionId`, dario sends the string it got from headers — override at your client level or use `--preserve-tools`.
|
|
392
|
+
|
|
393
|
+
Hybrid mode was built to resolve [#29](https://github.com/askalf/dario/issues/29) cleanly for OpenClaw-style agents whose `process` tool declares `sessionId`, after the full provider-comparison diagnostic from [@boeingchoco](https://github.com/boeingchoco) made clear that the problem wasn't fixable in the translation layer alone.
|
|
394
|
+
|
|
350
395
|
### Library mode
|
|
351
396
|
|
|
352
397
|
```typescript
|
|
@@ -530,6 +575,8 @@ npm run dev # runs with tsx, no build step
|
|
|
530
575
|
| [@GodsBoy](https://github.com/GodsBoy) | Proxy authentication, token redaction, error sanitization ([#2](https://github.com/askalf/dario/pull/2)) |
|
|
531
576
|
| [@belangertrading](https://github.com/belangertrading) | Billing classification investigation ([#4](https://github.com/askalf/dario/issues/4)), cache_control fingerprinting ([#6](https://github.com/askalf/dario/issues/6)), billing reclassification root cause ([#7](https://github.com/askalf/dario/issues/7)), OAuth client_id discovery ([#12](https://github.com/askalf/dario/issues/12)), multi-agent session-level billing analysis ([#23](https://github.com/askalf/dario/issues/23)) |
|
|
532
577
|
| [@nathan-widjaja](https://github.com/nathan-widjaja) | README positioning rewrite structure ([#21](https://github.com/askalf/dario/issues/21)) |
|
|
578
|
+
| [@iNicholasBE](https://github.com/iNicholasBE) | macOS keychain credential detection ([#30](https://github.com/askalf/dario/pull/30)) |
|
|
579
|
+
| [@boeingchoco](https://github.com/boeingchoco) | Reverse-direction tool parameter translation ([#29](https://github.com/askalf/dario/issues/29)), SSE event-group framing regression catch (v3.7.1), provider-comparison diagnostic that surfaced the `--preserve-tools` discoverability gap (v3.8.1), and the motivating case for hybrid tool mode ([#33](https://github.com/askalf/dario/issues/33), v3.9.0) |
|
|
533
580
|
|
|
534
581
|
---
|
|
535
582
|
|
package/dist/cc-template.d.ts
CHANGED
|
@@ -37,6 +37,25 @@ export interface ToolMapping {
|
|
|
37
37
|
ccTool: string;
|
|
38
38
|
translateArgs?: (args: Record<string, unknown>) => Record<string, unknown>;
|
|
39
39
|
translateBack?: (args: Record<string, unknown>) => Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* Top-level field names the client's original tool schema declared.
|
|
42
|
+
* Populated only in hybrid mode (`hybridTools: true`) so the reverse
|
|
43
|
+
* path can inject request-context values (sessionId, requestId, …)
|
|
44
|
+
* into fields CC's schema doesn't carry. Unset in default mode.
|
|
45
|
+
*/
|
|
46
|
+
clientFields?: string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Request context extracted once per incoming request. Source for
|
|
50
|
+
* hybrid-mode field injection — fields declared on the client's tool
|
|
51
|
+
* but not on CC's get filled from here on the reverse path.
|
|
52
|
+
*/
|
|
53
|
+
export interface RequestContext {
|
|
54
|
+
sessionId: string;
|
|
55
|
+
requestId: string;
|
|
56
|
+
channelId?: string;
|
|
57
|
+
userId?: string;
|
|
58
|
+
timestamp: string;
|
|
40
59
|
}
|
|
41
60
|
/**
|
|
42
61
|
* Build a CC-template request from a client request.
|
|
@@ -52,6 +71,7 @@ export declare function buildCCRequest(clientBody: Record<string, unknown>, bill
|
|
|
52
71
|
sessionId: string;
|
|
53
72
|
}, opts?: {
|
|
54
73
|
preserveTools?: boolean;
|
|
74
|
+
hybridTools?: boolean;
|
|
55
75
|
}): {
|
|
56
76
|
body: Record<string, unknown>;
|
|
57
77
|
toolMap: Map<string, ToolMapping>;
|
|
@@ -64,7 +84,7 @@ export declare function buildCCRequest(clientBody: Record<string, unknown>, bill
|
|
|
64
84
|
* body isn't valid JSON (e.g. an error response, a partial chunk),
|
|
65
85
|
* returns it unchanged.
|
|
66
86
|
*/
|
|
67
|
-
export declare function reverseMapResponse(responseBody: string, toolMap: Map<string, ToolMapping
|
|
87
|
+
export declare function reverseMapResponse(responseBody: string, toolMap: Map<string, ToolMapping>, ctx?: RequestContext): string;
|
|
68
88
|
/**
|
|
69
89
|
* Streaming reverse-mapper for SSE responses.
|
|
70
90
|
*
|
|
@@ -111,4 +131,4 @@ export interface StreamingReverseMapper {
|
|
|
111
131
|
feed(chunk: Uint8Array): Uint8Array;
|
|
112
132
|
end(): Uint8Array;
|
|
113
133
|
}
|
|
114
|
-
export declare function createStreamingReverseMapper(toolMap: Map<string, ToolMapping
|
|
134
|
+
export declare function createStreamingReverseMapper(toolMap: Map<string, ToolMapping>, ctx?: RequestContext): StreamingReverseMapper;
|
package/dist/cc-template.js
CHANGED
|
@@ -39,6 +39,49 @@ export function scrubFrameworkIdentifiers(text) {
|
|
|
39
39
|
}
|
|
40
40
|
return result;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Map from client-declared field name (lowercase) to the RequestContext
|
|
44
|
+
* key that supplies its value. A field declared on the client's tool
|
|
45
|
+
* whose name matches one of these gets auto-filled in hybrid mode.
|
|
46
|
+
*
|
|
47
|
+
* Case-insensitive match on the client's declared field name. Both
|
|
48
|
+
* snake_case and camelCase variants map to the same source.
|
|
49
|
+
*/
|
|
50
|
+
const CONTEXT_FIELD_SOURCES = {
|
|
51
|
+
sessionid: 'sessionId',
|
|
52
|
+
session_id: 'sessionId',
|
|
53
|
+
requestid: 'requestId',
|
|
54
|
+
request_id: 'requestId',
|
|
55
|
+
channelid: 'channelId',
|
|
56
|
+
channel_id: 'channelId',
|
|
57
|
+
userid: 'userId',
|
|
58
|
+
user_id: 'userId',
|
|
59
|
+
timestamp: 'timestamp',
|
|
60
|
+
createdat: 'timestamp',
|
|
61
|
+
created_at: 'timestamp',
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Fill in fields declared on the client's tool schema that are still
|
|
65
|
+
* absent from the translated input, drawing values from the request
|
|
66
|
+
* context. Only runs when a mapping has `clientFields` populated
|
|
67
|
+
* (hybrid mode) and an input object is present. Fields already set
|
|
68
|
+
* by `translateBack` are never overwritten.
|
|
69
|
+
*/
|
|
70
|
+
function injectContextFields(input, clientFields, ctx) {
|
|
71
|
+
if (!clientFields || !ctx)
|
|
72
|
+
return input;
|
|
73
|
+
for (const field of clientFields) {
|
|
74
|
+
if (field in input && input[field] !== undefined && input[field] !== null && input[field] !== '')
|
|
75
|
+
continue;
|
|
76
|
+
const sourceKey = CONTEXT_FIELD_SOURCES[field.toLowerCase()];
|
|
77
|
+
if (!sourceKey)
|
|
78
|
+
continue;
|
|
79
|
+
const value = ctx[sourceKey];
|
|
80
|
+
if (value !== undefined)
|
|
81
|
+
input[field] = value;
|
|
82
|
+
}
|
|
83
|
+
return input;
|
|
84
|
+
}
|
|
42
85
|
const TOOL_MAP = {
|
|
43
86
|
// Direct maps
|
|
44
87
|
bash: {
|
|
@@ -233,7 +276,18 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
233
276
|
const name = (tool.name || '').toLowerCase();
|
|
234
277
|
const mapping = TOOL_MAP[name];
|
|
235
278
|
if (mapping) {
|
|
236
|
-
|
|
279
|
+
// In hybrid mode, clone the shared mapping and attach the
|
|
280
|
+
// client-declared top-level field names from input_schema.
|
|
281
|
+
// The reverse path uses these to inject request-context values
|
|
282
|
+
// into fields CC's schema doesn't carry.
|
|
283
|
+
if (opts.hybridTools) {
|
|
284
|
+
const schema = tool.input_schema;
|
|
285
|
+
const fields = schema?.properties ? Object.keys(schema.properties) : [];
|
|
286
|
+
activeToolMap.set(tool.name, { ...mapping, clientFields: fields });
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
activeToolMap.set(tool.name, mapping);
|
|
290
|
+
}
|
|
237
291
|
claimedCC.add(mapping.ccTool);
|
|
238
292
|
}
|
|
239
293
|
}
|
|
@@ -435,7 +489,7 @@ function buildReverseLookup(toolMap) {
|
|
|
435
489
|
* leaving the input shape in CC's parameter names which the client's
|
|
436
490
|
* own validator would reject.
|
|
437
491
|
*/
|
|
438
|
-
function rewriteToolUseBlock(block, reverseMap) {
|
|
492
|
+
function rewriteToolUseBlock(block, reverseMap, ctx) {
|
|
439
493
|
const ccName = block.name;
|
|
440
494
|
if (typeof ccName !== 'string')
|
|
441
495
|
return;
|
|
@@ -453,6 +507,13 @@ function rewriteToolUseBlock(block, reverseMap) {
|
|
|
453
507
|
// the same broken input it would have seen pre-v3.7.0.
|
|
454
508
|
}
|
|
455
509
|
}
|
|
510
|
+
// Hybrid mode: inject request-context values into any client-declared
|
|
511
|
+
// fields still missing after translateBack. No-op unless the mapping
|
|
512
|
+
// was built with `clientFields` populated (hybridTools: true) and a
|
|
513
|
+
// context was passed in.
|
|
514
|
+
if (entry.mapping.clientFields && block.input && typeof block.input === 'object') {
|
|
515
|
+
injectContextFields(block.input, entry.mapping.clientFields, ctx);
|
|
516
|
+
}
|
|
456
517
|
}
|
|
457
518
|
/**
|
|
458
519
|
* Reverse-map CC tool calls in a non-streaming response back to the
|
|
@@ -461,7 +522,7 @@ function rewriteToolUseBlock(block, reverseMap) {
|
|
|
461
522
|
* body isn't valid JSON (e.g. an error response, a partial chunk),
|
|
462
523
|
* returns it unchanged.
|
|
463
524
|
*/
|
|
464
|
-
export function reverseMapResponse(responseBody, toolMap) {
|
|
525
|
+
export function reverseMapResponse(responseBody, toolMap, ctx) {
|
|
465
526
|
if (toolMap.size === 0)
|
|
466
527
|
return responseBody;
|
|
467
528
|
const reverseMap = buildReverseLookup(toolMap);
|
|
@@ -477,12 +538,12 @@ export function reverseMapResponse(responseBody, toolMap) {
|
|
|
477
538
|
return responseBody;
|
|
478
539
|
for (const block of content) {
|
|
479
540
|
if (block && typeof block === 'object' && block.type === 'tool_use') {
|
|
480
|
-
rewriteToolUseBlock(block, reverseMap);
|
|
541
|
+
rewriteToolUseBlock(block, reverseMap, ctx);
|
|
481
542
|
}
|
|
482
543
|
}
|
|
483
544
|
return JSON.stringify(parsed);
|
|
484
545
|
}
|
|
485
|
-
export function createStreamingReverseMapper(toolMap) {
|
|
546
|
+
export function createStreamingReverseMapper(toolMap, ctx) {
|
|
486
547
|
const noop = {
|
|
487
548
|
feed: (chunk) => chunk,
|
|
488
549
|
end: () => new Uint8Array(0),
|
|
@@ -490,11 +551,13 @@ export function createStreamingReverseMapper(toolMap) {
|
|
|
490
551
|
if (toolMap.size === 0)
|
|
491
552
|
return noop;
|
|
492
553
|
const reverseMap = buildReverseLookup(toolMap);
|
|
493
|
-
// If no mapping needs translation, fall back to
|
|
494
|
-
// so we don't pay the SSE-parsing cost on every chunk.
|
|
554
|
+
// If no mapping needs translation OR context injection, fall back to
|
|
555
|
+
// identity behavior so we don't pay the SSE-parsing cost on every chunk.
|
|
556
|
+
// Hybrid mode with clientFields always needs the streaming path so the
|
|
557
|
+
// injection can run at content_block_stop.
|
|
495
558
|
let anyNeedsTranslation = false;
|
|
496
559
|
for (const { mapping } of reverseMap.values()) {
|
|
497
|
-
if (mapping.translateBack) {
|
|
560
|
+
if (mapping.translateBack || (mapping.clientFields && mapping.clientFields.length > 0)) {
|
|
498
561
|
anyNeedsTranslation = true;
|
|
499
562
|
break;
|
|
500
563
|
}
|
|
@@ -568,7 +631,9 @@ export function createStreamingReverseMapper(toolMap) {
|
|
|
568
631
|
const block = event.content_block;
|
|
569
632
|
if (block && block.type === 'tool_use' && typeof block.name === 'string') {
|
|
570
633
|
const entry = reverseMap.get(block.name);
|
|
571
|
-
|
|
634
|
+
const needsBuffering = entry && idx >= 0 && (entry.mapping.translateBack ||
|
|
635
|
+
(entry.mapping.clientFields && entry.mapping.clientFields.length > 0));
|
|
636
|
+
if (entry && needsBuffering) {
|
|
572
637
|
// Stash the block so we can flush a translated version at
|
|
573
638
|
// content_block_stop. Emit a rewritten start event now so
|
|
574
639
|
// the client sees its own tool name immediately.
|
|
@@ -619,6 +684,9 @@ export function createStreamingReverseMapper(toolMap) {
|
|
|
619
684
|
translatedInput = buf.mapping.translateBack
|
|
620
685
|
? buf.mapping.translateBack(parsedInput)
|
|
621
686
|
: parsedInput;
|
|
687
|
+
if (buf.mapping.clientFields && buf.mapping.clientFields.length > 0) {
|
|
688
|
+
injectContextFields(translatedInput, buf.mapping.clientFields, ctx);
|
|
689
|
+
}
|
|
622
690
|
}
|
|
623
691
|
catch {
|
|
624
692
|
parseOk = false;
|
package/dist/cli.js
CHANGED
|
@@ -140,9 +140,14 @@ async function proxy() {
|
|
|
140
140
|
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
141
141
|
const passthrough = args.includes('--passthrough') || args.includes('--thin');
|
|
142
142
|
const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
|
|
143
|
+
const hybridTools = args.includes('--hybrid-tools') || args.includes('--context-inject');
|
|
144
|
+
if (preserveTools && hybridTools) {
|
|
145
|
+
console.error('[dario] --preserve-tools and --hybrid-tools are mutually exclusive. Pick one.');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
143
148
|
const modelArg = args.find(a => a.startsWith('--model='));
|
|
144
149
|
const model = modelArg ? modelArg.split('=')[1] : undefined;
|
|
145
|
-
await startProxy({ port, host, verbose, model, passthrough, preserveTools });
|
|
150
|
+
await startProxy({ port, host, verbose, model, passthrough, preserveTools, hybridTools });
|
|
146
151
|
}
|
|
147
152
|
async function accounts() {
|
|
148
153
|
const sub = args[1];
|
|
@@ -369,6 +374,10 @@ async function help() {
|
|
|
369
374
|
Default: passthrough (client decides)
|
|
370
375
|
--passthrough, --thin Thin proxy — OAuth swap only, no injection
|
|
371
376
|
--preserve-tools Keep client tool schemas (for agents with custom tools)
|
|
377
|
+
--hybrid-tools Remap to CC tools AND inject request-context fields
|
|
378
|
+
(sessionId, requestId, channelId, userId, timestamp)
|
|
379
|
+
declared on the client's tool schema but missing
|
|
380
|
+
from CC's. Preserves CC fingerprint. See #33.
|
|
372
381
|
--port=PORT Port to listen on (default: 3456)
|
|
373
382
|
--host=ADDRESS Address to bind to (default: 127.0.0.1)
|
|
374
383
|
Use 0.0.0.0 to accept connections from other machines.
|
package/dist/proxy.d.ts
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -626,6 +626,20 @@ export async function startProxy(opts = {}) {
|
|
|
626
626
|
let finalBody = body.length > 0 ? body : undefined;
|
|
627
627
|
let ccToolMap = null;
|
|
628
628
|
let requestModel = '';
|
|
629
|
+
// Request context for hybrid-mode field injection (#33). Built once
|
|
630
|
+
// per request from incoming headers so the reverse mapper can fill
|
|
631
|
+
// client-declared fields like `sessionId` that CC's schema doesn't
|
|
632
|
+
// carry. Undefined when hybridTools is off — the reverse path then
|
|
633
|
+
// skips injection entirely.
|
|
634
|
+
const reqCtx = opts.hybridTools ? {
|
|
635
|
+
sessionId: req.headers['x-session-id']
|
|
636
|
+
?? req.headers['x-client-session-id']
|
|
637
|
+
?? SESSION_ID,
|
|
638
|
+
requestId: req.headers['x-request-id'] ?? randomUUID(),
|
|
639
|
+
channelId: req.headers['x-channel-id'],
|
|
640
|
+
userId: req.headers['x-user-id'],
|
|
641
|
+
timestamp: new Date().toISOString(),
|
|
642
|
+
} : undefined;
|
|
629
643
|
if (body.length > 0) {
|
|
630
644
|
try {
|
|
631
645
|
const parsed = JSON.parse(body.toString());
|
|
@@ -649,7 +663,10 @@ export async function startProxy(opts = {}) {
|
|
|
649
663
|
const bodyIdentity = poolAccount
|
|
650
664
|
? poolAccount.identity
|
|
651
665
|
: { deviceId: identity.deviceId, accountUuid: identity.accountUuid, sessionId: SESSION_ID };
|
|
652
|
-
const { body: ccBody, toolMap } = buildCCRequest(r, billingTag, CACHE_1H, bodyIdentity, {
|
|
666
|
+
const { body: ccBody, toolMap } = buildCCRequest(r, billingTag, CACHE_1H, bodyIdentity, {
|
|
667
|
+
preserveTools: opts.preserveTools ?? false,
|
|
668
|
+
hybridTools: opts.hybridTools ?? false,
|
|
669
|
+
});
|
|
653
670
|
// Store tool map for response reverse-mapping
|
|
654
671
|
ccToolMap = toolMap;
|
|
655
672
|
// Replace request body entirely with CC template
|
|
@@ -944,7 +961,7 @@ export async function startProxy(opts = {}) {
|
|
|
944
961
|
// path; the non-streaming reverseMapResponse covers buffered
|
|
945
962
|
// responses below.
|
|
946
963
|
const streamMapper = ccToolMap && !isOpenAI
|
|
947
|
-
? createStreamingReverseMapper(ccToolMap)
|
|
964
|
+
? createStreamingReverseMapper(ccToolMap, reqCtx)
|
|
948
965
|
: null;
|
|
949
966
|
try {
|
|
950
967
|
let buffer = '';
|
|
@@ -1039,7 +1056,7 @@ export async function startProxy(opts = {}) {
|
|
|
1039
1056
|
let responseBody = await upstream.text();
|
|
1040
1057
|
// Reverse tool name mapping so client sees original names
|
|
1041
1058
|
if (ccToolMap)
|
|
1042
|
-
responseBody = reverseMapResponse(responseBody, ccToolMap);
|
|
1059
|
+
responseBody = reverseMapResponse(responseBody, ccToolMap, reqCtx);
|
|
1043
1060
|
if (isOpenAI && upstream.status >= 200 && upstream.status < 300) {
|
|
1044
1061
|
try {
|
|
1045
1062
|
const parsed = JSON.parse(responseBody);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
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": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc && cp src/cc-template-data.json dist/",
|
|
24
|
-
"test": "node test/issue-29-tool-translation.mjs && node test/analytics-recording.mjs && node test/failover-429.mjs",
|
|
24
|
+
"test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/analytics-recording.mjs && node test/failover-429.mjs",
|
|
25
25
|
"audit": "npm audit --production --audit-level=high",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"start": "node dist/cli.js",
|