@askalf/dario 4.8.120 → 4.8.121
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.d.ts +8 -0
- package/dist/proxy.js +76 -43
- package/package.json +1 -1
package/dist/proxy.d.ts
CHANGED
|
@@ -174,6 +174,14 @@ export declare function buildOrchestrationPatterns(preserveTags?: Set<string>):
|
|
|
174
174
|
* opt any tag out of the scrub. dario#78.
|
|
175
175
|
*/
|
|
176
176
|
export declare function sanitizeMessages(body: Record<string, unknown>, preserveTags?: Set<string>): void;
|
|
177
|
+
/**
|
|
178
|
+
* Translate Anthropic SSE → OpenAI SSE. Returns a stateful per-CALL closure so
|
|
179
|
+
* concurrent /v1/chat/completions streams don't share tool-call index/id state
|
|
180
|
+
* (#642-audit: the previous module-global counters cross-contaminated
|
|
181
|
+
* interleaved streams and emitted malformed tool_calls deltas). One translator
|
|
182
|
+
* per request, mirroring createStreamingReverseMapper.
|
|
183
|
+
*/
|
|
184
|
+
export declare function createOpenAIStreamTranslator(): (line: string) => string | null;
|
|
177
185
|
export declare const OPENAI_MODELS_LIST: {
|
|
178
186
|
object: string;
|
|
179
187
|
data: Array<{
|
package/dist/proxy.js
CHANGED
|
@@ -544,48 +544,55 @@ function anthropicToOpenai(body) {
|
|
|
544
544
|
usage: { prompt_tokens: u?.input_tokens ?? 0, completion_tokens: u?.output_tokens ?? 0, total_tokens: (u?.input_tokens ?? 0) + (u?.output_tokens ?? 0) },
|
|
545
545
|
};
|
|
546
546
|
}
|
|
547
|
-
/**
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Translate Anthropic SSE → OpenAI SSE. Returns a stateful per-CALL closure so
|
|
549
|
+
* concurrent /v1/chat/completions streams don't share tool-call index/id state
|
|
550
|
+
* (#642-audit: the previous module-global counters cross-contaminated
|
|
551
|
+
* interleaved streams and emitted malformed tool_calls deltas). One translator
|
|
552
|
+
* per request, mirroring createStreamingReverseMapper.
|
|
553
|
+
*/
|
|
554
|
+
export function createOpenAIStreamTranslator() {
|
|
555
|
+
let toolIndex = 0;
|
|
556
|
+
let toolId = '';
|
|
557
|
+
return function translateStreamChunk(line) {
|
|
558
|
+
if (!line.startsWith('data: '))
|
|
559
|
+
return null;
|
|
560
|
+
const json = line.slice(6).trim();
|
|
561
|
+
if (json === '[DONE]')
|
|
562
|
+
return 'data: [DONE]\n\n';
|
|
563
|
+
try {
|
|
564
|
+
const e = JSON.parse(json);
|
|
565
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
566
|
+
if (e.type === 'content_block_start') {
|
|
567
|
+
const block = e.content_block;
|
|
568
|
+
if (block?.type === 'tool_use' && block.name) {
|
|
569
|
+
toolId = block.id ?? `call_${toolIndex}`;
|
|
570
|
+
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: { tool_calls: [{ index: toolIndex, id: toolId, type: 'function', function: { name: block.name, arguments: '' } }] }, finish_reason: null }] })}\n\n`;
|
|
571
|
+
}
|
|
565
572
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
573
|
+
if (e.type === 'content_block_delta') {
|
|
574
|
+
const d = e.delta;
|
|
575
|
+
if (d?.type === 'text_delta' && d.text)
|
|
576
|
+
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: { content: d.text }, finish_reason: null }] })}\n\n`;
|
|
577
|
+
if (d?.type === 'input_json_delta' && d.partial_json)
|
|
578
|
+
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: { tool_calls: [{ index: toolIndex, function: { arguments: d.partial_json } }] }, finish_reason: null }] })}\n\n`;
|
|
579
|
+
}
|
|
580
|
+
if (e.type === 'content_block_stop') {
|
|
581
|
+
if (toolId) {
|
|
582
|
+
toolIndex++;
|
|
583
|
+
toolId = '';
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
if (e.type === 'message_stop') {
|
|
588
|
+
toolIndex = 0;
|
|
589
|
+
toolId = '';
|
|
590
|
+
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })}\n\ndata: [DONE]\n\n`;
|
|
578
591
|
}
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
if (e.type === 'message_stop') {
|
|
582
|
-
_streamToolIndex = 0;
|
|
583
|
-
_streamToolId = '';
|
|
584
|
-
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })}\n\ndata: [DONE]\n\n`;
|
|
585
592
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
593
|
+
catch { }
|
|
594
|
+
return null;
|
|
595
|
+
};
|
|
589
596
|
}
|
|
590
597
|
// Baked /v1/models payload — what the proxy advertises before (or without)
|
|
591
598
|
// a successful upstream catalog fetch. The live route serves the
|
|
@@ -2779,14 +2786,29 @@ export async function startProxy(opts = {}) {
|
|
|
2779
2786
|
const streamMapper = ccToolMap && !isOpenAI
|
|
2780
2787
|
? createStreamingReverseMapper(ccToolMap, reqCtx)
|
|
2781
2788
|
: null;
|
|
2789
|
+
// Per-request OpenAI SSE translator — isolated tool-call state so
|
|
2790
|
+
// concurrent /v1/chat/completions streams don't cross-contaminate (#642-audit).
|
|
2791
|
+
const openaiTranslate = isOpenAI ? createOpenAIStreamTranslator() : null;
|
|
2782
2792
|
// Gated writer — a no-op once the downstream client has gone away
|
|
2783
2793
|
// in drain-on-close mode. The read loop keeps consuming so the
|
|
2784
2794
|
// upstream sees a full-length read; writes to a closed socket are
|
|
2785
2795
|
// suppressed to avoid EPIPE/warnings and pointless work.
|
|
2796
|
+
// Honor downstream backpressure (#642-audit): when res.write() returns
|
|
2797
|
+
// false the client's socket buffer is full, so pause the read loop until
|
|
2798
|
+
// it drains instead of buffering the whole (fast) upstream in memory.
|
|
2799
|
+
let needsDrain = false;
|
|
2786
2800
|
const writeToClient = (chunk) => {
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2801
|
+
if (clientDisconnected)
|
|
2802
|
+
return;
|
|
2803
|
+
if (res.write(chunk) === false)
|
|
2804
|
+
needsDrain = true;
|
|
2789
2805
|
};
|
|
2806
|
+
// Resolves on 'close' too, so a vanished client can't wedge the loop.
|
|
2807
|
+
const waitForDrain = () => new Promise((resolve) => {
|
|
2808
|
+
const done = () => { res.off('drain', done); res.off('close', done); resolve(); };
|
|
2809
|
+
res.once('drain', done);
|
|
2810
|
+
res.once('close', done);
|
|
2811
|
+
});
|
|
2790
2812
|
try {
|
|
2791
2813
|
let buffer = '';
|
|
2792
2814
|
const MAX_LINE_LENGTH = 1_000_000; // 1MB max per SSE line
|
|
@@ -2860,7 +2882,7 @@ export async function startProxy(opts = {}) {
|
|
|
2860
2882
|
const lines = buffer.split('\n');
|
|
2861
2883
|
buffer = lines.pop() ?? '';
|
|
2862
2884
|
for (const line of lines) {
|
|
2863
|
-
const translated =
|
|
2885
|
+
const translated = openaiTranslate(line);
|
|
2864
2886
|
if (translated)
|
|
2865
2887
|
writeToClient(translated);
|
|
2866
2888
|
}
|
|
@@ -2873,10 +2895,16 @@ export async function startProxy(opts = {}) {
|
|
|
2873
2895
|
else {
|
|
2874
2896
|
writeToClient(value);
|
|
2875
2897
|
}
|
|
2898
|
+
// Backpressure (#642-audit): if the last writes filled the client
|
|
2899
|
+
// buffer, pause reading upstream until it drains.
|
|
2900
|
+
if (needsDrain && !clientDisconnected) {
|
|
2901
|
+
needsDrain = false;
|
|
2902
|
+
await waitForDrain();
|
|
2903
|
+
}
|
|
2876
2904
|
}
|
|
2877
2905
|
// Flush remaining buffer
|
|
2878
2906
|
if (isOpenAI && buffer.trim()) {
|
|
2879
|
-
const translated =
|
|
2907
|
+
const translated = openaiTranslate(buffer);
|
|
2880
2908
|
if (translated)
|
|
2881
2909
|
writeToClient(translated);
|
|
2882
2910
|
}
|
|
@@ -2889,6 +2917,11 @@ export async function startProxy(opts = {}) {
|
|
|
2889
2917
|
catch (err) {
|
|
2890
2918
|
if (verbose)
|
|
2891
2919
|
console.error('[dario] Stream error:', sanitizeError(err));
|
|
2920
|
+
// Tear down the upstream body reader deterministically on an abnormal
|
|
2921
|
+
// mid-stream error (e.g. a bare upstream socket reset) so the undici
|
|
2922
|
+
// socket is released now, not at GC (#642-audit). No-op if aborted.
|
|
2923
|
+
if (!upstreamAbort.signal.aborted)
|
|
2924
|
+
upstreamAbort.abort();
|
|
2892
2925
|
}
|
|
2893
2926
|
res.end();
|
|
2894
2927
|
// Stamp the response-completion timestamp + token count so the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.121",
|
|
4
4
|
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|