@askalf/dario 4.8.120 → 4.8.122
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/model-catalog.d.ts +1 -1
- package/dist/model-catalog.js +11 -2
- package/dist/proxy.d.ts +8 -0
- package/dist/proxy.js +103 -45
- package/package.json +1 -1
package/dist/model-catalog.d.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* — the same ordering normalizeUpstreamIds() produces for live data.
|
|
33
33
|
*/
|
|
34
34
|
export declare const BAKED_BASE_MODELS: readonly string[];
|
|
35
|
-
/** The set of suspended families, resolved from env
|
|
35
|
+
/** The set of suspended families, resolved from env (memoized by raw value). */
|
|
36
36
|
export declare function suspendedFamilies(): Set<string>;
|
|
37
37
|
/** True when `model` (any spelling) belongs to a suspended family. */
|
|
38
38
|
export declare function isSuspendedModel(model: string): boolean;
|
package/dist/model-catalog.js
CHANGED
|
@@ -67,14 +67,23 @@ export const BAKED_BASE_MODELS = [
|
|
|
67
67
|
* to re-suspend if access is ever pulled again.
|
|
68
68
|
*/
|
|
69
69
|
const DEFAULT_SUSPENDED_MODELS = '';
|
|
70
|
-
|
|
70
|
+
// Memoized by the raw env value (#642-audit): isSuspendedModel runs per request,
|
|
71
|
+
// and this rebuilt a Set from process.env every call. Keyed on the raw string so
|
|
72
|
+
// a runtime change to DARIO_SUSPENDED_MODELS still re-parses; the common (empty)
|
|
73
|
+
// value allocates the Set once.
|
|
74
|
+
let _suspendedCache = null;
|
|
75
|
+
/** The set of suspended families, resolved from env (memoized by raw value). */
|
|
71
76
|
export function suspendedFamilies() {
|
|
72
77
|
const raw = process.env.DARIO_SUSPENDED_MODELS ?? DEFAULT_SUSPENDED_MODELS;
|
|
73
|
-
|
|
78
|
+
if (_suspendedCache && _suspendedCache.raw === raw)
|
|
79
|
+
return _suspendedCache.set;
|
|
80
|
+
const set = new Set(raw
|
|
74
81
|
.split(',')
|
|
75
82
|
.map((s) => s.trim())
|
|
76
83
|
.filter((s) => s.length > 0)
|
|
77
84
|
.map((s) => modelFamily(s) ?? s.toLowerCase()));
|
|
85
|
+
_suspendedCache = { raw, set };
|
|
86
|
+
return set;
|
|
78
87
|
}
|
|
79
88
|
/** True when `model` (any spelling) belongs to a suspended family. */
|
|
80
89
|
export function isSuspendedModel(model) {
|
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
|
@@ -455,6 +455,21 @@ function sanitizeContent(text, patterns) {
|
|
|
455
455
|
}
|
|
456
456
|
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
457
457
|
}
|
|
458
|
+
// Memoize the compiled preserve-tag pattern set by Set reference (#642-audit):
|
|
459
|
+
// opts.preserveOrchestrationTags is a single Set instance passed on every
|
|
460
|
+
// request, so recompile the ~28 patterns once instead of per request. The
|
|
461
|
+
// default (no-preserve) path already uses the precompiled constant.
|
|
462
|
+
let _orchPreserveKey;
|
|
463
|
+
let _orchPatterns;
|
|
464
|
+
function orchestrationPatternsFor(preserveTags) {
|
|
465
|
+
if (preserveTags === undefined)
|
|
466
|
+
return ORCHESTRATION_PATTERNS_DEFAULT;
|
|
467
|
+
if (preserveTags !== _orchPreserveKey) {
|
|
468
|
+
_orchPreserveKey = preserveTags;
|
|
469
|
+
_orchPatterns = buildOrchestrationPatterns(preserveTags);
|
|
470
|
+
}
|
|
471
|
+
return _orchPatterns;
|
|
472
|
+
}
|
|
458
473
|
/**
|
|
459
474
|
* Strip orchestration tags from all messages in a request body.
|
|
460
475
|
*
|
|
@@ -465,7 +480,7 @@ export function sanitizeMessages(body, preserveTags) {
|
|
|
465
480
|
const messages = body.messages;
|
|
466
481
|
if (!messages)
|
|
467
482
|
return;
|
|
468
|
-
const patterns =
|
|
483
|
+
const patterns = orchestrationPatternsFor(preserveTags);
|
|
469
484
|
for (const msg of messages) {
|
|
470
485
|
if (typeof msg.content === 'string') {
|
|
471
486
|
msg.content = sanitizeContent(msg.content, patterns);
|
|
@@ -544,48 +559,55 @@ function anthropicToOpenai(body) {
|
|
|
544
559
|
usage: { prompt_tokens: u?.input_tokens ?? 0, completion_tokens: u?.output_tokens ?? 0, total_tokens: (u?.input_tokens ?? 0) + (u?.output_tokens ?? 0) },
|
|
545
560
|
};
|
|
546
561
|
}
|
|
547
|
-
/**
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Translate Anthropic SSE → OpenAI SSE. Returns a stateful per-CALL closure so
|
|
564
|
+
* concurrent /v1/chat/completions streams don't share tool-call index/id state
|
|
565
|
+
* (#642-audit: the previous module-global counters cross-contaminated
|
|
566
|
+
* interleaved streams and emitted malformed tool_calls deltas). One translator
|
|
567
|
+
* per request, mirroring createStreamingReverseMapper.
|
|
568
|
+
*/
|
|
569
|
+
export function createOpenAIStreamTranslator() {
|
|
570
|
+
let toolIndex = 0;
|
|
571
|
+
let toolId = '';
|
|
572
|
+
return function translateStreamChunk(line) {
|
|
573
|
+
if (!line.startsWith('data: '))
|
|
574
|
+
return null;
|
|
575
|
+
const json = line.slice(6).trim();
|
|
576
|
+
if (json === '[DONE]')
|
|
577
|
+
return 'data: [DONE]\n\n';
|
|
578
|
+
try {
|
|
579
|
+
const e = JSON.parse(json);
|
|
580
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
581
|
+
if (e.type === 'content_block_start') {
|
|
582
|
+
const block = e.content_block;
|
|
583
|
+
if (block?.type === 'tool_use' && block.name) {
|
|
584
|
+
toolId = block.id ?? `call_${toolIndex}`;
|
|
585
|
+
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`;
|
|
586
|
+
}
|
|
565
587
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
588
|
+
if (e.type === 'content_block_delta') {
|
|
589
|
+
const d = e.delta;
|
|
590
|
+
if (d?.type === 'text_delta' && d.text)
|
|
591
|
+
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`;
|
|
592
|
+
if (d?.type === 'input_json_delta' && d.partial_json)
|
|
593
|
+
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`;
|
|
594
|
+
}
|
|
595
|
+
if (e.type === 'content_block_stop') {
|
|
596
|
+
if (toolId) {
|
|
597
|
+
toolIndex++;
|
|
598
|
+
toolId = '';
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
if (e.type === 'message_stop') {
|
|
603
|
+
toolIndex = 0;
|
|
604
|
+
toolId = '';
|
|
605
|
+
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
606
|
}
|
|
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
607
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
608
|
+
catch { }
|
|
609
|
+
return null;
|
|
610
|
+
};
|
|
589
611
|
}
|
|
590
612
|
// Baked /v1/models payload — what the proxy advertises before (or without)
|
|
591
613
|
// a successful upstream catalog fetch. The live route serves the
|
|
@@ -1751,9 +1773,15 @@ export async function startProxy(opts = {}) {
|
|
|
1751
1773
|
// recognizes through its own Anthropic gateway, bypassing localhost).
|
|
1752
1774
|
let forcedProvider = cliProviderOverride;
|
|
1753
1775
|
let requestEffort; // dario#419 — per-request effort parsed from a model-name suffix (model:high / model-high)
|
|
1776
|
+
// Parsed body, shared between the provider-prefix detection below and the
|
|
1777
|
+
// template-build block further down so the same bytes are not JSON.parsed
|
|
1778
|
+
// twice per request (#642-audit). Mutations in the prefix block re-serialize
|
|
1779
|
+
// `body` FROM this object, so it always represents the current body.
|
|
1780
|
+
let parsedBody = null;
|
|
1754
1781
|
if (body.length > 0) {
|
|
1755
1782
|
try {
|
|
1756
1783
|
const parsed = JSON.parse(body.toString());
|
|
1784
|
+
parsedBody = parsed;
|
|
1757
1785
|
const rawModel = parsed.model ?? '';
|
|
1758
1786
|
const prefix = parseProviderPrefix(rawModel);
|
|
1759
1787
|
if (prefix) {
|
|
@@ -1859,7 +1887,11 @@ export async function startProxy(opts = {}) {
|
|
|
1859
1887
|
} : undefined;
|
|
1860
1888
|
if (body.length > 0) {
|
|
1861
1889
|
try {
|
|
1862
|
-
|
|
1890
|
+
// Reuse the object parsed for provider-prefix detection above — the same
|
|
1891
|
+
// bytes, so re-parsing is wasted work on large bodies (#642-audit). Falls
|
|
1892
|
+
// back to a fresh parse when the earlier block didn't run (e.g. the body
|
|
1893
|
+
// was not JSON, or an OpenAI-backend reroute skipped it).
|
|
1894
|
+
const parsed = parsedBody ?? JSON.parse(body.toString());
|
|
1863
1895
|
// Strip orchestration tags from messages (Aider, Cursor, etc.)
|
|
1864
1896
|
sanitizeMessages(parsed, opts.preserveOrchestrationTags);
|
|
1865
1897
|
const result = isOpenAI ? openaiToAnthropic(parsed, modelOverride) : (modelOverride ? { ...parsed, model: modelOverride } : parsed);
|
|
@@ -2779,14 +2811,29 @@ export async function startProxy(opts = {}) {
|
|
|
2779
2811
|
const streamMapper = ccToolMap && !isOpenAI
|
|
2780
2812
|
? createStreamingReverseMapper(ccToolMap, reqCtx)
|
|
2781
2813
|
: null;
|
|
2814
|
+
// Per-request OpenAI SSE translator — isolated tool-call state so
|
|
2815
|
+
// concurrent /v1/chat/completions streams don't cross-contaminate (#642-audit).
|
|
2816
|
+
const openaiTranslate = isOpenAI ? createOpenAIStreamTranslator() : null;
|
|
2782
2817
|
// Gated writer — a no-op once the downstream client has gone away
|
|
2783
2818
|
// in drain-on-close mode. The read loop keeps consuming so the
|
|
2784
2819
|
// upstream sees a full-length read; writes to a closed socket are
|
|
2785
2820
|
// suppressed to avoid EPIPE/warnings and pointless work.
|
|
2821
|
+
// Honor downstream backpressure (#642-audit): when res.write() returns
|
|
2822
|
+
// false the client's socket buffer is full, so pause the read loop until
|
|
2823
|
+
// it drains instead of buffering the whole (fast) upstream in memory.
|
|
2824
|
+
let needsDrain = false;
|
|
2786
2825
|
const writeToClient = (chunk) => {
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2826
|
+
if (clientDisconnected)
|
|
2827
|
+
return;
|
|
2828
|
+
if (res.write(chunk) === false)
|
|
2829
|
+
needsDrain = true;
|
|
2789
2830
|
};
|
|
2831
|
+
// Resolves on 'close' too, so a vanished client can't wedge the loop.
|
|
2832
|
+
const waitForDrain = () => new Promise((resolve) => {
|
|
2833
|
+
const done = () => { res.off('drain', done); res.off('close', done); resolve(); };
|
|
2834
|
+
res.once('drain', done);
|
|
2835
|
+
res.once('close', done);
|
|
2836
|
+
});
|
|
2790
2837
|
try {
|
|
2791
2838
|
let buffer = '';
|
|
2792
2839
|
const MAX_LINE_LENGTH = 1_000_000; // 1MB max per SSE line
|
|
@@ -2860,7 +2907,7 @@ export async function startProxy(opts = {}) {
|
|
|
2860
2907
|
const lines = buffer.split('\n');
|
|
2861
2908
|
buffer = lines.pop() ?? '';
|
|
2862
2909
|
for (const line of lines) {
|
|
2863
|
-
const translated =
|
|
2910
|
+
const translated = openaiTranslate(line);
|
|
2864
2911
|
if (translated)
|
|
2865
2912
|
writeToClient(translated);
|
|
2866
2913
|
}
|
|
@@ -2873,10 +2920,16 @@ export async function startProxy(opts = {}) {
|
|
|
2873
2920
|
else {
|
|
2874
2921
|
writeToClient(value);
|
|
2875
2922
|
}
|
|
2923
|
+
// Backpressure (#642-audit): if the last writes filled the client
|
|
2924
|
+
// buffer, pause reading upstream until it drains.
|
|
2925
|
+
if (needsDrain && !clientDisconnected) {
|
|
2926
|
+
needsDrain = false;
|
|
2927
|
+
await waitForDrain();
|
|
2928
|
+
}
|
|
2876
2929
|
}
|
|
2877
2930
|
// Flush remaining buffer
|
|
2878
2931
|
if (isOpenAI && buffer.trim()) {
|
|
2879
|
-
const translated =
|
|
2932
|
+
const translated = openaiTranslate(buffer);
|
|
2880
2933
|
if (translated)
|
|
2881
2934
|
writeToClient(translated);
|
|
2882
2935
|
}
|
|
@@ -2889,6 +2942,11 @@ export async function startProxy(opts = {}) {
|
|
|
2889
2942
|
catch (err) {
|
|
2890
2943
|
if (verbose)
|
|
2891
2944
|
console.error('[dario] Stream error:', sanitizeError(err));
|
|
2945
|
+
// Tear down the upstream body reader deterministically on an abnormal
|
|
2946
|
+
// mid-stream error (e.g. a bare upstream socket reset) so the undici
|
|
2947
|
+
// socket is released now, not at GC (#642-audit). No-op if aborted.
|
|
2948
|
+
if (!upstreamAbort.signal.aborted)
|
|
2949
|
+
upstreamAbort.abort();
|
|
2892
2950
|
}
|
|
2893
2951
|
res.end();
|
|
2894
2952
|
// 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.122",
|
|
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": {
|