@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/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-02 パリティ解消): 全 4 provider の
65
- // 非ストリーミング success path promptBody / completionBody を抽出する。
66
- // streaming 経路は全 provider 未対応(メタデータは記録され、平文のみ省略)
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
- * - streaming 経路 = 未対応(captureContent true でも promptBody /
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
- recorder.record({
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
- recorder.record({
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
- recorder.record({
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
- recorder.record({
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
- recorder.record({
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
- recorder.record({
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
- recorder.record({
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
- recorder.record({
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
- recorder.record({
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
- recorder.record({
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
  })();