clauderipple 0.9.3 → 0.9.5
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/CHANGELOG.md +41 -0
- package/dist/app/package.json +1 -1
- package/dist/mod/hooks/i18n.ts +36 -0
- package/dist/mod/hooks/register.tsx +8 -4
- package/dist/mod/hooks/session.ts +2 -2
- package/dist/mod/hooks/ui/alerts.tsx +10 -0
- package/dist/mod/hooks/ui/art.ts +64 -1
- package/dist/mod/hooks/ui/band.tsx +70 -42
- package/dist/mod/hooks/ui/kit.tsx +34 -2
- package/dist/mod/hooks/ui/view.ts +5 -0
- package/dist/mod/tests/register.test.tsx +31 -3
- package/dist/mod/tests/ui.test.tsx +108 -18
- package/dist/mod/types/index.d.ts +1 -1
- package/dist/router/src/admin.js +65 -6
- package/dist/router/src/agents.js +5 -0
- package/dist/router/src/catalog.js +18 -8
- package/dist/router/src/index.js +13 -4
- package/dist/router/src/presets.js +3 -0
- package/dist/router/src/providers/chatgpt/index.js +49 -11
- package/dist/router/src/providers/chatgpt/translate.js +147 -7
- package/dist/router/src/providers/openai/index.js +29 -5
- package/dist/router/src/providers/openai/translate.js +247 -25
- package/dist/router/src/providers/retry.js +15 -0
- package/dist/router/src/reasoning.js +157 -0
- package/dist/router/src/thinking.js +20 -9
- package/dist/router/src/version.js +1 -1
- package/docs/ARCHITECTURE.md +56 -4
- package/package.json +3 -2
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
// Prompt-cache rule: the Responses `input` we build must be a byte-stable prefix of the
|
|
4
4
|
// next turn's `input`. Claude Code resends the whole history every turn, so the mapping
|
|
5
5
|
// has to be deterministic and must not inject anything that varies (timestamps, salts,
|
|
6
|
-
// re-signed reasoning).
|
|
7
|
-
//
|
|
6
|
+
// re-signed reasoning). A reasoning item that came back from the backend is the exception
|
|
7
|
+
// that proves the rule: it is put back exactly as it arrived, so the bytes are the
|
|
8
|
+
// backend's own, and a turn that carried one maps the same way every time. Thinking
|
|
9
|
+
// blocks with no stored item still have nothing to put back and are dropped as before.
|
|
10
|
+
// `prompt_cache_key` is derived from the conversation's first user message — or,
|
|
8
11
|
// for a request that is not a conversation at all, from its system prompt (see `conversationKey`).
|
|
9
12
|
import crypto from "node:crypto";
|
|
10
13
|
import { identityLine } from "../../identity.js";
|
|
14
|
+
import { encodeEnvelope, envelopeFor, isOurSignature, replayableReasoning } from "../../reasoning.js";
|
|
11
15
|
function blockText(c) {
|
|
12
16
|
if (typeof c === "string")
|
|
13
17
|
return c;
|
|
@@ -235,6 +239,7 @@ export function toResponsesRequest(req, opts) {
|
|
|
235
239
|
// whose call_id has no function_call in the same input ("No tool call found…", measured 2026-09-13).
|
|
236
240
|
// Such results are sent as plain user text instead.
|
|
237
241
|
const knownCalls = new Set();
|
|
242
|
+
let lastCallId;
|
|
238
243
|
for (const m of req.messages) {
|
|
239
244
|
const role = m.role === "assistant" ? "assistant" : "user";
|
|
240
245
|
if (typeof m.content === "string") {
|
|
@@ -251,6 +256,32 @@ export function toResponsesRequest(req, opts) {
|
|
|
251
256
|
pending = [];
|
|
252
257
|
};
|
|
253
258
|
for (const b of m.content) {
|
|
259
|
+
// The reasoning this turn came back with, put back exactly where it was: the CLI replays the
|
|
260
|
+
// block in front of the function calls it belongs to, so iterating in order reproduces the
|
|
261
|
+
// original positions, parallel calls included. `lastCallId` is the call the carrier arrived
|
|
262
|
+
// after — the response path addresses the next turn's summary to it (see `onArrival`) — and is
|
|
263
|
+
// undefined for a lead-in item, which is where such a summary belongs.
|
|
264
|
+
const decoded = envelopeFor(b, "responses", opts.env, (reason) => {
|
|
265
|
+
(opts.carried ?? (opts.carried = { replayed: 0, undecodable: 0 })).undecodable++;
|
|
266
|
+
if (opts.replay !== false)
|
|
267
|
+
opts.log?.(`dropped a reasoning carrier: ${reason}`);
|
|
268
|
+
});
|
|
269
|
+
// A carrier for the other wire is not ours to touch and falls through to the drop below.
|
|
270
|
+
if (decoded) {
|
|
271
|
+
if (opts.replay !== false) {
|
|
272
|
+
const item = replayableReasoning(decoded.r);
|
|
273
|
+
if (item) {
|
|
274
|
+
flush();
|
|
275
|
+
input.push(item);
|
|
276
|
+
(opts.carried ?? (opts.carried = { replayed: 0, undecodable: 0 })).replayed++;
|
|
277
|
+
opts.onArrival?.(item, lastCallId);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
(opts.carried ?? (opts.carried = { replayed: 0, undecodable: 0 })).undecodable++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
254
285
|
switch (b.type) {
|
|
255
286
|
case "text": {
|
|
256
287
|
const t = b.text;
|
|
@@ -272,6 +303,7 @@ export function toResponsesRequest(req, opts) {
|
|
|
272
303
|
flush();
|
|
273
304
|
const tu = b;
|
|
274
305
|
knownCalls.add(tu.id);
|
|
306
|
+
lastCallId = tu.id;
|
|
275
307
|
input.push({ type: "function_call", call_id: tu.id, name: toolNameForResponses(tu.name), arguments: typeof tu.input === "string" ? tu.input : JSON.stringify(tu.input ?? {}) });
|
|
276
308
|
break;
|
|
277
309
|
}
|
|
@@ -293,7 +325,8 @@ export function toResponsesRequest(req, opts) {
|
|
|
293
325
|
break;
|
|
294
326
|
}
|
|
295
327
|
default:
|
|
296
|
-
// thinking
|
|
328
|
+
// thinking, an unmarked redacted_thinking, anything unknown: no stored item to put back,
|
|
329
|
+
// so dropped as before (see header comment)
|
|
297
330
|
break;
|
|
298
331
|
}
|
|
299
332
|
}
|
|
@@ -327,6 +360,11 @@ export function toResponsesRequest(req, opts) {
|
|
|
327
360
|
prompt_cache_key: conversationId(req),
|
|
328
361
|
client_metadata: { session_id: conversationId(req), thread_id: conversationId(req), turn_id: crypto.randomUUID(), "x-codex-window-id": `${conversationId(req)}:0` },
|
|
329
362
|
};
|
|
363
|
+
// On every turn, as Codex asks. With `store: false` the backend keeps nothing, and without
|
|
364
|
+
// `include` it hands back no sealed item either — so a conversation that waited until it already
|
|
365
|
+
// carried reasoning before asking would never carry any. Only the retry after a refusal leaves it off.
|
|
366
|
+
if (opts.replay !== false)
|
|
367
|
+
out.include = ["reasoning.encrypted_content"];
|
|
330
368
|
if (tools.length > 0) {
|
|
331
369
|
out.tools = tools;
|
|
332
370
|
out.parallel_tool_calls = !(tc?.disable_parallel_tool_use ?? false);
|
|
@@ -348,6 +386,7 @@ export function estimateTokens(req) {
|
|
|
348
386
|
export class StreamMapper {
|
|
349
387
|
started = false;
|
|
350
388
|
blockIndex = -1;
|
|
389
|
+
/** `signed` marks a thinking block that already carries its signature — the envelope we wrote. */
|
|
351
390
|
open = null;
|
|
352
391
|
sawToolCall = false;
|
|
353
392
|
finished = false;
|
|
@@ -360,14 +399,30 @@ export class StreamMapper {
|
|
|
360
399
|
/** Accumulated content for non-streaming responses. */
|
|
361
400
|
content = [];
|
|
362
401
|
stopReason = "end_turn";
|
|
402
|
+
/**
|
|
403
|
+
* How many reasoning items this response carried whole — the ones the client will replay. The
|
|
404
|
+
* `reasoning=` figure the request log shows next to `replayed=`.
|
|
405
|
+
*/
|
|
406
|
+
reasoningCount = 0;
|
|
407
|
+
/** The reasoning item being assembled: its identity so far and the call it followed. */
|
|
408
|
+
pendingReasoning = null;
|
|
409
|
+
/** The most recent function_call item added, which is what a following reasoning item describes. */
|
|
410
|
+
lastCallId;
|
|
363
411
|
/** Input-token figure announced in message_start (the real one only arrives with response.completed). */
|
|
364
412
|
startInput;
|
|
365
413
|
/** Mangled tool name → the name Claude Code knows, from `toolNameRestoreMap`. */
|
|
366
414
|
toolNames;
|
|
367
|
-
|
|
415
|
+
/**
|
|
416
|
+
* The provider name that goes in each reasoning envelope (see reasoning.ts). A blob sealed here is
|
|
417
|
+
* only sent back here: reasoning outlives the request that made it, and a conversation moved to
|
|
418
|
+
* another provider must not carry this one's sealed state with it.
|
|
419
|
+
*/
|
|
420
|
+
provider;
|
|
421
|
+
constructor(model, startInput = 0, toolNames = new Map(), provider = "") {
|
|
368
422
|
this.model = model;
|
|
369
423
|
this.startInput = startInput;
|
|
370
424
|
this.toolNames = toolNames;
|
|
425
|
+
this.provider = provider;
|
|
371
426
|
this.messageId = `msg_${crypto.randomBytes(12).toString("hex")}`;
|
|
372
427
|
}
|
|
373
428
|
get isFinished() {
|
|
@@ -393,11 +448,17 @@ export class StreamMapper {
|
|
|
393
448
|
},
|
|
394
449
|
];
|
|
395
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* A thinking block must carry a signature even when there is none to give — an unsigned one is
|
|
453
|
+
* refused the moment the session moves to a Claude model (`thinking.ts`) — so a block that was
|
|
454
|
+
* opened without one gets an empty `signature_delta` as it closes. A block that already carries
|
|
455
|
+
* the envelope is left alone: a second, empty delta would replace it on the client.
|
|
456
|
+
*/
|
|
396
457
|
closeBlock() {
|
|
397
458
|
if (!this.open)
|
|
398
459
|
return [];
|
|
399
460
|
const evs = [];
|
|
400
|
-
if (this.open.kind === "thinking")
|
|
461
|
+
if (this.open.kind === "thinking" && !this.open.signed)
|
|
401
462
|
evs.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.blockIndex, delta: { type: "signature_delta", signature: "" } } });
|
|
402
463
|
evs.push({ event: "content_block_stop", data: { type: "content_block_stop", index: this.blockIndex } });
|
|
403
464
|
this.open = null;
|
|
@@ -410,6 +471,56 @@ export class StreamMapper {
|
|
|
410
471
|
evs.push({ event: "content_block_start", data: { type: "content_block_start", index: this.blockIndex, content_block: block } });
|
|
411
472
|
return evs;
|
|
412
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Finish the reasoning item being assembled and hand the client the block that carries it: a
|
|
476
|
+
* thinking block whose text is the summary (empty when the backend sent none — measured
|
|
477
|
+
* 2026-10-14: Claude Code keeps and replays such a block, signature and all) and whose signature
|
|
478
|
+
* is the envelope around the sealed reasoning.
|
|
479
|
+
*
|
|
480
|
+
* An item with nothing sealed is only worth its summary, and then only if it has one: there is
|
|
481
|
+
* nothing to replay, and an empty thinking block is refused by Anthropic the moment the session
|
|
482
|
+
* switches to a Claude model (`thinking.ts`).
|
|
483
|
+
*/
|
|
484
|
+
sealReasoning() {
|
|
485
|
+
const pending = this.pendingReasoning;
|
|
486
|
+
if (!pending)
|
|
487
|
+
return [];
|
|
488
|
+
this.pendingReasoning = null;
|
|
489
|
+
const text = pending.text.trim() || (typeof pending.summary === "string" ? pending.summary.trim() : "");
|
|
490
|
+
const events = [];
|
|
491
|
+
if (!pending.encrypted) {
|
|
492
|
+
if (text) {
|
|
493
|
+
events.push(...this.openBlock("thinking", { type: "thinking", thinking: "" }));
|
|
494
|
+
events.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.blockIndex, delta: { type: "thinking_delta", thinking: text } } });
|
|
495
|
+
events.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.blockIndex, delta: { type: "signature_delta", signature: "" } } });
|
|
496
|
+
events.push(...this.closeBlock());
|
|
497
|
+
this.content.push({ type: "thinking", thinking: text, signature: "" });
|
|
498
|
+
}
|
|
499
|
+
return events;
|
|
500
|
+
}
|
|
501
|
+
const item = { type: "reasoning", encrypted_content: pending.encrypted };
|
|
502
|
+
if (pending.id)
|
|
503
|
+
item.id = pending.id;
|
|
504
|
+
if (Array.isArray(pending.summary))
|
|
505
|
+
item.summary = pending.summary;
|
|
506
|
+
const signature = encodeEnvelope(this.envelopeFor(item));
|
|
507
|
+
events.push(...this.openBlock("thinking", { type: "thinking", thinking: "" }));
|
|
508
|
+
if (text)
|
|
509
|
+
events.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.blockIndex, delta: { type: "thinking_delta", thinking: text } } });
|
|
510
|
+
// The signature must arrive on this very block: it is the carrier, and the client keeps the block
|
|
511
|
+
// whole or not at all. It goes last, after the text, which is the order Anthropic's own blocks use.
|
|
512
|
+
events.push({ event: "content_block_delta", data: { type: "content_block_delta", index: this.blockIndex, delta: { type: "signature_delta", signature } } });
|
|
513
|
+
if (this.open)
|
|
514
|
+
this.open.signed = true;
|
|
515
|
+
events.push(...this.closeBlock());
|
|
516
|
+
this.content.push({ type: "thinking", thinking: text, signature });
|
|
517
|
+
this.reasoningCount++;
|
|
518
|
+
return events;
|
|
519
|
+
}
|
|
520
|
+
/** Where this item's envelope says it came from — this adapter's own provider and model. */
|
|
521
|
+
envelopeFor(item) {
|
|
522
|
+
return { w: "responses", p: this.provider, m: this.model, r: item };
|
|
523
|
+
}
|
|
413
524
|
/** Map one upstream event. Returns Anthropic events to emit (possibly none). */
|
|
414
525
|
feed(ev) {
|
|
415
526
|
const type = ev.type;
|
|
@@ -426,12 +537,23 @@ export class StreamMapper {
|
|
|
426
537
|
// The model echoes the mangled name; Claude Code only recognises the original.
|
|
427
538
|
const called = item.name ?? "tool";
|
|
428
539
|
const name = this.toolNames.get(called) ?? called;
|
|
540
|
+
this.lastCallId = id;
|
|
429
541
|
this.content.push({ type: "tool_use", id, name, input: {}, _args: "" });
|
|
430
542
|
out.push(...this.openBlock("tool", { type: "tool_use", id, name, input: {} }, item.id));
|
|
431
543
|
}
|
|
432
544
|
else if (item.type === "reasoning") {
|
|
433
|
-
|
|
434
|
-
|
|
545
|
+
// The item's reasoning is held until it is complete and handed over whole (see
|
|
546
|
+
// `sealReasoning`): one thinking block whose signature carries the envelope. The summary,
|
|
547
|
+
// when the backend sends one, is that block's text — and the summary deltas below fill it
|
|
548
|
+
// in. Passing the summary through `openBlock` here as well would split one item across two
|
|
549
|
+
// blocks, so this opens nothing.
|
|
550
|
+
this.pendingReasoning = {
|
|
551
|
+
...(item.id ? { id: item.id } : {}),
|
|
552
|
+
...(typeof item.encrypted_content === "string" && item.encrypted_content ? { encrypted: item.encrypted_content } : {}),
|
|
553
|
+
...(item.summary !== undefined ? { summary: item.summary } : {}),
|
|
554
|
+
text: "",
|
|
555
|
+
callId: this.lastCallId,
|
|
556
|
+
};
|
|
435
557
|
}
|
|
436
558
|
else if (item.type === "message") {
|
|
437
559
|
this.content.push({ type: "text", text: "" });
|
|
@@ -454,6 +576,13 @@ export class StreamMapper {
|
|
|
454
576
|
case "response.reasoning_summary_text.delta":
|
|
455
577
|
case "response.reasoning_text.delta": {
|
|
456
578
|
const delta = String(ev.delta ?? "");
|
|
579
|
+
// The summary is the text of the reasoning item's block, and that block also carries the
|
|
580
|
+
// envelope — both are written when the item is done, so the block is not opened here. One
|
|
581
|
+
// item is one block; streaming the text into a block of its own would hand the client two.
|
|
582
|
+
if (this.pendingReasoning) {
|
|
583
|
+
this.pendingReasoning.text += delta;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
457
586
|
if (!this.open || this.open.kind !== "thinking") {
|
|
458
587
|
this.content.push({ type: "thinking", thinking: "", signature: "" });
|
|
459
588
|
out.push(...this.openBlock("thinking", { type: "thinking", thinking: "" }));
|
|
@@ -488,7 +617,18 @@ export class StreamMapper {
|
|
|
488
617
|
break;
|
|
489
618
|
}
|
|
490
619
|
case "response.output_item.done": {
|
|
620
|
+
const done = ev.item;
|
|
621
|
+
// The backend may put `encrypted_content` only on the completed item, so the seal is taken
|
|
622
|
+
// here as well as from `response.output_item.added`, and the item is only handed over now
|
|
623
|
+
// anyway — a half-assembled reasoning block must not reach the client.
|
|
624
|
+
if (done?.type === "reasoning" && this.pendingReasoning) {
|
|
625
|
+
if (typeof done.encrypted_content === "string" && done.encrypted_content)
|
|
626
|
+
this.pendingReasoning.encrypted = done.encrypted_content;
|
|
627
|
+
if (done.summary !== undefined)
|
|
628
|
+
this.pendingReasoning.summary = done.summary;
|
|
629
|
+
}
|
|
491
630
|
out.push(...this.closeBlock());
|
|
631
|
+
out.push(...this.sealReasoning());
|
|
492
632
|
break;
|
|
493
633
|
}
|
|
494
634
|
case "response.completed":
|
|
@@ -8,6 +8,8 @@ import { credentialHeaderValues, redactErrorText } from "../../redact.js";
|
|
|
8
8
|
import { SseParser } from "../chatgpt/sse.js";
|
|
9
9
|
import { fetchWithRetry } from "../retry.js";
|
|
10
10
|
import { estimateTokens, formatSse, OpenAiStreamMapper, toOpenAiRequest } from "./translate.js";
|
|
11
|
+
import { refusesReplayedReasoning } from "../../reasoning.js";
|
|
12
|
+
import { PRESETS } from "../../presets.js";
|
|
11
13
|
import { conversationKey, serverToolNames, toolNameRestoreMap } from "../chatgpt/translate.js";
|
|
12
14
|
const PING_MS = 15_000;
|
|
13
15
|
function anthropicError(status, type, message) {
|
|
@@ -110,15 +112,25 @@ export class OpenAiCompatibleAdapter {
|
|
|
110
112
|
const serverTools = serverToolNames(json.tools);
|
|
111
113
|
if (serverTools.size > 0)
|
|
112
114
|
this.log.warn(`${this.tag} ${this.name}: dropped server tools for ${model}: ${[...serverTools].join(", ")} (Anthropic runs these; this provider cannot)`);
|
|
113
|
-
|
|
115
|
+
// The conversation is the storage for a reasoning model's reasoning (reasoning.ts), so what to
|
|
116
|
+
// replay is read out of the request itself and the tally is reported in the request log. `env`
|
|
117
|
+
// names this provider and model: reasoning sealed by another model is never replayed here.
|
|
118
|
+
const carried = { replayed: 0, undecodable: 0 };
|
|
119
|
+
const translate = (replay = true) => toOpenAiRequest(json, {
|
|
114
120
|
model,
|
|
115
121
|
wire,
|
|
116
122
|
...(effort ? { effort } : {}),
|
|
117
123
|
caps,
|
|
124
|
+
replay,
|
|
125
|
+
env: { p: this.name, m: model },
|
|
126
|
+
...(PRESETS.find((entry) => entry.id === this.cfg.preset)?.sealedReasoning ? { sealedReasoning: true } : {}),
|
|
127
|
+
carried,
|
|
128
|
+
log: (line) => this.log.info(`${this.tag} ${this.name}: ${line} (${model})`),
|
|
118
129
|
...(this.cfg.identity === undefined ? {} : { identity: this.cfg.identity }),
|
|
119
130
|
...(this.cfg.instructionsAppend ? { instructionsAppend: this.cfg.instructionsAppend } : {}),
|
|
120
131
|
});
|
|
121
|
-
const
|
|
132
|
+
const upstreamRequest = translate();
|
|
133
|
+
let requestBody = JSON.stringify(upstreamRequest);
|
|
122
134
|
// Some vendors key their prompt cache on a session header rather than on the request's own
|
|
123
135
|
// shape, and hand a cold cache to anyone who does not send one. `conversationKey` is the value
|
|
124
136
|
// this codebase already trusts to be stable for one conversation and different between two.
|
|
@@ -173,6 +185,17 @@ export class OpenAiCompatibleAdapter {
|
|
|
173
185
|
upstream = new Response(refusal, { status: 401 });
|
|
174
186
|
}
|
|
175
187
|
}
|
|
188
|
+
// A 4xx that names reasoning is this vendor refusing the reasoning the conversation carried
|
|
189
|
+
// back — a field it does not take, or a value from another model. The turn goes again without
|
|
190
|
+
// it: replaying is an optimisation the user's turn must never depend on (reasoning.ts).
|
|
191
|
+
if (carried.replayed > 0 && refusesReplayedReasoning(upstream.status, await upstream.clone().text().catch(() => ""))) {
|
|
192
|
+
const refusal = await upstream.text().catch(() => "");
|
|
193
|
+
this.log.warn(`${this.tag} ${this.name}: upstream ${upstream.status} refused ${carried.replayed} replayed reasoning turn(s) for ${model}; asking again without them: ${refusal.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
194
|
+
carried.replayed = 0;
|
|
195
|
+
carried.undecodable = 0;
|
|
196
|
+
requestBody = JSON.stringify(translate(false));
|
|
197
|
+
upstream = await ask();
|
|
198
|
+
}
|
|
176
199
|
}
|
|
177
200
|
catch (error) {
|
|
178
201
|
res.off("close", onClose);
|
|
@@ -202,7 +225,8 @@ export class OpenAiCompatibleAdapter {
|
|
|
202
225
|
return { status: out.status, bytes: Buffer.byteLength(out.body), note: `upstream ${upstream.status}`, exhaustion: providerExhaustion(upstream.status, text, retryAfterMs(Object.fromEntries(upstream.headers))) };
|
|
203
226
|
}
|
|
204
227
|
const wantStream = json.stream === true;
|
|
205
|
-
const mapper = new OpenAiStreamMapper(model, startInput, toolNameRestoreMap(json));
|
|
228
|
+
const mapper = new OpenAiStreamMapper(model, startInput, toolNameRestoreMap(json), { p: this.name, m: model });
|
|
229
|
+
mapper.wire = wire;
|
|
206
230
|
mapper.redactError = redact;
|
|
207
231
|
const parser = new SseParser();
|
|
208
232
|
const reader = upstream.body.getReader();
|
|
@@ -283,8 +307,8 @@ export class OpenAiCompatibleAdapter {
|
|
|
283
307
|
exhaustion: failure ? providerExhaustion(429, `${failure.code ?? ""} ${failure.message}`, retryAfterMs(Object.fromEntries(upstream.headers))) : undefined,
|
|
284
308
|
bytes,
|
|
285
309
|
note: failure
|
|
286
|
-
? `${wantStream ? "mid-stream " : ""}${failure.type}: ${failure.message} (in=${usage.input_tokens} cached=${usage.cache_read_input_tokens} out=${usage.output_tokens})`
|
|
287
|
-
: `in=${usage.input_tokens} cached=${usage.cache_read_input_tokens} out=${usage.output_tokens} stop=${mapper.stopReason}`,
|
|
310
|
+
? `${wantStream ? "mid-stream " : ""}${failure.type}: ${failure.message} (in=${usage.input_tokens} cached=${usage.cache_read_input_tokens} out=${usage.output_tokens} replayed=${carried.replayed} reasoning=${mapper.reasoningCount})`
|
|
311
|
+
: `in=${usage.input_tokens} cached=${usage.cache_read_input_tokens} out=${usage.output_tokens} stop=${mapper.stopReason} replayed=${carried.replayed} reasoning=${mapper.reasoningCount}`,
|
|
288
312
|
usage: { input: usage.input_tokens, cached: usage.cache_read_input_tokens, output: usage.output_tokens },
|
|
289
313
|
stopReason: mapper.stopReason,
|
|
290
314
|
...(wantStream || !failure ? { messageId: mapper.messageId } : {}),
|