@askalf/dario 6.1.0 → 6.2.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 +1 -1
- package/dist/midstream.d.ts +26 -3
- package/dist/midstream.js +71 -23
- package/dist/proxy.js +20 -6
- package/docs/midstream-continuation.md +27 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -289,7 +289,7 @@ That is a **chain**, read left to right; each provider takes the first entry it
|
|
|
289
289
|
|
|
290
290
|
A single-entry chain is one-way and means what it always meant, so an existing config is unaffected. Failover is opt-in: without `--pool-fallback`, a drained pool still returns its honest 429/503. Only a **429 or 5xx** fails over; a 400 surfaces, because a bad request that fails over just reproduces itself on the other provider and buries the real cause. A 429 also cools that provider for a bounded interval, its `retry-after` if it sent one and 60 s otherwise, never longer than 15 min, and an entry that already declined is not asked again within the same request. When every entry is cooling, the request ends on one honest `429` with a `retry-after` instead of a retry storm. The Claude entry has to be a model the pool can actually serve, checked positively against the live catalog, so a typo can't trade a recoverable 429 for an unrecoverable 404.
|
|
291
291
|
|
|
292
|
-
The chain also covers a stream that dies **mid-answer**. Until 6.1 that was the one failure nothing could catch: bytes were on the wire, so the socket reset, the in-band `overloaded_error`, the codex `response.failed` all ended the stream where they happened, with no `message_stop`, and the client threw away every word it already had. Now dario finishes the same stream
|
|
292
|
+
The chain also covers a stream that dies **mid-answer**. Until 6.1 that was the one failure nothing could catch: bytes were on the wire, so the socket reset, the in-band `overloaded_error`, the codex `response.failed` all ended the stream where they happened, with no `message_stop`, and the client threw away every word it already had. Now dario finishes the same stream — on the same model first, a fresh request through its own front door; on the other subscription when that delivers nothing or dies too. The resume picks up inside the still-open content block, a comment marks each seam (`: dario continuation claude-opus-5 (same model) after 1240 chars`), and the client sees one message. The model is asked to repeat the last few words verbatim and dario trims the repeat, so the join is rendered by a model and cut by a parser, never guessed. Text only — a cut inside a tool call ends as it always did. On by default, `--no-midstream-continue` turns it off; without a chain the second hop is simply not there. [How it works](docs/midstream-continuation.md).
|
|
293
293
|
|
|
294
294
|
`dario doctor` tells you which of these you are actually in:
|
|
295
295
|
|
package/dist/midstream.d.ts
CHANGED
|
@@ -49,8 +49,22 @@
|
|
|
49
49
|
*/
|
|
50
50
|
import type { ServerResponse } from 'node:http';
|
|
51
51
|
export type WireShape = 'anthropic' | 'openai';
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Marks a loopback request as a continuation and carries its DEPTH: `1` for
|
|
54
|
+
* the resume of a client request, `2` for the resume of that resume. A
|
|
55
|
+
* request at `MAX_CONTINUATION_DEPTH` is never continued itself.
|
|
56
|
+
*/
|
|
53
57
|
export declare const CONTINUATION_HEADER = "x-dario-continuation";
|
|
58
|
+
/**
|
|
59
|
+
* Two hops. The first resume asks for the SAME model again through the front
|
|
60
|
+
* door — the pool picks a seat and the existing pre-byte failover applies —
|
|
61
|
+
* so a transient reset finishes on the model the client asked for. If that
|
|
62
|
+
* resume dies too, the second hop goes to the OTHER provider's chain entry.
|
|
63
|
+
* A third hop would be a third attempt at whatever is failing.
|
|
64
|
+
*/
|
|
65
|
+
export declare const MAX_CONTINUATION_DEPTH = 2;
|
|
66
|
+
/** The depth a request carries, 0 for an ordinary client request. */
|
|
67
|
+
export declare function continuationDepth(headerValue: string | string[] | undefined): number;
|
|
54
68
|
/** Characters of the partial the model is asked to repeat verbatim (the seam anchor). */
|
|
55
69
|
export declare const ANCHOR_CHARS = 40;
|
|
56
70
|
export interface SseFrame {
|
|
@@ -219,6 +233,8 @@ export declare class Splicer {
|
|
|
219
233
|
dropped: number;
|
|
220
234
|
emitted: number;
|
|
221
235
|
};
|
|
236
|
+
/** `message_start.model` of the resume — who actually served it. */
|
|
237
|
+
resumeModel: string | null;
|
|
222
238
|
constructor(shape: WireShape, state: ClientStreamState, partial: string);
|
|
223
239
|
/** Frames to write to the client for one resume frame. */
|
|
224
240
|
feed(f: SseFrame): string[];
|
|
@@ -261,8 +277,13 @@ export interface ResumeOptions {
|
|
|
261
277
|
loopbackBase: string;
|
|
262
278
|
/** Auth + attribution headers for the loopback request. */
|
|
263
279
|
loopbackHeaders: Record<string, string>;
|
|
264
|
-
/**
|
|
265
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Where to resume, decided at failure time. `choice` is 1 for the same
|
|
282
|
+
* model again and 2 for the other provider's chain entry; the guard asks
|
|
283
|
+
* in that order and moves on when a choice is null or its loopback delivers
|
|
284
|
+
* nothing. Null for every choice = the stream ends as before.
|
|
285
|
+
*/
|
|
286
|
+
resolveTarget: (choice: number) => Promise<ContinuationTarget | null>;
|
|
266
287
|
/** Called right before the loopback request is made — the site releases its own queue slot here. */
|
|
267
288
|
onBeforeResume?: () => void;
|
|
268
289
|
timeoutMs: number;
|
|
@@ -277,6 +298,8 @@ export interface MidstreamGuardOptions {
|
|
|
277
298
|
isClientGone: () => boolean;
|
|
278
299
|
resume: ResumeOptions | null;
|
|
279
300
|
requestNo: number;
|
|
301
|
+
/** Depth of the request THIS guard protects (0 = a client request). Its resume is `depth + 1`. */
|
|
302
|
+
depth?: number;
|
|
280
303
|
verbose: boolean;
|
|
281
304
|
log?: (line: string) => void;
|
|
282
305
|
}
|
package/dist/midstream.js
CHANGED
|
@@ -47,8 +47,28 @@
|
|
|
47
47
|
* the existing pre-byte failover covers them), and the api-key OpenAI backend.
|
|
48
48
|
* A stream that cannot be continued ends exactly as it did before this module.
|
|
49
49
|
*/
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Marks a loopback request as a continuation and carries its DEPTH: `1` for
|
|
52
|
+
* the resume of a client request, `2` for the resume of that resume. A
|
|
53
|
+
* request at `MAX_CONTINUATION_DEPTH` is never continued itself.
|
|
54
|
+
*/
|
|
51
55
|
export const CONTINUATION_HEADER = 'x-dario-continuation';
|
|
56
|
+
/**
|
|
57
|
+
* Two hops. The first resume asks for the SAME model again through the front
|
|
58
|
+
* door — the pool picks a seat and the existing pre-byte failover applies —
|
|
59
|
+
* so a transient reset finishes on the model the client asked for. If that
|
|
60
|
+
* resume dies too, the second hop goes to the OTHER provider's chain entry.
|
|
61
|
+
* A third hop would be a third attempt at whatever is failing.
|
|
62
|
+
*/
|
|
63
|
+
export const MAX_CONTINUATION_DEPTH = 2;
|
|
64
|
+
/** The depth a request carries, 0 for an ordinary client request. */
|
|
65
|
+
export function continuationDepth(headerValue) {
|
|
66
|
+
const v = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
67
|
+
if (v === undefined)
|
|
68
|
+
return 0;
|
|
69
|
+
const n = Number.parseInt(v, 10);
|
|
70
|
+
return Number.isFinite(n) && n > 0 ? n : 1;
|
|
71
|
+
}
|
|
52
72
|
/** Characters of the partial the model is asked to repeat verbatim (the seam anchor). */
|
|
53
73
|
export const ANCHOR_CHARS = 40;
|
|
54
74
|
/** Upper bound on continuation text held back while looking for the anchor. */
|
|
@@ -457,6 +477,8 @@ export class Splicer {
|
|
|
457
477
|
terminalSeen = false;
|
|
458
478
|
/** Diagnostics for the log line. */
|
|
459
479
|
stats = { anchor: 'n/a', dropped: 0, emitted: 0 };
|
|
480
|
+
/** `message_start.model` of the resume — who actually served it. */
|
|
481
|
+
resumeModel = null;
|
|
460
482
|
constructor(shape, state, partial) {
|
|
461
483
|
this.shape = shape;
|
|
462
484
|
this.partial = partial;
|
|
@@ -467,7 +489,11 @@ export class Splicer {
|
|
|
467
489
|
}
|
|
468
490
|
/** Frames to write to the client for one resume frame. */
|
|
469
491
|
feed(f) {
|
|
470
|
-
|
|
492
|
+
// A second hop's seam comment, written by the inner guard, rides through
|
|
493
|
+
// so a raw capture shows every takeover; every other comment is dropped.
|
|
494
|
+
if (f.comment)
|
|
495
|
+
return f.raw.startsWith(SEAM_COMMENT_PREFIX) ? [f.raw] : [];
|
|
496
|
+
if (f.dataText === null)
|
|
471
497
|
return [];
|
|
472
498
|
return this.shape === 'anthropic' ? this.feedAnthropic(f) : this.feedOpenAI(f);
|
|
473
499
|
}
|
|
@@ -490,8 +516,12 @@ export class Splicer {
|
|
|
490
516
|
switch (d.type) {
|
|
491
517
|
case 'ping':
|
|
492
518
|
return [f.raw];
|
|
493
|
-
case 'message_start':
|
|
519
|
+
case 'message_start': {
|
|
520
|
+
const m = d.message;
|
|
521
|
+
if (typeof m?.model === 'string')
|
|
522
|
+
this.resumeModel = m.model;
|
|
494
523
|
return []; // the client already has one
|
|
524
|
+
}
|
|
495
525
|
case 'content_block_start': {
|
|
496
526
|
const idx = d.index;
|
|
497
527
|
const cb = d.content_block;
|
|
@@ -607,6 +637,8 @@ export class Splicer {
|
|
|
607
637
|
const c = choices?.[0];
|
|
608
638
|
if (!c)
|
|
609
639
|
return [];
|
|
640
|
+
if (typeof d.model === 'string' && !this.resumeModel)
|
|
641
|
+
this.resumeModel = d.model;
|
|
610
642
|
const out = [];
|
|
611
643
|
if (typeof c.delta?.content === 'string' && c.delta.content.length > 0) {
|
|
612
644
|
this.hold += c.delta.content;
|
|
@@ -661,6 +693,7 @@ export class Splicer {
|
|
|
661
693
|
return [openaiChunk({ content: text }, null)];
|
|
662
694
|
}
|
|
663
695
|
}
|
|
696
|
+
const SEAM_COMMENT_PREFIX = ': dario continuation';
|
|
664
697
|
function openaiChunk(delta, finish) {
|
|
665
698
|
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'claude', choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`;
|
|
666
699
|
}
|
|
@@ -722,27 +755,41 @@ export class MidstreamGuard {
|
|
|
722
755
|
cleanEnd();
|
|
723
756
|
return 'not-continuable';
|
|
724
757
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
758
|
+
// The choices, in order: 1 = the same model again, 2 = the other
|
|
759
|
+
// provider's chain entry. A client request tries both; a request that IS
|
|
760
|
+
// a resume (depth 1) starts at 2 — its model is the one that just failed
|
|
761
|
+
// twice. A choice whose loopback delivers nothing (refused, unreachable,
|
|
762
|
+
// dead before its first byte) hands over to the next; the first one that
|
|
763
|
+
// puts content on the wire ends the search, finished or not.
|
|
764
|
+
const partial = s.textSoFar;
|
|
765
|
+
let tried = 0;
|
|
766
|
+
for (let choice = (this.o.depth ?? 0) + 1; choice <= MAX_CONTINUATION_DEPTH; choice++) {
|
|
767
|
+
let target = null;
|
|
768
|
+
try {
|
|
769
|
+
target = await this.o.resume.resolveTarget(choice);
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
target = null;
|
|
773
|
+
}
|
|
774
|
+
if (!target)
|
|
775
|
+
continue;
|
|
776
|
+
tried++;
|
|
777
|
+
this.log(`#${this.o.requestNo} stream died after ${partial.length} chars → continuing as ${target.label}`);
|
|
778
|
+
const outcome = await this.continueFrom(target, partial);
|
|
779
|
+
if (outcome === 'failed') {
|
|
780
|
+
this.log(`#${this.o.requestNo} continuation as ${target.label} delivered nothing${choice < MAX_CONTINUATION_DEPTH ? ' — trying the next choice' : ''}`);
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
this.o.end();
|
|
784
|
+
return outcome === 'finished' ? 'continued' : 'continued-unfinished';
|
|
731
785
|
}
|
|
732
|
-
if (
|
|
733
|
-
this.log(`#${this.o.requestNo} stream died after ${
|
|
786
|
+
if (tried === 0) {
|
|
787
|
+
this.log(`#${this.o.requestNo} stream died after ${partial.length} chars — no continuation target (set --pool-fallback with an entry for the other provider)`);
|
|
734
788
|
cleanEnd();
|
|
735
789
|
return 'no-target';
|
|
736
790
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const outcome = await this.continueFrom(target, partial);
|
|
740
|
-
if (outcome === 'failed') {
|
|
741
|
-
cleanEnd();
|
|
742
|
-
return 'resume-failed';
|
|
743
|
-
}
|
|
744
|
-
this.o.end();
|
|
745
|
-
return outcome === 'finished' ? 'continued' : 'continued-unfinished';
|
|
791
|
+
cleanEnd();
|
|
792
|
+
return 'resume-failed';
|
|
746
793
|
}
|
|
747
794
|
/**
|
|
748
795
|
* 'failed': nothing of the resume reached the client — the site ends the
|
|
@@ -770,7 +817,7 @@ export class MidstreamGuard {
|
|
|
770
817
|
r.onBeforeResume?.();
|
|
771
818
|
const res = await fetchImpl(`${r.loopbackBase}${path}`, {
|
|
772
819
|
method: 'POST',
|
|
773
|
-
headers: { 'content-type': 'application/json', [CONTINUATION_HEADER]: String(this.o.
|
|
820
|
+
headers: { 'content-type': 'application/json', [CONTINUATION_HEADER]: String((this.o.depth ?? 0) + 1), ...r.loopbackHeaders },
|
|
774
821
|
body: JSON.stringify(body),
|
|
775
822
|
signal: abort.signal,
|
|
776
823
|
});
|
|
@@ -781,7 +828,7 @@ export class MidstreamGuard {
|
|
|
781
828
|
}
|
|
782
829
|
// An SSE comment, ignored by every parser, so a raw capture shows where
|
|
783
830
|
// the second provider took over.
|
|
784
|
-
this.o.write(
|
|
831
|
+
this.o.write(`${SEAM_COMMENT_PREFIX} ${target.label} after ${partial.length} chars\n\n`);
|
|
785
832
|
const reader = res.body.getReader();
|
|
786
833
|
const split = new SseFrameSplitter();
|
|
787
834
|
let sawContent = false;
|
|
@@ -826,7 +873,8 @@ export class MidstreamGuard {
|
|
|
826
873
|
return 'unfinished';
|
|
827
874
|
}
|
|
828
875
|
if (splicer.terminalSeen) {
|
|
829
|
-
|
|
876
|
+
const by = splicer.resumeModel && splicer.resumeModel !== 'claude' ? ` by ${splicer.resumeModel}` : '';
|
|
877
|
+
this.log(`#${this.o.requestNo} continuation done${by}: +${st.emitted} chars in ${Date.now() - startedAt}ms (anchor ${st.anchor}, trimmed ${st.dropped})`);
|
|
830
878
|
return 'finished';
|
|
831
879
|
}
|
|
832
880
|
// The resume body ended without its terminal event — a reset on the
|
package/dist/proxy.js
CHANGED
|
@@ -26,7 +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
|
+
import { MidstreamGuard, guardFor, loopbackBaseFor, CONTINUATION_HEADER, MAX_CONTINUATION_DEPTH, continuationDepth } from './midstream.js';
|
|
30
30
|
import { isClaudeServableModel } from './claude-model.js';
|
|
31
31
|
import { MODEL_UNROUTABLE } from './upstream-rejection.js';
|
|
32
32
|
import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
|
|
@@ -2952,9 +2952,21 @@ export async function startProxy(opts = {}) {
|
|
|
2952
2952
|
// re-issues the CLIENT's request, not the rewritten one, so dario's own
|
|
2953
2953
|
// rules apply to the resume the same way they applied to the original.
|
|
2954
2954
|
const clientBodyBytes = body;
|
|
2955
|
-
//
|
|
2956
|
-
//
|
|
2957
|
-
|
|
2955
|
+
// How deep in a continuation chain this request sits: 0 for a client
|
|
2956
|
+
// request, 1 for its resume, 2 for the resume of that resume — which is
|
|
2957
|
+
// never continued itself (MAX_CONTINUATION_DEPTH).
|
|
2958
|
+
const requestDepth = continuationDepth(req.headers[CONTINUATION_HEADER]);
|
|
2959
|
+
const isContinuation = requestDepth >= MAX_CONTINUATION_DEPTH;
|
|
2960
|
+
/**
|
|
2961
|
+
* First hop: the SAME model again, through the front door. The pool
|
|
2962
|
+
* picks a seat (sticky binding keeps the prompt cache warm), and if the
|
|
2963
|
+
* provider cannot take it at all the existing pre-byte failover already
|
|
2964
|
+
* hands it to the other one. Null when the client named no model.
|
|
2965
|
+
*/
|
|
2966
|
+
const sameModelTarget = () => {
|
|
2967
|
+
const m = parseClientBody()?.model;
|
|
2968
|
+
return typeof m === 'string' && m.length > 0 ? { model: m, label: `${m} (same model)` } : null;
|
|
2969
|
+
};
|
|
2958
2970
|
const loopbackHeaders = () => {
|
|
2959
2971
|
const h = {};
|
|
2960
2972
|
if (apiKey)
|
|
@@ -3373,12 +3385,13 @@ export async function startProxy(opts = {}) {
|
|
|
3373
3385
|
res.write(chunk); },
|
|
3374
3386
|
isClientGone: () => res.destroyed || res.writableEnded,
|
|
3375
3387
|
requestNo: codexReq,
|
|
3388
|
+
depth: requestDepth,
|
|
3376
3389
|
verbose,
|
|
3377
3390
|
resume: {
|
|
3378
3391
|
clientBody: parseClientBody,
|
|
3379
3392
|
loopbackBase,
|
|
3380
3393
|
loopbackHeaders: loopbackHeaders(),
|
|
3381
|
-
resolveTarget: async () => claudeContinuation,
|
|
3394
|
+
resolveTarget: async (hop) => hop === 1 ? sameModelTarget() : claudeContinuation,
|
|
3382
3395
|
onBeforeResume: releaseQueueSlot,
|
|
3383
3396
|
timeoutMs: upstreamTimeoutMs,
|
|
3384
3397
|
},
|
|
@@ -4704,12 +4717,13 @@ export async function startProxy(opts = {}) {
|
|
|
4704
4717
|
res.end(); },
|
|
4705
4718
|
isClientGone: () => clientDisconnected || res.destroyed || upstreamAbortReason === 'client_closed' || upstreamAbortReason === 'sse_overflow',
|
|
4706
4719
|
requestNo: requestCount,
|
|
4720
|
+
depth: requestDepth,
|
|
4707
4721
|
verbose,
|
|
4708
4722
|
resume: {
|
|
4709
4723
|
clientBody: parseClientBody,
|
|
4710
4724
|
loopbackBase,
|
|
4711
4725
|
loopbackHeaders: loopbackHeaders(),
|
|
4712
|
-
resolveTarget: codexContinuationTarget,
|
|
4726
|
+
resolveTarget: async (hop) => hop === 1 ? sameModelTarget() : codexContinuationTarget(),
|
|
4713
4727
|
onBeforeResume: releaseQueueSlot,
|
|
4714
4728
|
timeoutMs: upstreamTimeoutMs,
|
|
4715
4729
|
},
|
|
@@ -46,23 +46,34 @@ Non-streaming requests are untouched; nothing was on the wire.
|
|
|
46
46
|
|
|
47
47
|
Through dario's own front door. The resume is a loopback `POST` to the same
|
|
48
48
|
proxy, so the pool, the codex translator, cch, the template, every rule that
|
|
49
|
-
applied to the original request applies to the resume.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
The
|
|
54
|
-
a
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
applied to the original request applies to the resume.
|
|
50
|
+
|
|
51
|
+
Two choices, in order:
|
|
52
|
+
|
|
53
|
+
1. **The same model again.** A fresh request for the model the client asked
|
|
54
|
+
for. The pool picks a seat — the sticky binding keeps the prompt cache warm
|
|
55
|
+
— and if the provider cannot take the request at all, the existing pre-byte
|
|
56
|
+
failover already hands it to the other one. A transient reset therefore
|
|
57
|
+
finishes on the model the user chose, with no chain configured. This is
|
|
58
|
+
what most streams that die get, and most users have one plan.
|
|
59
|
+
2. **The other provider's entry in `--pool-fallback`**, exactly the entry a
|
|
60
|
+
mid-flight 429 would use: a Claude stream goes to the codex half of the
|
|
61
|
+
chain (`gpt-5.6-sol` in `--pool-fallback=gpt-5.6-sol,claude:claude-sonnet-5`),
|
|
62
|
+
resolved at failure time against the account's live model list; a codex
|
|
63
|
+
stream goes to the Claude half, resolved against the live catalog.
|
|
64
|
+
|
|
65
|
+
Choice 2 is taken when choice 1 delivers nothing — refused, unreachable, dead
|
|
66
|
+
before its first byte — or when the resume itself dies mid-way. In the second
|
|
67
|
+
case the resume's own guard makes the hop, so the client stream carries two
|
|
68
|
+
seams: `(same model)` then `(codex live)`. The loopback carries
|
|
69
|
+
`x-dario-continuation: <depth>`; a request at depth 2 is never continued. Two
|
|
70
|
+
hops, never three: a third would be a third attempt at whatever is failing.
|
|
71
|
+
|
|
72
|
+
With only one plan and no chain, a stream whose same-model resume also fails
|
|
73
|
+
ends where the resume stopped, and the log says why once:
|
|
64
74
|
|
|
65
75
|
```
|
|
76
|
+
[dario] #42 continuation as claude-opus-5 (same model) delivered nothing — trying the next choice
|
|
66
77
|
[dario] #42 stream died after 1240 chars — no continuation target (set --pool-fallback with an entry for the other provider)
|
|
67
78
|
```
|
|
68
79
|
|
|
@@ -120,7 +131,7 @@ is never closed with a synthetic `end_turn`; only the resume's own
|
|
|
120
131
|
|---|---|
|
|
121
132
|
| `--no-midstream-continue` | off for this proxy |
|
|
122
133
|
| `DARIO_MIDSTREAM_CONTINUE=0` | same, for the container |
|
|
123
|
-
| `--pool-fallback=…` | where
|
|
134
|
+
| `--pool-fallback=…` | where the second hop goes; without an entry for the other provider a stream gets the same-model resume only |
|
|
124
135
|
|
|
125
136
|
On by default: it only ever acts where the alternative is a broken stream.
|
|
126
137
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.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": {
|