@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 CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  <p><strong>One local endpoint. Every AI tool you own. The subscriptions you already pay for.</strong></p>
29
29
 
30
- <sub><code>npm i -g @askalf/dario</code> · <strong>0</strong> runtime deps · <a href="https://www.npmjs.com/package/@askalf/dario">SLSA-attested</a> every release · nothing phones home · ~33k lines you can read in a weekend · independent, unofficial, third-party (<a href="DISCLAIMER.md">DISCLAIMER.md</a>)</sub>
30
+ <sub><code>npm i -g @askalf/dario</code> · <strong>0</strong> runtime deps · <a href="https://www.npmjs.com/package/@askalf/dario">SLSA-attested</a> every release · nothing phones home · ~34k lines you can read in a weekend · independent, unofficial, third-party (<a href="DISCLAIMER.md">DISCLAIMER.md</a>)</sub>
31
31
 
32
32
  <sub><a href="#start-in-60-seconds">Start</a> · <a href="#point-your-tools-at-it">Your tools</a> · <a href="#what-it-does-with-a-request">Routing</a> · <a href="#two-plans-one-endpoint">Two plans</a> · <a href="#many-seats-one-endpoint">Pool</a> · <a href="#it-tracks-a-moving-target">Drift</a> · <a href="#trust--transparency">Trust</a> · <a href="#will-my-account-get-suspended">Risk</a> · <a href="#commands">Commands</a> · <a href="#faq">FAQ</a> · <a href="docs/returning.md">Coming back after a while?</a></sub>
33
33
 
@@ -289,6 +289,8 @@ 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 from the other subscription — the resume picks up inside the still-open content block, a comment marks the seam (`: dario continuation gpt-5.6-sol (codex live) 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, and without a chain entry for the other provider it is inert. [How it works](docs/midstream-continuation.md).
293
+
292
294
  `dario doctor` tells you which of these you are actually in:
293
295
 
294
296
  ```
package/dist/cli.js CHANGED
@@ -625,6 +625,11 @@ async function proxy() {
625
625
  // ProxyOptions.honorClientThinking for rationale.
626
626
  const honorClientThinking = args.includes('--honor-client-thinking')
627
627
  || ['1', 'true', 'yes', 'on'].includes((process.env['DARIO_HONOR_CLIENT_THINKING'] ?? '').toLowerCase());
628
+ // --no-midstream-continue / DARIO_MIDSTREAM_CONTINUE=0 — turn off finishing
629
+ // a streamed answer that dies mid-way from the other subscription (v6.1).
630
+ // On by default; see ProxyOptions.midstreamContinue.
631
+ const midstreamContinue = !(args.includes('--no-midstream-continue')
632
+ || ['0', 'false', 'no', 'off'].includes((process.env['DARIO_MIDSTREAM_CONTINUE'] ?? '').toLowerCase()));
628
633
  // --preserve-output-format — carry the client body's `output_config.format`
629
634
  // (structured-output JSON schema) through to upstream instead of dropping it
630
635
  // during the CC rebuild. See ProxyOptions.preserveOutputFormat for rationale.
@@ -649,7 +654,7 @@ async function proxy() {
649
654
  console.error(`[dario] Override (not recommended): pass --unsafe-no-auth if you have out-of-band network controls and accept the risk.`);
650
655
  process.exit(1);
651
656
  }
652
- await startProxy({ port, host, verbose, verboseBodies, model, fastModel, noClaudeAuth, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, maxConcurrentPerConsumer, poolStrategy, poolSharedState, poolSharedStateIntervalMs, effort, maxTokens, poolFallbackModel, modelAliases, logFile, passthroughBetas, skipFields, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs, honorClientThinking, preserveOutputFormat });
657
+ await startProxy({ port, host, verbose, verboseBodies, model, fastModel, noClaudeAuth, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, maxConcurrentPerConsumer, poolStrategy, poolSharedState, poolSharedStateIntervalMs, effort, maxTokens, poolFallbackModel, modelAliases, logFile, passthroughBetas, skipFields, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs, honorClientThinking, preserveOutputFormat, midstreamContinue });
653
658
  }
654
659
  /**
655
660
  * Parse `--system-prompt=<verbatim|partial|aggressive|filepath>` (or the
@@ -1686,6 +1691,19 @@ async function help() {
1686
1691
  is fully generated even if nobody reads
1687
1692
  it) for fingerprint fidelity. Bounded by
1688
1693
  the 5-minute upstream timeout. (v3.25)
1694
+ --no-midstream-continue Do not finish a streamed answer that dies
1695
+ mid-way from the other subscription. By
1696
+ default a stream that breaks with content
1697
+ already on the wire (socket reset,
1698
+ overloaded_error, codex response.failed)
1699
+ is resumed through dario's own front door
1700
+ at the other provider's entry in
1701
+ --pool-fallback, spliced onto the same
1702
+ client stream, and closed cleanly; the
1703
+ client sees one message. Off, or with no
1704
+ fallback entry for the other provider,
1705
+ the stream ends truncated as before.
1706
+ Env: DARIO_MIDSTREAM_CONTINUE=0. (v6.1)
1689
1707
  --session-idle-rotate=MS Idle ms before an account's session id
1690
1708
  rotates (default: 900000 = 15 min).
1691
1709
  Real CC rotates once per conversation, not
@@ -3,6 +3,7 @@ import type { CodexAccountCredentials } from './codex-accounts.js';
3
3
  import { type ResponsesReasoningConfig, type ResponsesUsage } from './anthropic-responses-translate.js';
4
4
  import { type ModelResolver, type ClaudeTarget } from './claude-model.js';
5
5
  import { type EffortValue } from './effort.js';
6
+ import type { MidstreamGuard } from './midstream.js';
6
7
  export declare const CODEX_BACKEND_BASE_URL: string;
7
8
  /**
8
9
  * Client version sent on the model-discovery call. The backend REQUIRES the
@@ -143,6 +144,13 @@ export type CodexRequestShape = 'openai' | 'anthropic';
143
144
  * the chat-path half was the same bug, pre-existing.
144
145
  */
145
146
  export declare function isTerminalResponsesEvent(type: string): boolean;
147
+ /**
148
+ * Whether a raw Responses SSE line carries a failed terminal event — the same
149
+ * test the chat translator applies once it has parsed the line, run here so a
150
+ * mid-stream continuation guard learns of the failure before the translator's
151
+ * error + `[DONE]` frames are written.
152
+ */
153
+ export declare function isFailedResponsesLine(line: string): boolean;
146
154
  /** True when a terminal Responses payload describes a FAILURE rather than a turn. */
147
155
  export declare function isFailedResponse(resp: unknown): boolean;
148
156
  /** The upstream message on a failed Responses payload, for the error body. */
@@ -289,4 +297,11 @@ export declare function forwardToCodex(req: IncomingMessage, res: ServerResponse
289
297
  * a chat/completions caller sets `reasoning_effort` itself and that already
290
298
  * translates. Undefined leaves the request exactly as it was.
291
299
  */
292
- effort?: ResponsesReasoningConfig['effort']): Promise<boolean>;
300
+ effort?: ResponsesReasoningConfig['effort'],
301
+ /**
302
+ * Mid-stream continuation guard (v6.1, src/midstream.ts). When present,
303
+ * every streamed frame goes through it and it owns the streaming exits: a
304
+ * stream that dies with content on the wire is finished from the Claude
305
+ * pool instead of ending truncated.
306
+ */
307
+ midstream?: MidstreamGuard | null): Promise<boolean>;
@@ -419,6 +419,23 @@ export function chatCompletionsToResponses(body) {
419
419
  export function isTerminalResponsesEvent(type) {
420
420
  return type === 'response.completed' || type === 'response.incomplete' || type === 'response.failed';
421
421
  }
422
+ /**
423
+ * Whether a raw Responses SSE line carries a failed terminal event — the same
424
+ * test the chat translator applies once it has parsed the line, run here so a
425
+ * mid-stream continuation guard learns of the failure before the translator's
426
+ * error + `[DONE]` frames are written.
427
+ */
428
+ export function isFailedResponsesLine(line) {
429
+ if (!line.startsWith('data: ') || !line.includes('"response.'))
430
+ return false;
431
+ try {
432
+ const e = JSON.parse(line.slice(6));
433
+ return e.type === 'response.failed' || (isTerminalResponsesEvent(e.type ?? '') && isFailedResponse(e.response));
434
+ }
435
+ catch {
436
+ return false;
437
+ }
438
+ }
422
439
  /** True when a terminal Responses payload describes a FAILURE rather than a turn. */
423
440
  export function isFailedResponse(resp) {
424
441
  if (!resp || typeof resp !== 'object')
@@ -761,7 +778,14 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
761
778
  * a chat/completions caller sets `reasoning_effort` itself and that already
762
779
  * translates. Undefined leaves the request exactly as it was.
763
780
  */
764
- effort) {
781
+ effort,
782
+ /**
783
+ * Mid-stream continuation guard (v6.1, src/midstream.ts). When present,
784
+ * every streamed frame goes through it and it owns the streaming exits: a
785
+ * stream that dies with content on the wire is finished from the Claude
786
+ * pool instead of ending truncated.
787
+ */
788
+ midstream) {
765
789
  void req;
766
790
  const isAnthropic = shape === 'anthropic';
767
791
  // Reported exactly once, on every exit that answered the client. Without
@@ -832,8 +856,22 @@ effort) {
832
856
  res.on('close', onClientClose);
833
857
  // Every streamed write goes through here: after a disconnect the socket is
834
858
  // gone and writing to it is wasted at best, an EPIPE at worst.
835
- const write = (chunk) => { if (!clientGone)
836
- res.write(chunk); };
859
+ const write = (chunk) => {
860
+ if (clientGone)
861
+ return;
862
+ if (midstream)
863
+ midstream.write(chunk);
864
+ else
865
+ res.write(chunk);
866
+ };
867
+ // The streaming exits: the guard decides whether the stream is complete,
868
+ // continuable, or simply over.
869
+ const endStream = async () => {
870
+ if (midstream)
871
+ await midstream.finish();
872
+ else
873
+ res.end();
874
+ };
837
875
  // Usage seen so far, so a stream the client abandoned still reports what the
838
876
  // subscription already spent. Populated once the translators exist.
839
877
  let usageSoFar = () => null;
@@ -908,6 +946,11 @@ effort) {
908
946
  terminalResponse = r;
909
947
  if (t === 'response.failed')
910
948
  anthropicFailed = true;
949
+ // Flag it BEFORE the translator closes the turn: on this shape a
950
+ // failed turn still ends in message_delta + message_stop, which the
951
+ // guard must withhold to see the stream as unfinished.
952
+ if (midstream && (anthropicFailed || isFailedResponse(r)))
953
+ midstream.markUpstreamFailed();
911
954
  }
912
955
  const produced = antTranslator.push(ev);
913
956
  if (!clientWantsStream)
@@ -951,6 +994,8 @@ effort) {
951
994
  const lines = buffered.split('\n');
952
995
  buffered = lines.pop() ?? '';
953
996
  for (const line of lines) {
997
+ if (midstream && isFailedResponsesLine(line))
998
+ midstream.markUpstreamFailed();
954
999
  const out = translator.chunk(line);
955
1000
  if (out && clientWantsStream)
956
1001
  write(out);
@@ -999,7 +1044,7 @@ effort) {
999
1044
  for (const out of antTranslator.end())
1000
1045
  write(formatResponsesAnthropicSSE(out));
1001
1046
  finished = true;
1002
- res.end();
1047
+ await endStream();
1003
1048
  }
1004
1049
  else {
1005
1050
  res.writeHead(200, {
@@ -1014,7 +1059,7 @@ effort) {
1014
1059
  }
1015
1060
  else if (clientWantsStream) {
1016
1061
  finished = true;
1017
- res.end();
1062
+ await endStream();
1018
1063
  }
1019
1064
  else {
1020
1065
  res.writeHead(200, {
@@ -1072,9 +1117,12 @@ effort) {
1072
1117
  res.end(errBody('Upstream Codex backend error', { account: creds.alias }));
1073
1118
  }
1074
1119
  else {
1120
+ // Headers are out and the socket reset under a live stream — the case
1121
+ // the guard exists for. Without one this is the truncated stream it
1122
+ // always was.
1075
1123
  finished = true;
1076
1124
  try {
1077
- res.end();
1125
+ await endStream();
1078
1126
  }
1079
1127
  catch { /* already closed */ }
1080
1128
  }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Mid-stream continuation (v6.1) — the answer does not stop when the plan does.
3
+ *
4
+ * Until now a streamed `/v1/messages` (or `/v1/chat/completions`) answer that
5
+ * died part-way through — an upstream socket reset, an in-band
6
+ * `overloaded_error`, a codex `response.failed` — ended with `res.end()` and
7
+ * the client got a truncated stream: no `message_stop`, no `[DONE]`, an SDK
8
+ * that throws "stream ended without producing a Message", and every word
9
+ * already on screen wasted. Once bytes were on the wire the request was
10
+ * treated as too late to hand to anyone else.
11
+ *
12
+ * This module finishes the SAME client stream from the other subscription
13
+ * instead. It sits between the request handler and the client socket:
14
+ *
15
+ * 1. every frame written to the client passes through `write()`, which
16
+ * tracks what the client has already seen (message_start, the open
17
+ * content block, the text so far) and WITHHOLDS a terminal error frame
18
+ * rather than forwarding it;
19
+ * 2. `finish()` replaces the site's `res.end()`. A clean stream ends as
20
+ * before. A stream that died with content on the wire re-issues the
21
+ * request through dario's own front door (a loopback POST — so the pool,
22
+ * the codex translator, cch and every other rule apply to the resume
23
+ * exactly as to any client request) at the OTHER provider, with the
24
+ * partial answer appended as the assistant turn and a resume notice as
25
+ * the user turn;
26
+ * 3. the resume stream is spliced onto the client's still-open block: its
27
+ * message_start and thinking blocks are dropped, its first text block
28
+ * continues the open index, anything after that is renumbered, and it
29
+ * closes the message with its own message_delta / message_stop.
30
+ *
31
+ * Two things the spike (2026-09-11, prod 6.0.51, real Opus 5 + real ChatGPT
32
+ * Plus) settled that are easy to get wrong again:
33
+ *
34
+ * - NO assistant prefill. Claude 4.6+/5 answers a trailing assistant turn
35
+ * with a 400, and the Responses API never had the concept. The resume is
36
+ * instruction-driven on both providers, which works — zero restarts, zero
37
+ * preamble, zero repetition across ten real runs.
38
+ * - The seam is a WHITESPACE problem, not a content problem. Told merely to
39
+ * "continue", Claude-as-continuer dropped the boundary space 2/3 times
40
+ * (`replies<CUT>with`, a 3-vs-4-space indent). So the notice asks the model
41
+ * to begin by repeating the last ~40 characters verbatim, and `findAnchor`
42
+ * trims that repeat with a whitespace-normalized match. The model renders
43
+ * the seam inside its own token stream; we only cut. Matched exactly 5/5.
44
+ *
45
+ * Out of scope here, on purpose: a cut inside a tool_use block (the partial
46
+ * JSON is not resumable), non-streaming requests (nothing is on the wire yet;
47
+ * the existing pre-byte failover covers them), and the api-key OpenAI backend.
48
+ * A stream that cannot be continued ends exactly as it did before this module.
49
+ */
50
+ import type { ServerResponse } from 'node:http';
51
+ export type WireShape = 'anthropic' | 'openai';
52
+ /** Client-visible marker that a loopback request is a continuation, so the handler never nests one. */
53
+ export declare const CONTINUATION_HEADER = "x-dario-continuation";
54
+ /** Characters of the partial the model is asked to repeat verbatim (the seam anchor). */
55
+ export declare const ANCHOR_CHARS = 40;
56
+ export interface SseFrame {
57
+ /** The frame exactly as it will go on the wire, trailing blank line included. */
58
+ raw: string;
59
+ /** `event:` field, or the JSON `type` when the event line is absent. */
60
+ event: string;
61
+ /** Parsed `data:` payload, null for comments and non-JSON data. */
62
+ data: Record<string, unknown> | null;
63
+ /** The literal data text (`[DONE]` for the OpenAI sentinel). */
64
+ dataText: string | null;
65
+ comment: boolean;
66
+ }
67
+ /**
68
+ * Splits a byte/text stream into complete SSE frames. A trailing partial frame
69
+ * stays buffered until its blank line arrives. Frames are returned with their
70
+ * original bytes, so forwarding `raw` is byte-identical to the input.
71
+ */
72
+ export declare class SseFrameSplitter {
73
+ private buf;
74
+ private readonly decoder;
75
+ feed(chunk: string | Uint8Array): SseFrame[];
76
+ /** Whatever is buffered and not yet a complete frame. */
77
+ flush(): string;
78
+ }
79
+ export declare function parseFrame(raw: string): SseFrame;
80
+ export declare function formatFrame(event: string, data: unknown): string;
81
+ interface BlockState {
82
+ type: string;
83
+ open: boolean;
84
+ text: string;
85
+ }
86
+ /**
87
+ * The client-side state a continuation has to pick up from. Both shapes are
88
+ * tracked by ONE class so the guard has a single view: `blocks` carries
89
+ * Anthropic content blocks; on the OpenAI shape there is exactly one implicit
90
+ * text block (index 0) that opens on the first content delta.
91
+ */
92
+ export declare class ClientStreamState {
93
+ readonly shape: WireShape;
94
+ started: boolean;
95
+ finished: boolean;
96
+ blocks: BlockState[];
97
+ /**
98
+ * Frames the guard withheld instead of forwarding, in order: a terminal
99
+ * error, and — once the site has flagged the upstream as failed — the
100
+ * closing frames a translator emits for a failed turn. Released verbatim
101
+ * if no continuation happens, so the client sees exactly what it would have.
102
+ */
103
+ withheld: SseFrame[];
104
+ /**
105
+ * Set by the site when it KNOWS the upstream turn failed even though the
106
+ * translator will close it politely (the codex Anthropic path answers
107
+ * `response.failed` with message_delta + message_stop). The closing frames
108
+ * are then withheld so the stream reads as unfinished, i.e. continuable.
109
+ */
110
+ upstreamFailed: boolean;
111
+ /** True once anything non-continuable was seen (tool_use in progress, tool_calls). */
112
+ toolInProgress: boolean;
113
+ forwardedFrames: number;
114
+ /** message_start's message.model — kept so a continuation can name what the client believes it is talking to. */
115
+ model: string | null;
116
+ constructor(shape: WireShape);
117
+ get openIdx(): number;
118
+ get openType(): string | null;
119
+ /** Every text emitted so far, blocks concatenated in order. */
120
+ get textSoFar(): string;
121
+ /** Text of the open text block only — what the seam anchor is cut from. */
122
+ get openText(): string;
123
+ /**
124
+ * Whether a stream that stopped HERE can be continued: bytes are on the
125
+ * wire, the message is not finished, and nothing non-resumable is open.
126
+ * An open tool_use block or an OpenAI tool call in flight is a definite no —
127
+ * half a JSON argument object cannot be handed to another model.
128
+ */
129
+ get continuable(): boolean;
130
+ /**
131
+ * Observe one client-bound frame. Returns false when the frame is a terminal
132
+ * error the guard should withhold (recorded in `withheld`), true to forward.
133
+ */
134
+ observe(f: SseFrame): boolean;
135
+ private observeAnthropic;
136
+ private observeOpenAI;
137
+ }
138
+ /**
139
+ * The tail of the partial the model is told to repeat. Starts at a
140
+ * non-whitespace character: the API strips a reply's leading whitespace, so an
141
+ * anchor beginning with a space could never be matched exactly.
142
+ */
143
+ export declare function anchorOf(partial: string): string;
144
+ /**
145
+ * Locate the repeated anchor at the head of the continuation and return the
146
+ * raw offset just past it, or null when the model did not repeat it. Tries the
147
+ * whole anchor first, then shorter tails, tolerating whitespace and quote
148
+ * differences; the match must sit at (or within a few characters of) the
149
+ * start, so a genuine later recurrence of the phrase is never mistaken for it.
150
+ */
151
+ export declare function findAnchor(partial: string, head: string): {
152
+ cut: number;
153
+ exact: boolean;
154
+ } | null;
155
+ /** Longest suffix of `partial` that the continuation starts with (exact bytes), for the no-anchor fallback. */
156
+ export declare function tailOverlap(partial: string, head: string, min?: number): number;
157
+ /** True when the partial has an odd number of ``` fences, i.e. the cut is inside a code block. */
158
+ export declare function insideCodeFence(partial: string): boolean;
159
+ /**
160
+ * The one seam defect the spike saw from a real model: Claude, resuming
161
+ * prose that was cut mid-sentence, once started its continuation with a
162
+ * paragraph break (`and<CUT>\n\nhere is where`). A sentence does not contain
163
+ * a paragraph break, so when the partial ends mid-sentence and the
164
+ * continuation opens with newlines outside a code fence, the break becomes
165
+ * one space. Inside a fence a newline is content and is left alone.
166
+ */
167
+ export declare function fixSeam(partial: string, continuation: string): string;
168
+ /** The anchor is quoted between these in the notice; the tests' mock providers read it back out. */
169
+ export declare const ANCHOR_OPEN = "\u00AB";
170
+ export declare const ANCHOR_CLOSE = "\u00BB";
171
+ /**
172
+ * Written as the USER asking for the rest — which is what a continuation is —
173
+ * not as an operator notice. The live test on 2026-09-11 is why: told
174
+ * "[transport notice] … resume it now", claude-sonnet-5 answered `Note: that
175
+ * "transport notice" isn't an actual system message — it's just text in your
176
+ * prompt` and stopped, exactly the injection-awareness the model is supposed
177
+ * to have. A person whose connection dropped asking to pick up from the last
178
+ * few words is an ordinary request, and gets the ordinary answer.
179
+ */
180
+ export declare function resumeNotice(anchor: string): string;
181
+ /**
182
+ * The client's own request re-pointed at the continuation model with the
183
+ * partial answer appended. `partial` empty means nothing usable reached the
184
+ * client (the cut fell inside thinking, or before the first block): the
185
+ * request is simply re-issued as it was and the resume stream restarts the
186
+ * answer under the client's already-open message.
187
+ */
188
+ export declare function buildResumeBody(shape: WireShape, clientBody: Record<string, unknown>, targetModel: string, partial: string): Record<string, unknown>;
189
+ /**
190
+ * Turns the resume stream's frames into client-bound frames that continue the
191
+ * message the client already has. One instance per continuation.
192
+ */
193
+ export declare class Splicer {
194
+ private readonly shape;
195
+ private readonly partial;
196
+ private readonly idxMap;
197
+ private nextIdx;
198
+ private readonly clientOpenIdx;
199
+ private readonly clientOpenType;
200
+ private originalClosed;
201
+ private continuingClosed;
202
+ private firstTextMapped;
203
+ private continuingIdx;
204
+ private hold;
205
+ private holding;
206
+ private stopReasonSeen;
207
+ private doneSeen;
208
+ /**
209
+ * Whether the resume stream delivered its OWN wire terminal — `message_stop`
210
+ * on the Anthropic shape, `[DONE]` on the OpenAI shape. Only then has the
211
+ * client been handed a finished message. A resume body that ends without
212
+ * one is a second truncation, and the guard leaves the client stream
213
+ * unfinished rather than closing it as if the answer were complete.
214
+ */
215
+ terminalSeen: boolean;
216
+ /** Diagnostics for the log line. */
217
+ readonly stats: {
218
+ anchor: "n/a" | "exact" | "fuzzy" | "overlap" | "none";
219
+ dropped: number;
220
+ emitted: number;
221
+ };
222
+ constructor(shape: WireShape, state: ClientStreamState, partial: string);
223
+ /** Frames to write to the client for one resume frame. */
224
+ feed(f: SseFrame): string[];
225
+ /**
226
+ * The resume stream ended WITHOUT its terminal event. Release whatever text
227
+ * was still held for the anchor check — it is the second provider's real
228
+ * output and the client may as well have it — but close nothing: no
229
+ * content_block_stop, no message_delta/message_stop, no finish chunk, no
230
+ * [DONE]. A synthesized clean end here would present a doubly-truncated
231
+ * answer as a complete one (review finding on #1286).
232
+ */
233
+ abandon(): string[];
234
+ private feedAnthropic;
235
+ /** Close the block the resume has been writing into, once. */
236
+ private closeContinuing;
237
+ /** Close the block the client had open when the primary died, once, unless the resume is continuing it. */
238
+ private closeOriginal;
239
+ private feedOpenAI;
240
+ /**
241
+ * Text from the resume's first text block is held until the anchor is
242
+ * found (or enough has arrived to give up looking), then trimmed and
243
+ * released. After that every delta streams straight through.
244
+ */
245
+ private releaseHold;
246
+ }
247
+ export interface ContinuationTarget {
248
+ /** The model spelling the loopback request carries (`codex:gpt-5.6-terra:high`, `claude:claude-opus-5`). */
249
+ model: string;
250
+ /** Human-readable, for the log line and the SSE comment. */
251
+ label: string;
252
+ }
253
+ export interface ResumeOptions {
254
+ /**
255
+ * The client's request as it arrived, before dario's own rewrites. A
256
+ * function so the bytes are parsed only when a resume actually happens —
257
+ * never on the hot path of a stream that ends normally.
258
+ */
259
+ clientBody: () => Record<string, unknown> | null;
260
+ /** `http://127.0.0.1:<port>` — dario's own front door. */
261
+ loopbackBase: string;
262
+ /** Auth + attribution headers for the loopback request. */
263
+ loopbackHeaders: Record<string, string>;
264
+ /** Decides where to resume, once, at failure time. Null = nowhere; the stream ends as before. */
265
+ resolveTarget: () => Promise<ContinuationTarget | null>;
266
+ /** Called right before the loopback request is made — the site releases its own queue slot here. */
267
+ onBeforeResume?: () => void;
268
+ timeoutMs: number;
269
+ fetchImpl?: typeof fetch;
270
+ }
271
+ export interface MidstreamGuardOptions {
272
+ shape: WireShape;
273
+ /** The site's client writer (already gated on client disconnect). */
274
+ write: (chunk: string) => void;
275
+ /** The real `res.end()`. */
276
+ end: () => void;
277
+ isClientGone: () => boolean;
278
+ resume: ResumeOptions | null;
279
+ requestNo: number;
280
+ verbose: boolean;
281
+ log?: (line: string) => void;
282
+ }
283
+ export type FinishOutcome = 'clean' | 'continued' | 'continued-unfinished' | 'ended' | 'not-continuable' | 'no-target' | 'resume-failed';
284
+ export declare class MidstreamGuard {
285
+ private readonly o;
286
+ readonly state: ClientStreamState;
287
+ private readonly splitter;
288
+ private finished;
289
+ constructor(o: MidstreamGuardOptions);
290
+ /**
291
+ * The site knows the upstream turn failed (a codex `response.failed`, a
292
+ * terminal payload with an error status) before the translator's polite
293
+ * closing frames go out. From here on those frames are withheld, so the
294
+ * stream is treated as unfinished — and finished from the other provider.
295
+ */
296
+ markUpstreamFailed(): void;
297
+ /** Forward a client-bound chunk, withholding a terminal error frame. */
298
+ write(chunk: string | Uint8Array): void;
299
+ /**
300
+ * End the client response. Replaces the site's `res.end()` on the streaming
301
+ * exits. Resolves once the client stream is closed either way.
302
+ */
303
+ finish(): Promise<FinishOutcome>;
304
+ /**
305
+ * 'failed': nothing of the resume reached the client — the site ends the
306
+ * stream exactly as it would have. 'finished': the resume delivered its
307
+ * terminal event and the client holds one complete message. 'unfinished':
308
+ * the resume put content on the wire and then died too; the stream is left
309
+ * open-ended (no synthesized close) so the client sees the truncation.
310
+ */
311
+ private continueFrom;
312
+ private log;
313
+ }
314
+ /** Convenience for sites that hold a ServerResponse: the guard writes through `write`, ends through `res.end()`. */
315
+ export declare function guardFor(res: ServerResponse, o: Omit<MidstreamGuardOptions, 'end'>): MidstreamGuard;
316
+ /**
317
+ * The loopback origin for a bound listen address. A wildcard bind is reached
318
+ * on the loopback interface; a specific address is reached on itself.
319
+ */
320
+ export declare function loopbackBaseFor(host: string, port: number): string;
321
+ export {};