@argosvix/sdk 0.4.15-alpha.0 → 0.4.17-alpha.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 +63 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +254 -32
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +12 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +19 -0
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts +45 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +88 -0
- package/dist/prompts.js.map +1 -0
- package/dist/recorder.d.ts +2 -0
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +20 -0
- package/dist/recorder.js.map +1 -1
- package/package.json +83 -83
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { calculateCost, calculateCostWithCache } from "./pricing.js";
|
|
2
2
|
import { Recorder } from "./recorder.js";
|
|
3
3
|
import { generateId } from "./ids.js";
|
|
4
|
-
import { getAmbientTraceContext, _registerObservationSink } from "./context.js";
|
|
4
|
+
import { getAmbientTraceContext, getAmbientPromptTag, _registerObservationSink } from "./context.js";
|
|
5
5
|
const wrappedClients = new WeakMap();
|
|
6
6
|
const wrappedGeminiModels = new WeakSet();
|
|
7
7
|
/**
|
|
@@ -61,9 +61,11 @@ export function wrap(client, config = {}) {
|
|
|
61
61
|
break;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
// 平文 capture の対応範囲(2026-07
|
|
65
|
-
// 非ストリーミング success path
|
|
66
|
-
//
|
|
64
|
+
// 平文 capture の対応範囲(2026-07 streaming 対応): 全 4 provider の
|
|
65
|
+
// 非ストリーミング success path に加え、既存 stream wrapper がある streaming 経路
|
|
66
|
+
// (OpenAI Chat / Anthropic / Mistral / Gemini legacy + 新 SDK)でも promptBody /
|
|
67
|
+
// completionBody を抽出する。OpenAI Responses の streaming は stream wrapper 自体が
|
|
68
|
+
// 未実装(下記 "not yet observed" warn)のため平文も未対応。
|
|
67
69
|
wrappedClients.set(client, recorder);
|
|
68
70
|
return client;
|
|
69
71
|
}
|
|
@@ -186,6 +188,13 @@ function buildTags(config, provider, requestArgs) {
|
|
|
186
188
|
if (native !== undefined)
|
|
187
189
|
tags.userId = native;
|
|
188
190
|
}
|
|
191
|
+
// withPrompt() の内側なら prompt タグ({name}@v{version})を自動付与。
|
|
192
|
+
// 明示 tags.prompt が優先(上書きしない)。版別の品質/コスト比較の基盤(2026-07-02 #4)。
|
|
193
|
+
if (tags.prompt === undefined) {
|
|
194
|
+
const ambientPrompt = getAmbientPromptTag();
|
|
195
|
+
if (ambientPrompt !== undefined)
|
|
196
|
+
tags.prompt = ambientPrompt;
|
|
197
|
+
}
|
|
189
198
|
return tags;
|
|
190
199
|
}
|
|
191
200
|
/**
|
|
@@ -201,8 +210,10 @@ function buildTags(config, provider, requestArgs) {
|
|
|
201
210
|
* - anthropic (Messages) = prompt + completion
|
|
202
211
|
* - mistral (chat.complete) = prompt + completion
|
|
203
212
|
* - gemini (legacy generateContent + 新 SDK models.generateContent) = prompt + completion
|
|
204
|
-
* -
|
|
205
|
-
* completionBody
|
|
213
|
+
* - streaming 経路(2026-07 対応)= 既存 stream wrapper の finalize で promptBody
|
|
214
|
+
* (入口 snapshot)+ completionBody(テキスト delta 蓄積、256KB 上限)を付与。
|
|
215
|
+
* ツール呼び出し引数の streaming 組み立ては対象外。OpenAI Responses streaming は
|
|
216
|
+
* stream wrapper 未実装のため未対応。
|
|
206
217
|
*/
|
|
207
218
|
function extractOpenAIChatPromptBody(requestArgs) {
|
|
208
219
|
if (!Array.isArray(requestArgs.messages) || requestArgs.messages.length === 0) {
|
|
@@ -462,6 +473,154 @@ function safeStringify(value) {
|
|
|
462
473
|
return undefined;
|
|
463
474
|
}
|
|
464
475
|
}
|
|
476
|
+
// ============================================================
|
|
477
|
+
// streaming 平文キャプチャ(2026-07 設計、 docs/handoff/streaming-capture-design-2026-07.md)
|
|
478
|
+
//
|
|
479
|
+
// promptBody は既存の extract*PromptBody を stream 入口で snapshot し、
|
|
480
|
+
// completionBody は既存 stream wrapper(usage 集計で全チャンク観測済み)に
|
|
481
|
+
// テキスト delta の蓄積を足すだけで得る(= 新しい傍受点は作らない)。
|
|
482
|
+
// redaction は従来どおり Recorder の flush 時に一括適用される。
|
|
483
|
+
// ============================================================
|
|
484
|
+
/**
|
|
485
|
+
* 蓄積バッファの上限(= 262,144 bytes = 256KB)。ingest 側の本文上限と整合。
|
|
486
|
+
* 暴走ストリームで wrapper がホストアプリのメモリを食う事故を構造的に防ぐ。
|
|
487
|
+
*/
|
|
488
|
+
const STREAM_CAPTURE_MAX_BYTES = 262_144;
|
|
489
|
+
const STREAM_TRUNCATED_MARKER = "…[truncated]";
|
|
490
|
+
/**
|
|
491
|
+
* UTF-16 code unit 走査で UTF-8 バイト長を数える(TextEncoder の毎 delta 割り当てを
|
|
492
|
+
* 避ける)。surrogate pair は 4 bytes で 1 code point として数える。
|
|
493
|
+
*/
|
|
494
|
+
function utf8ByteLength(text) {
|
|
495
|
+
let bytes = 0;
|
|
496
|
+
for (let i = 0; i < text.length; i++) {
|
|
497
|
+
const code = text.charCodeAt(i);
|
|
498
|
+
if (code < 0x80)
|
|
499
|
+
bytes += 1;
|
|
500
|
+
else if (code < 0x800)
|
|
501
|
+
bytes += 2;
|
|
502
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
|
|
503
|
+
// surrogate pair = 4 bytes(後続の low surrogate を 1 つ消費)
|
|
504
|
+
bytes += 4;
|
|
505
|
+
i++;
|
|
506
|
+
}
|
|
507
|
+
else
|
|
508
|
+
bytes += 3;
|
|
509
|
+
}
|
|
510
|
+
return bytes;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* streaming テキスト delta の蓄積バッファ。256KB 上限で超過分は捨てて、
|
|
514
|
+
* result() が末尾に truncated マーカーを付けた本文を返す。
|
|
515
|
+
*/
|
|
516
|
+
class StreamTextAccumulator {
|
|
517
|
+
parts = [];
|
|
518
|
+
bytes = 0;
|
|
519
|
+
truncated = false;
|
|
520
|
+
push(text) {
|
|
521
|
+
if (typeof text !== "string" || text.length === 0 || this.truncated)
|
|
522
|
+
return;
|
|
523
|
+
const size = utf8ByteLength(text);
|
|
524
|
+
if (this.bytes + size <= STREAM_CAPTURE_MAX_BYTES) {
|
|
525
|
+
this.parts.push(text);
|
|
526
|
+
this.bytes += size;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
// 上限をまたぐ delta は入る分だけ code point 境界で継ぎ足し、以降は捨てる。
|
|
530
|
+
let remaining = STREAM_CAPTURE_MAX_BYTES - this.bytes;
|
|
531
|
+
let head = "";
|
|
532
|
+
for (const ch of text) {
|
|
533
|
+
const chBytes = utf8ByteLength(ch);
|
|
534
|
+
if (chBytes > remaining)
|
|
535
|
+
break;
|
|
536
|
+
head += ch;
|
|
537
|
+
remaining -= chBytes;
|
|
538
|
+
}
|
|
539
|
+
if (head.length > 0) {
|
|
540
|
+
this.parts.push(head);
|
|
541
|
+
this.bytes = STREAM_CAPTURE_MAX_BYTES - remaining;
|
|
542
|
+
}
|
|
543
|
+
this.truncated = true;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* 蓄積本文を返す(空なら undefined = record に載せない)。
|
|
547
|
+
* interrupted = 例外 / 途中打ち切りで「そこまでの本文」を返すとき、marker を強制付与。
|
|
548
|
+
*/
|
|
549
|
+
result(interrupted = false) {
|
|
550
|
+
if (this.parts.length === 0)
|
|
551
|
+
return undefined;
|
|
552
|
+
const body = this.parts.join("");
|
|
553
|
+
return this.truncated || interrupted ? body + STREAM_TRUNCATED_MARKER : body;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* stream 入口での capture 状態生成。captureContent でないときは undefined を返して
|
|
558
|
+
* 抽出コストも蓄積コストもゼロに保つ(promptBody 抽出は thunk で遅延)。
|
|
559
|
+
*/
|
|
560
|
+
function buildStreamCapture(config, extractPromptBody) {
|
|
561
|
+
if (config.captureContent !== true)
|
|
562
|
+
return undefined;
|
|
563
|
+
const capture = { acc: new StreamTextAccumulator() };
|
|
564
|
+
const promptBody = extractPromptBody();
|
|
565
|
+
if (promptBody !== undefined)
|
|
566
|
+
capture.promptBody = promptBody;
|
|
567
|
+
return capture;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* stream finalize / error record に promptBody / completionBody を付与する。
|
|
571
|
+
* redaction はここではなく Recorder の flush 時に一括適用される(非 stream と同方針)。
|
|
572
|
+
*/
|
|
573
|
+
function applyStreamCapture(record, capture, interrupted) {
|
|
574
|
+
if (!capture)
|
|
575
|
+
return;
|
|
576
|
+
if (capture.promptBody !== undefined)
|
|
577
|
+
record.promptBody = capture.promptBody;
|
|
578
|
+
const completionBody = capture.acc.result(interrupted);
|
|
579
|
+
if (completionBody !== undefined)
|
|
580
|
+
record.completionBody = completionBody;
|
|
581
|
+
}
|
|
582
|
+
/** OpenAI Chat stream チャンクのテキスト delta(choices[0].delta.content、string のみ)。 */
|
|
583
|
+
function extractOpenAIStreamDeltaText(chunk) {
|
|
584
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
585
|
+
const content = delta?.content;
|
|
586
|
+
return typeof content === "string" && content.length > 0 ? content : undefined;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Gemini stream チャンクのテキスト delta。新 SDK は chunk.text(string property)、
|
|
590
|
+
* legacy は chunk.text()(関数、safety block 等で throw しうる)。どちらも無ければ
|
|
591
|
+
* candidates[0].content.parts[].text を継ぎ足す(JSON fallback はしない = delta のみ)。
|
|
592
|
+
*/
|
|
593
|
+
function extractGeminiStreamChunkText(chunk) {
|
|
594
|
+
if (!chunk || typeof chunk !== "object")
|
|
595
|
+
return undefined;
|
|
596
|
+
const t = chunk.text;
|
|
597
|
+
if (typeof t === "string" && t.length > 0)
|
|
598
|
+
return t;
|
|
599
|
+
if (typeof t === "function") {
|
|
600
|
+
try {
|
|
601
|
+
const v = t.call(chunk);
|
|
602
|
+
if (typeof v === "string" && v.length > 0)
|
|
603
|
+
return v;
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// legacy chunk.text() は candidate なし等で throw する。 蓄積は諦めて続行。
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const candidates = chunk.candidates;
|
|
610
|
+
if (!Array.isArray(candidates) || candidates.length === 0)
|
|
611
|
+
return undefined;
|
|
612
|
+
const parts = candidates[0]
|
|
613
|
+
?.content?.parts;
|
|
614
|
+
if (!Array.isArray(parts))
|
|
615
|
+
return undefined;
|
|
616
|
+
const texts = [];
|
|
617
|
+
for (const p of parts) {
|
|
618
|
+
const text = p && typeof p === "object" ? p.text : undefined;
|
|
619
|
+
if (typeof text === "string" && text.length > 0)
|
|
620
|
+
texts.push(text);
|
|
621
|
+
}
|
|
622
|
+
return texts.length > 0 ? texts.join("") : undefined;
|
|
623
|
+
}
|
|
465
624
|
function extractErrorDetails(err) {
|
|
466
625
|
if (!err || typeof err !== "object")
|
|
467
626
|
return undefined;
|
|
@@ -571,7 +730,10 @@ function wrapOpenAIChat(client, recorder, config) {
|
|
|
571
730
|
await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
|
|
572
731
|
const response = await originalCreate(...args);
|
|
573
732
|
if (isStream) {
|
|
574
|
-
return wrapOpenAIStream(response, recorder, requestArgs, start, id, callTags, buildTraceMeta(config), usageInjected
|
|
733
|
+
return wrapOpenAIStream(response, recorder, requestArgs, start, id, callTags, buildTraceMeta(config), usageInjected,
|
|
734
|
+
// streaming 平文キャプチャ = promptBody は入口 snapshot、 completionBody は
|
|
735
|
+
// delta 蓄積(captureContent=true のときのみ生成)。
|
|
736
|
+
buildStreamCapture(config, () => extractOpenAIChatPromptBody(requestArgs)));
|
|
575
737
|
}
|
|
576
738
|
const r = response;
|
|
577
739
|
const latencyMs = Date.now() - start;
|
|
@@ -643,7 +805,9 @@ callTags,
|
|
|
643
805
|
traceMeta,
|
|
644
806
|
// 我々が include_usage を注入したか。 true のとき末尾 usage-only チャンクは host に
|
|
645
807
|
// yield せず観測だけする(consumer の stream 形状を変えない)。
|
|
646
|
-
usageInjected = false
|
|
808
|
+
usageInjected = false,
|
|
809
|
+
// streaming 平文キャプチャ(captureContent=true のときのみ渡る)。
|
|
810
|
+
capture) {
|
|
647
811
|
let finalModel = requestArgs.model || "unknown";
|
|
648
812
|
let promptTokens = 0;
|
|
649
813
|
let completionTokens = 0;
|
|
@@ -652,6 +816,8 @@ usageInjected = false) {
|
|
|
652
816
|
let lastUsage;
|
|
653
817
|
// 監査 Tier 2 = TTFT。 最初に本文 delta を載せたチャンク到達時刻 - 開始時刻。
|
|
654
818
|
let ttftMs;
|
|
819
|
+
// 正常完走したか(早期 break / abandon では false のまま → 本文に truncated marker)。
|
|
820
|
+
let completed = false;
|
|
655
821
|
// 監査 2026-06-28 = 消費側が早期 break / abandon すると generator が yield で
|
|
656
822
|
// .return() され、 ループ後の記録が skip されて課金済み呼び出しが record されない
|
|
657
823
|
// (budget gate の spend も漏れる)。 finally + recorded フラグで「正常完走 / error /
|
|
@@ -662,7 +828,7 @@ usageInjected = false) {
|
|
|
662
828
|
return;
|
|
663
829
|
recorded = true;
|
|
664
830
|
const streamCost = calculateCostWithCache("openai", finalModel, promptTokens, completionTokens, cachedReadTokens, 0);
|
|
665
|
-
|
|
831
|
+
const record = {
|
|
666
832
|
id,
|
|
667
833
|
provider: "openai",
|
|
668
834
|
model: finalModel,
|
|
@@ -679,7 +845,9 @@ usageInjected = false) {
|
|
|
679
845
|
tags: callTags,
|
|
680
846
|
...traceMeta,
|
|
681
847
|
requestMeta: buildOpenAIRequestMeta(requestArgs),
|
|
682
|
-
}
|
|
848
|
+
};
|
|
849
|
+
applyStreamCapture(record, capture, !completed);
|
|
850
|
+
recorder.record(record);
|
|
683
851
|
};
|
|
684
852
|
try {
|
|
685
853
|
for await (const chunk of stream) {
|
|
@@ -688,6 +856,7 @@ usageInjected = false) {
|
|
|
688
856
|
// 先頭チャンクでは立てない(= TTFT を過小評価しない、 Codex SHOULD_FIX)。
|
|
689
857
|
ttftMs = Date.now() - start;
|
|
690
858
|
}
|
|
859
|
+
capture?.acc.push(extractOpenAIStreamDeltaText(chunk));
|
|
691
860
|
if (chunk.model)
|
|
692
861
|
finalModel = chunk.model;
|
|
693
862
|
if (chunk.usage) {
|
|
@@ -708,12 +877,13 @@ usageInjected = false) {
|
|
|
708
877
|
}
|
|
709
878
|
yield chunk;
|
|
710
879
|
}
|
|
880
|
+
completed = true;
|
|
711
881
|
recordOnce();
|
|
712
882
|
}
|
|
713
883
|
catch (err) {
|
|
714
884
|
recorded = true; // error record で確定(finally の recordOnce を抑止)
|
|
715
885
|
const errorDetails = extractErrorDetails(err);
|
|
716
|
-
|
|
886
|
+
const errorRecord = {
|
|
717
887
|
id,
|
|
718
888
|
provider: "openai",
|
|
719
889
|
model: finalModel,
|
|
@@ -728,7 +898,10 @@ usageInjected = false) {
|
|
|
728
898
|
error: err instanceof Error ? err.message : String(err),
|
|
729
899
|
...(errorDetails ? { errorDetails } : {}),
|
|
730
900
|
requestMeta: buildOpenAIRequestMeta(requestArgs),
|
|
731
|
-
}
|
|
901
|
+
};
|
|
902
|
+
// 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
|
|
903
|
+
applyStreamCapture(errorRecord, capture, true);
|
|
904
|
+
recorder.record(errorRecord);
|
|
732
905
|
throw err;
|
|
733
906
|
}
|
|
734
907
|
finally {
|
|
@@ -880,7 +1053,7 @@ function wrapAnthropic(client, recorder, config) {
|
|
|
880
1053
|
await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
|
|
881
1054
|
const response = await originalCreate(...args);
|
|
882
1055
|
if (isStream) {
|
|
883
|
-
return wrapAnthropicStream(response, recorder, requestArgs, start, id, callTags, buildTraceMeta(config));
|
|
1056
|
+
return wrapAnthropicStream(response, recorder, requestArgs, start, id, callTags, buildTraceMeta(config), buildStreamCapture(config, () => extractAnthropicPromptBody(requestArgs)));
|
|
884
1057
|
}
|
|
885
1058
|
const r = response;
|
|
886
1059
|
const latencyMs = Date.now() - start;
|
|
@@ -942,13 +1115,17 @@ function wrapAnthropic(client, recorder, config) {
|
|
|
942
1115
|
}
|
|
943
1116
|
};
|
|
944
1117
|
}
|
|
945
|
-
async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, callTags, traceMeta
|
|
1118
|
+
async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, callTags, traceMeta,
|
|
1119
|
+
// streaming 平文キャプチャ(captureContent=true のときのみ渡る)。
|
|
1120
|
+
capture) {
|
|
946
1121
|
let model = requestArgs.model || "unknown";
|
|
947
1122
|
// Anthropic: input_tokens は「非キャッシュ input」。 cache 読取/書込は別計上。
|
|
948
1123
|
let inputTokens = 0;
|
|
949
1124
|
let cachedReadTokens = 0;
|
|
950
1125
|
let cachedWriteTokens = 0;
|
|
951
1126
|
let completionTokens = 0;
|
|
1127
|
+
// 正常完走したか(早期 break / abandon では false のまま → 本文に truncated marker)。
|
|
1128
|
+
let completed = false;
|
|
952
1129
|
// 監査 2026-06-28 = 早期 break でも観測済みトークンで 1 回だけ記録(OpenAI と同様)。
|
|
953
1130
|
let recorded = false;
|
|
954
1131
|
const recordOnce = () => {
|
|
@@ -957,7 +1134,7 @@ async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, ca
|
|
|
957
1134
|
recorded = true;
|
|
958
1135
|
const promptTokens = inputTokens + cachedReadTokens + cachedWriteTokens;
|
|
959
1136
|
const cost = calculateCostWithCache("anthropic", model, promptTokens, completionTokens, cachedReadTokens, cachedWriteTokens);
|
|
960
|
-
|
|
1137
|
+
const record = {
|
|
961
1138
|
id,
|
|
962
1139
|
provider: "anthropic",
|
|
963
1140
|
model,
|
|
@@ -973,10 +1150,19 @@ async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, ca
|
|
|
973
1150
|
tags: callTags,
|
|
974
1151
|
...traceMeta,
|
|
975
1152
|
requestMeta: buildAnthropicRequestMeta(requestArgs),
|
|
976
|
-
}
|
|
1153
|
+
};
|
|
1154
|
+
applyStreamCapture(record, capture, !completed);
|
|
1155
|
+
recorder.record(record);
|
|
977
1156
|
};
|
|
978
1157
|
try {
|
|
979
1158
|
for await (const event of stream) {
|
|
1159
|
+
if (capture &&
|
|
1160
|
+
event.type === "content_block_delta" &&
|
|
1161
|
+
event.delta?.type === "text_delta" &&
|
|
1162
|
+
typeof event.delta.text === "string") {
|
|
1163
|
+
// テキスト delta のみ蓄積(ツール引数の input_json_delta 等は対象外)。
|
|
1164
|
+
capture.acc.push(event.delta.text);
|
|
1165
|
+
}
|
|
980
1166
|
if (event.type === "message_start" && event.message) {
|
|
981
1167
|
if (event.message.model)
|
|
982
1168
|
model = event.message.model;
|
|
@@ -994,13 +1180,14 @@ async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, ca
|
|
|
994
1180
|
}
|
|
995
1181
|
yield event;
|
|
996
1182
|
}
|
|
1183
|
+
completed = true;
|
|
997
1184
|
recordOnce();
|
|
998
1185
|
}
|
|
999
1186
|
catch (err) {
|
|
1000
1187
|
recorded = true;
|
|
1001
1188
|
const errorDetails = extractErrorDetails(err);
|
|
1002
1189
|
const promptTokens = inputTokens + cachedReadTokens + cachedWriteTokens;
|
|
1003
|
-
|
|
1190
|
+
const errorRecord = {
|
|
1004
1191
|
id,
|
|
1005
1192
|
provider: "anthropic",
|
|
1006
1193
|
model,
|
|
@@ -1015,7 +1202,10 @@ async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, ca
|
|
|
1015
1202
|
error: err instanceof Error ? err.message : String(err),
|
|
1016
1203
|
...(errorDetails ? { errorDetails } : {}),
|
|
1017
1204
|
requestMeta: buildAnthropicRequestMeta(requestArgs),
|
|
1018
|
-
}
|
|
1205
|
+
};
|
|
1206
|
+
// 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
|
|
1207
|
+
applyStreamCapture(errorRecord, capture, true);
|
|
1208
|
+
recorder.record(errorRecord);
|
|
1019
1209
|
throw err;
|
|
1020
1210
|
}
|
|
1021
1211
|
finally {
|
|
@@ -1107,7 +1297,7 @@ function wrapMistral(client, recorder, config) {
|
|
|
1107
1297
|
try {
|
|
1108
1298
|
await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
|
|
1109
1299
|
const stream = (await originalStream(...args));
|
|
1110
|
-
return wrapMistralStream(stream, recorder, requestArgs, start, id, callTags, buildTraceMeta(config));
|
|
1300
|
+
return wrapMistralStream(stream, recorder, requestArgs, start, id, callTags, buildTraceMeta(config), buildStreamCapture(config, () => extractMistralPromptBody(requestArgs)));
|
|
1111
1301
|
}
|
|
1112
1302
|
catch (err) {
|
|
1113
1303
|
// H-2: stream initialization error (= auth/validation/connection)
|
|
@@ -1133,18 +1323,22 @@ function wrapMistral(client, recorder, config) {
|
|
|
1133
1323
|
};
|
|
1134
1324
|
}
|
|
1135
1325
|
}
|
|
1136
|
-
async function* wrapMistralStream(stream, recorder, requestArgs, start, id, callTags, traceMeta
|
|
1326
|
+
async function* wrapMistralStream(stream, recorder, requestArgs, start, id, callTags, traceMeta,
|
|
1327
|
+
// streaming 平文キャプチャ(captureContent=true のときのみ渡る)。
|
|
1328
|
+
capture) {
|
|
1137
1329
|
let model = requestArgs.model || "unknown";
|
|
1138
1330
|
let promptTokens = 0;
|
|
1139
1331
|
let completionTokens = 0;
|
|
1140
1332
|
let reportedTotal;
|
|
1333
|
+
// 正常完走したか(早期 break / abandon では false のまま → 本文に truncated marker)。
|
|
1334
|
+
let completed = false;
|
|
1141
1335
|
// 監査 2026-06-28 = 早期 break でも観測済みトークンで 1 回だけ記録(OpenAI と同様)。
|
|
1142
1336
|
let recorded = false;
|
|
1143
1337
|
const recordOnce = () => {
|
|
1144
1338
|
if (recorded)
|
|
1145
1339
|
return;
|
|
1146
1340
|
recorded = true;
|
|
1147
|
-
|
|
1341
|
+
const record = {
|
|
1148
1342
|
id,
|
|
1149
1343
|
provider: "mistral",
|
|
1150
1344
|
model,
|
|
@@ -1157,11 +1351,19 @@ async function* wrapMistralStream(stream, recorder, requestArgs, start, id, call
|
|
|
1157
1351
|
tags: callTags,
|
|
1158
1352
|
...traceMeta,
|
|
1159
1353
|
requestMeta: buildMistralRequestMeta(requestArgs),
|
|
1160
|
-
}
|
|
1354
|
+
};
|
|
1355
|
+
applyStreamCapture(record, capture, !completed);
|
|
1356
|
+
recorder.record(record);
|
|
1161
1357
|
};
|
|
1162
1358
|
try {
|
|
1163
1359
|
for await (const chunk of stream) {
|
|
1164
1360
|
const inner = chunk.data ?? chunk;
|
|
1361
|
+
if (capture) {
|
|
1362
|
+
// Mistral は OpenAI Chat 互換の delta 形状(choices[0].delta.content、string のみ)。
|
|
1363
|
+
const delta = inner.choices?.[0]?.delta;
|
|
1364
|
+
if (typeof delta?.content === "string")
|
|
1365
|
+
capture.acc.push(delta.content);
|
|
1366
|
+
}
|
|
1165
1367
|
if (inner.model)
|
|
1166
1368
|
model = inner.model;
|
|
1167
1369
|
if (inner.usage) {
|
|
@@ -1173,12 +1375,13 @@ async function* wrapMistralStream(stream, recorder, requestArgs, start, id, call
|
|
|
1173
1375
|
}
|
|
1174
1376
|
yield chunk;
|
|
1175
1377
|
}
|
|
1378
|
+
completed = true;
|
|
1176
1379
|
recordOnce();
|
|
1177
1380
|
}
|
|
1178
1381
|
catch (err) {
|
|
1179
1382
|
recorded = true;
|
|
1180
1383
|
const errorDetails = extractErrorDetails(err);
|
|
1181
|
-
|
|
1384
|
+
const errorRecord = {
|
|
1182
1385
|
id,
|
|
1183
1386
|
provider: "mistral",
|
|
1184
1387
|
model,
|
|
@@ -1193,7 +1396,10 @@ async function* wrapMistralStream(stream, recorder, requestArgs, start, id, call
|
|
|
1193
1396
|
error: err instanceof Error ? err.message : String(err),
|
|
1194
1397
|
...(errorDetails ? { errorDetails } : {}),
|
|
1195
1398
|
requestMeta: buildMistralRequestMeta(requestArgs),
|
|
1196
|
-
}
|
|
1399
|
+
};
|
|
1400
|
+
// 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
|
|
1401
|
+
applyStreamCapture(errorRecord, capture, true);
|
|
1402
|
+
recorder.record(errorRecord);
|
|
1197
1403
|
throw err;
|
|
1198
1404
|
}
|
|
1199
1405
|
finally {
|
|
@@ -1307,9 +1513,12 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
|
|
|
1307
1513
|
const originalStreamRef = result.stream;
|
|
1308
1514
|
// #1 R1: trace 軸は stream 生成時(= 入口、 ambient 有効)に snapshot する。
|
|
1309
1515
|
const traceMeta = buildTraceMeta(config);
|
|
1516
|
+
// streaming 平文キャプチャ = promptBody 入口 snapshot + delta 蓄積。
|
|
1517
|
+
const capture = buildStreamCapture(config, () => extractGeminiPromptBody(requestArgs));
|
|
1310
1518
|
const wrappedStream = (async function* () {
|
|
1311
1519
|
try {
|
|
1312
1520
|
for await (const chunk of originalStreamRef) {
|
|
1521
|
+
capture?.acc.push(extractGeminiStreamChunkText(chunk));
|
|
1313
1522
|
yield chunk;
|
|
1314
1523
|
}
|
|
1315
1524
|
const finalResponse = await result.response;
|
|
@@ -1318,7 +1527,7 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
|
|
|
1318
1527
|
const completionTokens = usage?.candidatesTokenCount ?? 0;
|
|
1319
1528
|
const cachedReadTokens = usage?.cachedContentTokenCount ?? 0;
|
|
1320
1529
|
const cost = calculateCostWithCache("gemini", modelName, promptTokens, completionTokens, cachedReadTokens, 0);
|
|
1321
|
-
|
|
1530
|
+
const record = {
|
|
1322
1531
|
id,
|
|
1323
1532
|
provider: "gemini",
|
|
1324
1533
|
model: modelName,
|
|
@@ -1333,11 +1542,13 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
|
|
|
1333
1542
|
tags: callTags,
|
|
1334
1543
|
...traceMeta,
|
|
1335
1544
|
requestMeta: buildGeminiRequestMeta(requestArgs),
|
|
1336
|
-
}
|
|
1545
|
+
};
|
|
1546
|
+
applyStreamCapture(record, capture, false);
|
|
1547
|
+
recorder.record(record);
|
|
1337
1548
|
}
|
|
1338
1549
|
catch (err) {
|
|
1339
1550
|
const errorDetails = extractErrorDetails(err);
|
|
1340
|
-
|
|
1551
|
+
const errorRecord = {
|
|
1341
1552
|
id,
|
|
1342
1553
|
provider: "gemini",
|
|
1343
1554
|
model: modelName,
|
|
@@ -1352,7 +1563,10 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
|
|
|
1352
1563
|
error: err instanceof Error ? err.message : String(err),
|
|
1353
1564
|
...(errorDetails ? { errorDetails } : {}),
|
|
1354
1565
|
requestMeta: buildGeminiRequestMeta(requestArgs),
|
|
1355
|
-
}
|
|
1566
|
+
};
|
|
1567
|
+
// 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
|
|
1568
|
+
applyStreamCapture(errorRecord, capture, true);
|
|
1569
|
+
recorder.record(errorRecord);
|
|
1356
1570
|
throw err;
|
|
1357
1571
|
}
|
|
1358
1572
|
})();
|
|
@@ -1464,10 +1678,13 @@ function wrapGeminiNew(client, recorder, config) {
|
|
|
1464
1678
|
// C-2 + H-3 fix: AsyncGenerator wrap with finally-record + error tied to consumption
|
|
1465
1679
|
// #1 R1: trace 軸は stream 生成時(= 入口、 ambient 有効)に snapshot する。
|
|
1466
1680
|
const traceMeta = buildTraceMeta(config);
|
|
1681
|
+
// streaming 平文キャプチャ = promptBody 入口 snapshot + delta 蓄積。
|
|
1682
|
+
const capture = buildStreamCapture(config, () => extractGeminiPromptBody(requestArgs));
|
|
1467
1683
|
return (async function* () {
|
|
1468
1684
|
let lastUsage;
|
|
1469
1685
|
try {
|
|
1470
1686
|
for await (const chunk of stream) {
|
|
1687
|
+
capture?.acc.push(extractGeminiStreamChunkText(chunk));
|
|
1471
1688
|
if (chunk.usageMetadata)
|
|
1472
1689
|
lastUsage = chunk.usageMetadata;
|
|
1473
1690
|
yield chunk;
|
|
@@ -1476,7 +1693,7 @@ function wrapGeminiNew(client, recorder, config) {
|
|
|
1476
1693
|
const completionTokens = lastUsage?.candidatesTokenCount ?? 0;
|
|
1477
1694
|
const cachedReadTokens = lastUsage?.cachedContentTokenCount ?? 0;
|
|
1478
1695
|
const cost = calculateCostWithCache("gemini", modelName, promptTokens, completionTokens, cachedReadTokens, 0);
|
|
1479
|
-
|
|
1696
|
+
const record = {
|
|
1480
1697
|
id,
|
|
1481
1698
|
provider: "gemini",
|
|
1482
1699
|
model: modelName,
|
|
@@ -1491,11 +1708,13 @@ function wrapGeminiNew(client, recorder, config) {
|
|
|
1491
1708
|
tags: callTags,
|
|
1492
1709
|
...traceMeta,
|
|
1493
1710
|
requestMeta: buildGeminiNewRequestMeta(requestArgs),
|
|
1494
|
-
}
|
|
1711
|
+
};
|
|
1712
|
+
applyStreamCapture(record, capture, false);
|
|
1713
|
+
recorder.record(record);
|
|
1495
1714
|
}
|
|
1496
1715
|
catch (err) {
|
|
1497
1716
|
const errorDetails = extractErrorDetails(err);
|
|
1498
|
-
|
|
1717
|
+
const errorRecord = {
|
|
1499
1718
|
id,
|
|
1500
1719
|
provider: "gemini",
|
|
1501
1720
|
model: modelName,
|
|
@@ -1512,7 +1731,10 @@ function wrapGeminiNew(client, recorder, config) {
|
|
|
1512
1731
|
error: err instanceof Error ? err.message : String(err),
|
|
1513
1732
|
...(errorDetails ? { errorDetails } : {}),
|
|
1514
1733
|
requestMeta: buildGeminiNewRequestMeta(requestArgs),
|
|
1515
|
-
}
|
|
1734
|
+
};
|
|
1735
|
+
// 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
|
|
1736
|
+
applyStreamCapture(errorRecord, capture, true);
|
|
1737
|
+
recorder.record(errorRecord);
|
|
1516
1738
|
throw err;
|
|
1517
1739
|
}
|
|
1518
1740
|
})();
|