@askalf/dario 6.0.53 → 6.1.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 +3 -1
- package/dist/cli.js +19 -1
- package/dist/codex-backend.d.ts +16 -1
- package/dist/codex-backend.js +54 -6
- package/dist/midstream.d.ts +321 -0
- package/dist/midstream.js +872 -0
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.js +130 -4
- package/docs/midstream-continuation.md +144 -0
- package/package.json +1 -1
package/dist/proxy.d.ts
CHANGED
|
@@ -317,6 +317,15 @@ interface ProxyOptions {
|
|
|
317
317
|
sessionStartJitterMs?: number;
|
|
318
318
|
stealth?: boolean;
|
|
319
319
|
drainOnClose?: boolean;
|
|
320
|
+
/**
|
|
321
|
+
* Finish a streamed answer that dies mid-way from the other subscription
|
|
322
|
+
* (v6.1, src/midstream.ts). On by default: it only ever acts where the
|
|
323
|
+
* alternative is a truncated stream, and the resume runs through dario's
|
|
324
|
+
* own front door at whatever `--pool-fallback` names for the other
|
|
325
|
+
* provider. `--no-midstream-continue` / `DARIO_MIDSTREAM_CONTINUE=0` turns
|
|
326
|
+
* it off; with no fallback chain it is inert and says so on the first miss.
|
|
327
|
+
*/
|
|
328
|
+
midstreamContinue?: boolean;
|
|
320
329
|
sessionIdleRotateMs?: number;
|
|
321
330
|
sessionRotateJitterMs?: number;
|
|
322
331
|
sessionMaxAgeMs?: number;
|
package/dist/proxy.js
CHANGED
|
@@ -26,6 +26,7 @@ import { createTokenBucket } from './rate-limit.js';
|
|
|
26
26
|
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
|
|
27
27
|
import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, isCodexModel, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
|
|
28
28
|
import { effortForCodex } from './effort.js';
|
|
29
|
+
import { MidstreamGuard, guardFor, loopbackBaseFor, CONTINUATION_HEADER } from './midstream.js';
|
|
29
30
|
import { isClaudeServableModel } from './claude-model.js';
|
|
30
31
|
import { MODEL_UNROUTABLE } from './upstream-rejection.js';
|
|
31
32
|
import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
|
|
@@ -1834,6 +1835,12 @@ export async function startProxy(opts = {}) {
|
|
|
1834
1835
|
// src/stream-drain.ts for the rationale + tradeoff.
|
|
1835
1836
|
const { decideOnClientClose, resolveDrainOnClose, waitForClientDrain } = await import('./stream-drain.js');
|
|
1836
1837
|
const drainOnClose = resolveDrainOnClose(opts.drainOnClose);
|
|
1838
|
+
const midstreamContinue = opts.midstreamContinue !== false;
|
|
1839
|
+
const loopbackBase = loopbackBaseFor(host, port);
|
|
1840
|
+
if (!midstreamContinue)
|
|
1841
|
+
console.log('[dario] mid-stream continuation: disabled (--no-midstream-continue)');
|
|
1842
|
+
else if (verbose)
|
|
1843
|
+
console.log(`[dario] mid-stream continuation: enabled (resume via ${loopbackBase}, target = the other provider's --pool-fallback entry)`);
|
|
1837
1844
|
if (verbose) {
|
|
1838
1845
|
console.log(`[dario] drain-on-close: ${drainOnClose ? 'enabled' : 'disabled'}`);
|
|
1839
1846
|
}
|
|
@@ -2625,8 +2632,20 @@ export async function startProxy(opts = {}) {
|
|
|
2625
2632
|
// replaces the v3.30.x-and-earlier unbounded semaphore — dario#80. A
|
|
2626
2633
|
// queue-full condition returns an explicit 429 with a `"queue-full"`
|
|
2627
2634
|
// marker in the body; a queue-timeout returns 504 with `"queue-timeout"`.
|
|
2635
|
+
// Released in the handler's finally — or EARLY by a mid-stream
|
|
2636
|
+
// continuation, whose loopback request needs the slot this request no
|
|
2637
|
+
// longer uses once its upstream is dead (a one-slot proxy would otherwise
|
|
2638
|
+
// wait on itself until the queue timeout).
|
|
2639
|
+
let queueSlotHeld = false;
|
|
2640
|
+
const releaseQueueSlot = () => {
|
|
2641
|
+
if (!queueSlotHeld)
|
|
2642
|
+
return;
|
|
2643
|
+
queueSlotHeld = false;
|
|
2644
|
+
queue.release(consumerFromHeaders);
|
|
2645
|
+
};
|
|
2628
2646
|
try {
|
|
2629
2647
|
await queue.acquire(consumerFromHeaders);
|
|
2648
|
+
queueSlotHeld = true;
|
|
2630
2649
|
}
|
|
2631
2650
|
catch (err) {
|
|
2632
2651
|
if (err instanceof QueueFullError) {
|
|
@@ -2928,6 +2947,63 @@ export async function startProxy(opts = {}) {
|
|
|
2928
2947
|
clearTimeout(bodyTimeout);
|
|
2929
2948
|
}
|
|
2930
2949
|
let body = Buffer.concat(chunks);
|
|
2950
|
+
// The request exactly as the client sent it. `body` is rewritten below
|
|
2951
|
+
// (aliases, prefixes, the CC template); a mid-stream continuation
|
|
2952
|
+
// re-issues the CLIENT's request, not the rewritten one, so dario's own
|
|
2953
|
+
// rules apply to the resume the same way they applied to the original.
|
|
2954
|
+
const clientBodyBytes = body;
|
|
2955
|
+
// A loopback request made by a continuation. Never continued itself —
|
|
2956
|
+
// one resume per client request, no nesting.
|
|
2957
|
+
const isContinuation = req.headers[CONTINUATION_HEADER] !== undefined;
|
|
2958
|
+
const loopbackHeaders = () => {
|
|
2959
|
+
const h = {};
|
|
2960
|
+
if (apiKey)
|
|
2961
|
+
h['x-api-key'] = apiKey;
|
|
2962
|
+
const c = req.headers[CONSUMER_HEADER];
|
|
2963
|
+
if (typeof c === 'string' && c.length > 0)
|
|
2964
|
+
h[CONSUMER_HEADER] = c;
|
|
2965
|
+
return h;
|
|
2966
|
+
};
|
|
2967
|
+
const parseClientBody = () => {
|
|
2968
|
+
try {
|
|
2969
|
+
const v = JSON.parse(clientBodyBytes.toString('utf-8'));
|
|
2970
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
2971
|
+
}
|
|
2972
|
+
catch {
|
|
2973
|
+
return null;
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
/**
|
|
2977
|
+
* Where a Claude stream that died mid-way resumes: the codex half of the
|
|
2978
|
+
* `--pool-fallback` chain, exactly the entry a mid-flight 429 would use.
|
|
2979
|
+
* Resolved at failure time — an account that was cooling or missing at
|
|
2980
|
+
* selection may be fine now.
|
|
2981
|
+
*/
|
|
2982
|
+
const codexContinuationTarget = async () => {
|
|
2983
|
+
const models = selectPoolFallbackForBody(body);
|
|
2984
|
+
if (models.length === 0)
|
|
2985
|
+
return null;
|
|
2986
|
+
if (!(await hasAnyCodexAccount().catch(() => false)))
|
|
2987
|
+
return null;
|
|
2988
|
+
const stored = await selectCodexAccount().catch(() => null);
|
|
2989
|
+
if (!stored)
|
|
2990
|
+
return null;
|
|
2991
|
+
let creds;
|
|
2992
|
+
try {
|
|
2993
|
+
creds = await getFreshCodexAccount(stored);
|
|
2994
|
+
}
|
|
2995
|
+
catch {
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
const slugs = await getCodexModelSlugs(creds).catch(() => []);
|
|
2999
|
+
const pick = pickCodexFallback(models, slugs);
|
|
3000
|
+
if (!pick)
|
|
3001
|
+
return null;
|
|
3002
|
+
return {
|
|
3003
|
+
model: `codex:${pick.model}${pick.effort ? `:${pick.effort}` : ''}`,
|
|
3004
|
+
label: `${pick.model} (codex ${creds.alias})`,
|
|
3005
|
+
};
|
|
3006
|
+
};
|
|
2931
3007
|
// A body that is not a JSON object cannot be routed — every decision
|
|
2932
3008
|
// below (alias, provider prefix, codex slug, template) peeks at `.model`
|
|
2933
3009
|
// and each peek swallows its parse error and falls through. So `{` used
|
|
@@ -3285,6 +3361,29 @@ export async function startProxy(opts = {}) {
|
|
|
3285
3361
|
return;
|
|
3286
3362
|
}
|
|
3287
3363
|
const codexReq = requestCount;
|
|
3364
|
+
// A codex stream that dies mid-way resumes on the Claude half of
|
|
3365
|
+
// the chain — the same target a declined codex request defers to.
|
|
3366
|
+
const claudeContinuation = claudeTarget && pool.size > 0 && !upstreamApiKey
|
|
3367
|
+
? { model: `claude:${claudeTarget.model}${claudeTarget.effort ? `:${claudeTarget.effort}` : ''}`, label: `${claudeTarget.model} (claude pool)` }
|
|
3368
|
+
: null;
|
|
3369
|
+
const codexGuard = midstreamContinue && !isContinuation
|
|
3370
|
+
? guardFor(res, {
|
|
3371
|
+
shape: isOpenAI ? 'openai' : 'anthropic',
|
|
3372
|
+
write: (chunk) => { if (!res.destroyed)
|
|
3373
|
+
res.write(chunk); },
|
|
3374
|
+
isClientGone: () => res.destroyed || res.writableEnded,
|
|
3375
|
+
requestNo: codexReq,
|
|
3376
|
+
verbose,
|
|
3377
|
+
resume: {
|
|
3378
|
+
clientBody: parseClientBody,
|
|
3379
|
+
loopbackBase,
|
|
3380
|
+
loopbackHeaders: loopbackHeaders(),
|
|
3381
|
+
resolveTarget: async () => claudeContinuation,
|
|
3382
|
+
onBeforeResume: releaseQueueSlot,
|
|
3383
|
+
timeoutMs: upstreamTimeoutMs,
|
|
3384
|
+
},
|
|
3385
|
+
})
|
|
3386
|
+
: null;
|
|
3288
3387
|
const served = codexAvailable && await forwardToCodex(req, res, body, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, isOpenAI ? 'openai' : 'anthropic', fetch, canDefer,
|
|
3289
3388
|
// Before this hook a codex request left no trace: nothing in
|
|
3290
3389
|
// /analytics, nothing in the request log, no per-account count.
|
|
@@ -3330,7 +3429,7 @@ export async function startProxy(opts = {}) {
|
|
|
3330
3429
|
// dario#1260 — the effort named by the model-name suffix stripped
|
|
3331
3430
|
// above. Undefined for every request that did not name one, which
|
|
3332
3431
|
// leaves the outbound body exactly as it was.
|
|
3333
|
-
effortForCodex(requestEffort));
|
|
3432
|
+
effortForCodex(requestEffort), codexGuard);
|
|
3334
3433
|
if (served) {
|
|
3335
3434
|
// A provider that just served is not rate-limited.
|
|
3336
3435
|
providerCooldowns.clear('codex');
|
|
@@ -4587,12 +4686,36 @@ export async function startProxy(opts = {}) {
|
|
|
4587
4686
|
// false the client's socket buffer is full, so pause the read loop until
|
|
4588
4687
|
// it drains instead of buffering the whole (fast) upstream in memory.
|
|
4589
4688
|
let needsDrain = false;
|
|
4590
|
-
const
|
|
4689
|
+
const writeToClientRaw = (chunk) => {
|
|
4591
4690
|
if (clientDisconnected)
|
|
4592
4691
|
return;
|
|
4593
4692
|
if (res.write(chunk) === false)
|
|
4594
4693
|
needsDrain = true;
|
|
4595
4694
|
};
|
|
4695
|
+
// Mid-stream continuation (v6.1): every client-bound frame passes
|
|
4696
|
+
// through the guard so a stream that dies with content on the wire can
|
|
4697
|
+
// be finished from the other subscription instead of truncated. Only a
|
|
4698
|
+
// 2xx stream is guarded — an upstream error body is not a message.
|
|
4699
|
+
const guard = midstreamContinue && !isContinuation && upstream.status >= 200 && upstream.status < 300
|
|
4700
|
+
? new MidstreamGuard({
|
|
4701
|
+
shape: isOpenAI ? 'openai' : 'anthropic',
|
|
4702
|
+
write: writeToClientRaw,
|
|
4703
|
+
end: () => { if (!res.writableEnded)
|
|
4704
|
+
res.end(); },
|
|
4705
|
+
isClientGone: () => clientDisconnected || res.destroyed || upstreamAbortReason === 'client_closed' || upstreamAbortReason === 'sse_overflow',
|
|
4706
|
+
requestNo: requestCount,
|
|
4707
|
+
verbose,
|
|
4708
|
+
resume: {
|
|
4709
|
+
clientBody: parseClientBody,
|
|
4710
|
+
loopbackBase,
|
|
4711
|
+
loopbackHeaders: loopbackHeaders(),
|
|
4712
|
+
resolveTarget: codexContinuationTarget,
|
|
4713
|
+
onBeforeResume: releaseQueueSlot,
|
|
4714
|
+
timeoutMs: upstreamTimeoutMs,
|
|
4715
|
+
},
|
|
4716
|
+
})
|
|
4717
|
+
: null;
|
|
4718
|
+
const writeToClient = guard ? (chunk) => guard.write(chunk) : writeToClientRaw;
|
|
4596
4719
|
// Resolves on 'close' (vanished client) AND on upstream abort. The
|
|
4597
4720
|
// abort arm is what keeps a connected-but-not-reading client from
|
|
4598
4721
|
// parking this handler forever and leaking its queue slot — the
|
|
@@ -4712,7 +4835,10 @@ export async function startProxy(opts = {}) {
|
|
|
4712
4835
|
if (!upstreamAbort.signal.aborted)
|
|
4713
4836
|
upstreamAbort.abort();
|
|
4714
4837
|
}
|
|
4715
|
-
|
|
4838
|
+
if (guard)
|
|
4839
|
+
await guard.finish();
|
|
4840
|
+
else
|
|
4841
|
+
res.end();
|
|
4716
4842
|
// Stamp the response-completion timestamp + token count so the
|
|
4717
4843
|
// next request's think-time delay can model human read time.
|
|
4718
4844
|
// Only on 2xx — error responses don't represent content the user
|
|
@@ -4875,7 +5001,7 @@ export async function startProxy(opts = {}) {
|
|
|
4875
5001
|
clearTimeout(upstreamTimeout);
|
|
4876
5002
|
if (onClientClose !== null)
|
|
4877
5003
|
req.off('close', onClientClose);
|
|
4878
|
-
|
|
5004
|
+
releaseQueueSlot();
|
|
4879
5005
|
}
|
|
4880
5006
|
});
|
|
4881
5007
|
server.on('error', async (err) => {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Mid-stream continuation
|
|
2
|
+
|
|
3
|
+
The answer does not stop when the plan does.
|
|
4
|
+
|
|
5
|
+
A streamed answer that dies part-way through used to end the way the transport
|
|
6
|
+
ended it: the upstream socket resets, Anthropic sends an in-band
|
|
7
|
+
`overloaded_error`, the codex backend answers `response.failed`, and the client
|
|
8
|
+
gets a stream with no `message_stop` and no `[DONE]`. An SDK throws "stream
|
|
9
|
+
ended without producing a Message"; a chat UI shows half a paragraph and a
|
|
10
|
+
spinner; an agent loses the tool call it was two tokens away from. Every word
|
|
11
|
+
already on screen is wasted, and the failover chain never fires, because once
|
|
12
|
+
bytes were on the wire the request was treated as too late to hand to anyone
|
|
13
|
+
else.
|
|
14
|
+
|
|
15
|
+
Since 6.1 dario finishes the same client stream from the other subscription.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
client ──▶ dario ──▶ Claude pool ✗ dies after 1,240 chars
|
|
19
|
+
└─▶ ChatGPT plan ✓ resumes at char 1,241, same stream
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The client sees one message: the same `message_start`, the same content block
|
|
23
|
+
still open, then the rest of the text, then a clean close. There is an SSE
|
|
24
|
+
comment at the seam (`: dario continuation gpt-5.6-sol (codex live) after 1240
|
|
25
|
+
chars`) that every parser ignores and every raw capture shows.
|
|
26
|
+
|
|
27
|
+
## When it fires
|
|
28
|
+
|
|
29
|
+
Only on a stream that is already partly delivered and then breaks — never on a
|
|
30
|
+
request that failed before its first byte (the existing failover covers those),
|
|
31
|
+
never on a stream that finished. Concretely, on `/v1/messages` and
|
|
32
|
+
`/v1/chat/completions` with `stream: true`, when a 2xx stream ends without its
|
|
33
|
+
terminal event, or carries an `error` event after content, or the codex
|
|
34
|
+
translator flags a failed turn:
|
|
35
|
+
|
|
36
|
+
| cut fell inside | what happens |
|
|
37
|
+
|---|---|
|
|
38
|
+
| a text block | the resume continues **that block** — no new `content_block_start` |
|
|
39
|
+
| thinking, or before any block | the open block is closed and the resume starts a fresh text block |
|
|
40
|
+
| between blocks | the resume starts a fresh text block |
|
|
41
|
+
| a `tool_use` block, or an OpenAI `tool_calls` delta | **not continued** — half a JSON argument is not resumable; the stream ends as before |
|
|
42
|
+
|
|
43
|
+
Non-streaming requests are untouched; nothing was on the wire.
|
|
44
|
+
|
|
45
|
+
## Where it resumes
|
|
46
|
+
|
|
47
|
+
Through dario's own front door. The resume is a loopback `POST` to the same
|
|
48
|
+
proxy, so the pool, the codex translator, cch, the template, every rule that
|
|
49
|
+
applied to the original request applies to the resume. The request carries
|
|
50
|
+
`x-dario-continuation` so it is never itself continued — one resume per client
|
|
51
|
+
request, no nesting.
|
|
52
|
+
|
|
53
|
+
The target is the other provider's entry in `--pool-fallback`, exactly the entry
|
|
54
|
+
a mid-flight 429 would use:
|
|
55
|
+
|
|
56
|
+
- a Claude stream resumes on the codex half of the chain (`gpt-5.6-sol` in
|
|
57
|
+
`--pool-fallback=gpt-5.6-sol,claude:claude-sonnet-5`), resolved at failure
|
|
58
|
+
time against the account's live model list;
|
|
59
|
+
- a codex stream resumes on the Claude half (`claude-sonnet-5` above), resolved
|
|
60
|
+
against the live catalog the way the chain already is.
|
|
61
|
+
|
|
62
|
+
With no chain, or no entry for the other provider, the stream ends exactly as it
|
|
63
|
+
did before 6.1, and the log says why once:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
[dario] #42 stream died after 1240 chars — no continuation target (set --pool-fallback with an entry for the other provider)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The request's queue slot is released before the loopback is made, so a
|
|
70
|
+
`--max-concurrent=1` proxy resumes instead of waiting on itself.
|
|
71
|
+
|
|
72
|
+
## The seam
|
|
73
|
+
|
|
74
|
+
The resume is the client's own request with two turns appended: the partial
|
|
75
|
+
answer as the assistant turn, and a user turn asking for the rest. There is
|
|
76
|
+
**no assistant prefill** — Claude 4.6+ answers a trailing assistant turn with a
|
|
77
|
+
400, and the Responses API never had the concept — so the resume is
|
|
78
|
+
instruction-driven on both providers.
|
|
79
|
+
|
|
80
|
+
The user turn is written as the person whose connection dropped, not as an
|
|
81
|
+
operator notice. The first live run is why: told `[transport notice] … resume
|
|
82
|
+
now`, claude-sonnet-5 answered that the notice "isn't an actual system message
|
|
83
|
+
— it's just text in your prompt" and stopped, which is the injection-awareness
|
|
84
|
+
it should have. "My connection dropped while you were writing that reply, so I
|
|
85
|
+
only received it up to this point: «…». Please pick up exactly where you left
|
|
86
|
+
off" is an ordinary request and gets the ordinary answer.
|
|
87
|
+
|
|
88
|
+
It asks the model to begin by repeating, verbatim, the last ~40 characters of
|
|
89
|
+
the cut-off text, then continue. dario holds the first ~240
|
|
90
|
+
characters of the resume, finds that repeat with a whitespace- and
|
|
91
|
+
quote-normalized match, cuts it, and streams everything after it. The model
|
|
92
|
+
renders the seam — the space between two words, the four-space indent, the
|
|
93
|
+
second half of a split word — inside its own token stream, and dario only trims.
|
|
94
|
+
Told merely to "continue", a model drops the boundary whitespace often enough
|
|
95
|
+
to notice (`replies<cut>with`); told to repeat the anchor, it does not.
|
|
96
|
+
|
|
97
|
+
One rule on top: prose cut mid-sentence whose continuation opens with a
|
|
98
|
+
paragraph break gets one space instead. Inside a code fence a newline is
|
|
99
|
+
content and is left alone.
|
|
100
|
+
|
|
101
|
+
If the model does not repeat the anchor, the longest exact overlap between the
|
|
102
|
+
partial's tail and the resume's head is trimmed; if there is none, nothing is.
|
|
103
|
+
|
|
104
|
+
## What it costs
|
|
105
|
+
|
|
106
|
+
One extra request on the other subscription, carrying the whole conversation
|
|
107
|
+
plus the partial. The original request is logged and counted as it was (a
|
|
108
|
+
502 on the codex path, a truncated 200 on the Claude path); the resume is
|
|
109
|
+
logged as its own request. A resume that fails before producing anything hands
|
|
110
|
+
the stream back to end as it would have. A resume that fails after producing
|
|
111
|
+
something forwards its error frame if it sent one, and otherwise simply stops —
|
|
112
|
+
whatever it had already written is on the wire, and the stream is left without
|
|
113
|
+
its terminal event so the client sees the truncation. A twice-truncated answer
|
|
114
|
+
is never closed with a synthetic `end_turn`; only the resume's own
|
|
115
|
+
`message_stop` / `[DONE]` finishes the message.
|
|
116
|
+
|
|
117
|
+
## Switches
|
|
118
|
+
|
|
119
|
+
| | |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `--no-midstream-continue` | off for this proxy |
|
|
122
|
+
| `DARIO_MIDSTREAM_CONTINUE=0` | same, for the container |
|
|
123
|
+
| `--pool-fallback=…` | where a stream resumes; no entry for the other provider means no resume |
|
|
124
|
+
|
|
125
|
+
On by default: it only ever acts where the alternative is a broken stream.
|
|
126
|
+
|
|
127
|
+
## How it was proven
|
|
128
|
+
|
|
129
|
+
`test/midstream-continuation-wiring.mjs` runs a real proxy against a fake
|
|
130
|
+
Anthropic upstream and a codex stub, kills the stream in every way listed
|
|
131
|
+
above, and replays what the client received through a strict grammar check of
|
|
132
|
+
both wire shapes. Before that, the same splice ran outside the proxy against
|
|
133
|
+
production dario 6.0.51 with real Opus 5 and a real ChatGPT Plus account, both
|
|
134
|
+
directions, prose and code: ten runs, zero restarts, zero preamble, zero
|
|
135
|
+
repetition, anchor matched exactly five times out of five, and the official
|
|
136
|
+
`@anthropic-ai/sdk` accepted every spliced stream as one message. The seam,
|
|
137
|
+
verbatim, from one of them:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
…concatenation of one or more unit components in the
|
|
141
|
+
order hours, min<CUT>utes, and seconds. At least one component is required.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Claude wrote the left half, GPT the right.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|