@argosvix/sdk 0.4.22-alpha.0 → 0.5.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.
Files changed (63) hide show
  1. package/README.md +4 -9
  2. package/dist/aiSdkMiddleware.d.ts +36 -20
  3. package/dist/aiSdkMiddleware.d.ts.map +1 -1
  4. package/dist/aiSdkMiddleware.js +98 -31
  5. package/dist/aiSdkMiddleware.js.map +1 -1
  6. package/dist/approvals.d.ts +37 -29
  7. package/dist/approvals.d.ts.map +1 -1
  8. package/dist/approvals.js +24 -18
  9. package/dist/approvals.js.map +1 -1
  10. package/dist/budgetGate.d.ts +30 -24
  11. package/dist/budgetGate.d.ts.map +1 -1
  12. package/dist/budgetGate.js +118 -92
  13. package/dist/budgetGate.js.map +1 -1
  14. package/dist/client.d.ts +15 -10
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +271 -222
  17. package/dist/client.js.map +1 -1
  18. package/dist/context.d.ts +59 -48
  19. package/dist/context.d.ts.map +1 -1
  20. package/dist/context.js +72 -58
  21. package/dist/context.js.map +1 -1
  22. package/dist/flush.d.ts +6 -4
  23. package/dist/flush.d.ts.map +1 -1
  24. package/dist/flush.js +6 -4
  25. package/dist/flush.js.map +1 -1
  26. package/dist/ids.d.ts +3 -2
  27. package/dist/ids.d.ts.map +1 -1
  28. package/dist/ids.js +6 -5
  29. package/dist/ids.js.map +1 -1
  30. package/dist/langchainCallback.d.ts +14 -13
  31. package/dist/langchainCallback.d.ts.map +1 -1
  32. package/dist/langchainCallback.js +22 -17
  33. package/dist/langchainCallback.js.map +1 -1
  34. package/dist/policyScan.d.ts +29 -22
  35. package/dist/policyScan.d.ts.map +1 -1
  36. package/dist/policyScan.js +47 -38
  37. package/dist/policyScan.js.map +1 -1
  38. package/dist/pricing.d.ts +5 -5
  39. package/dist/pricing.d.ts.map +1 -1
  40. package/dist/pricing.js +24 -22
  41. package/dist/pricing.js.map +1 -1
  42. package/dist/prompts.d.ts +20 -17
  43. package/dist/prompts.d.ts.map +1 -1
  44. package/dist/prompts.js +21 -17
  45. package/dist/prompts.js.map +1 -1
  46. package/dist/query.d.ts +21 -21
  47. package/dist/query.d.ts.map +1 -1
  48. package/dist/query.js +14 -14
  49. package/dist/recorder.d.ts +39 -19
  50. package/dist/recorder.d.ts.map +1 -1
  51. package/dist/recorder.js +120 -69
  52. package/dist/recorder.js.map +1 -1
  53. package/dist/redaction.d.ts +35 -33
  54. package/dist/redaction.d.ts.map +1 -1
  55. package/dist/redaction.js +73 -64
  56. package/dist/redaction.js.map +1 -1
  57. package/dist/types.d.ts +131 -114
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/version.d.ts +9 -9
  60. package/dist/version.d.ts.map +1 -1
  61. package/dist/version.js +9 -9
  62. package/dist/version.js.map +1 -1
  63. package/package.json +8 -2
package/dist/client.js CHANGED
@@ -32,8 +32,8 @@ export function wrap(client, config = {}) {
32
32
  return client;
33
33
  }
34
34
  const recorder = new Recorder(config);
35
- // #1 R4 = withSpan emit する observation の送信先として登録(= この recorder の
36
- // ingest 経路で observations[] を送る)
35
+ // Register this recorder as the sink for observations emitted by withSpan
36
+ // (observations[] are sent through this recorder's ingest path).
37
37
  _registerObservationSink(recorder);
38
38
  switch (provider) {
39
39
  case "openai": {
@@ -61,11 +61,11 @@ export function wrap(client, config = {}) {
61
61
  break;
62
62
  }
63
63
  }
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)のため平文も未対応。
64
+ // Plaintext capture coverage (streaming support added 2026-07): in addition to
65
+ // the non-streaming success path of all 4 providers, promptBody / completionBody
66
+ // are also extracted on the streaming paths that have an existing stream wrapper
67
+ // (OpenAI Chat / Anthropic / Mistral / Gemini legacy + new SDK). OpenAI Responses
68
+ // streaming has no stream wrapper yet, so plaintext capture is unsupported there too.
69
69
  wrappedClients.set(client, recorder);
70
70
  return client;
71
71
  }
@@ -73,9 +73,10 @@ export function getRecorder(client) {
73
73
  return wrappedClients.get(client) ?? null;
74
74
  }
75
75
  /**
76
- * 内部用 = wrap() を経由しない統合 (= Vercel AI SDK middleware ) が、 自前の
77
- * Recorder flushClient() / getRecorder() WeakMap に載せるための登録口。
78
- * これで `flushClient(middleware)` が wrap 済みクライアントと同じ経路で動く。
76
+ * Internal: registration hook for integrations that do not go through wrap()
77
+ * (e.g. the Vercel AI SDK middleware) to put their own Recorder into the WeakMap
78
+ * used by flushClient() / getRecorder(). This lets `flushClient(middleware)`
79
+ * work through the same path as wrapped clients.
79
80
  */
80
81
  export function _registerRecorderFor(obj, recorder) {
81
82
  wrappedClients.set(obj, recorder);
@@ -115,21 +116,22 @@ function detectProvider(client, override) {
115
116
  }
116
117
  function buildTraceMeta(config) {
117
118
  const meta = {};
118
- // #1 R1 自動コンテキスト伝播: traceId 明示 config > ambient(withTrace 内)の順。
119
- // autoContext:false ambient を無視し従来の静的挙動に戻す。
119
+ // Automatic context propagation: traceId resolution order is explicit config
120
+ // first, then ambient (inside withTrace). autoContext:false ignores the
121
+ // ambient context and restores the previous static behavior.
120
122
  const ambient = config.autoContext === false ? undefined : getAmbientTraceContext();
121
123
  const traceId = config.traceId ?? ambient?.traceId;
122
124
  if (traceId) {
123
125
  meta.traceId = traceId;
124
- // trace に属する呼び出しは自前の span を持つ(= trace のノードになる) 明示 spanId 優先。
126
+ // A call that belongs to a trace gets its own span (it becomes a node of the trace). An explicit spanId takes priority.
125
127
  meta.spanId = config.spanId ?? generateId();
126
- // parent 明示 config > ambient span(withSpan 等で設定、 R1 では trace 直下=undefined)
128
+ // Parent resolution: explicit config first, then the ambient span (set by withSpan etc.; undefined when directly under the trace).
127
129
  const parentSpanId = config.parentSpanId ?? ambient?.spanId;
128
130
  if (parentSpanId)
129
131
  meta.parentSpanId = parentSpanId;
130
132
  }
131
133
  else {
132
- // trace コンテキストが無い standalone 呼び出し = 従来挙動(明示指定のみ carry)
134
+ // Standalone call without a trace context: previous behavior (only explicitly provided values are forwarded).
133
135
  if (config.spanId)
134
136
  meta.spanId = config.spanId;
135
137
  if (config.parentSpanId)
@@ -140,15 +142,17 @@ function buildTraceMeta(config) {
140
142
  return meta;
141
143
  }
142
144
  /**
143
- * エンドユーザー識別子の自動取得 (= 2026-06-17 user 提案、 Codex review 反映)
144
- * provider ごとに「opaque 前提で設計された専用項目」のみ読み取り、 userId タグ
145
- * として集計に carry する:
146
- * - OpenAI: safety_identifier (= OpenAI 推奨の安定識別子、 hash 推奨)
147
- * - Anthropic: metadata.user_id (= 仕様上 PII 禁止・opaque 必須・256 文字以内)
148
- * OpenAI `user` PII (email / ID) が入りやすく自動転送は同意外の漏洩に
149
- * なりうるため、 自動取得の対象から外す (= 必要なら明示 tags.userId carry)。
150
- * provider を明示で分岐し、 Gemini / Mistral は常に no-op (= provider の項目名
151
- * を誤って拾わない)。 値は backend 上限に合わせ 256 文字に丸める。
145
+ * Automatic end-user identifier capture (added 2026-06-17, refined in review).
146
+ * For each provider, only fields designed to hold opaque identifiers are read,
147
+ * and the value is forwarded to aggregation as the userId tag:
148
+ * - OpenAI: safety_identifier (OpenAI's recommended stable identifier; hashing recommended)
149
+ * - Anthropic: metadata.user_id (per spec: no PII, must be opaque, max 256 chars)
150
+ * The legacy OpenAI `user` field is excluded from automatic capture: it often
151
+ * carries PII (emails / raw IDs), so auto-forwarding it could leak data beyond
152
+ * what was consented to (pass an explicit tags.userId if needed).
153
+ * Providers are matched explicitly; Gemini / Mistral are always a no-op (so we
154
+ * never mistakenly pick up another provider's field names). Values are trimmed
155
+ * to 256 characters to match the backend limit.
152
156
  */
153
157
  function pickEndUserId(provider, requestArgs) {
154
158
  if (!requestArgs || typeof requestArgs !== "object")
@@ -172,14 +176,15 @@ function pickEndUserId(provider, requestArgs) {
172
176
  return typeof cand === "string" ? cand.slice(0, 256) : undefined;
173
177
  }
174
178
  /**
175
- * record に乗せる tags を組み立てる。 config.tags を基底に、 captureUserId
176
- * 明示 false でない限り provider 専用項目から userId を補完する。 明示の
177
- * config.tags.userId が既にあればそれを優先する (= 自動取得で上書きしない)。
179
+ * Build the tags placed on a record. Starting from config.tags, the userId is
180
+ * filled in from the provider-specific field unless captureUserId is explicitly
181
+ * false. An existing explicit config.tags.userId takes priority (automatic
182
+ * capture never overwrites it).
178
183
  *
179
- * Codex HIGH 2 反映: wrapper の入口で 1 回だけ呼び、 確定した tags
180
- * success / error / stream の全 record で共有する (= await 後に requestArgs を
181
- * 読み直さない。 呼び出し元が request object を再利用・変更しても別ユーザーの
182
- * userId で記録されない)。
184
+ * Call this exactly once at each wrapper's entry point and share the resolved
185
+ * tags across all success / error / stream records: requestArgs must not be
186
+ * re-read after an await, so a caller that reuses or mutates the request object
187
+ * cannot cause the call to be recorded under another user's userId.
183
188
  */
184
189
  function buildTags(config, provider, requestArgs) {
185
190
  const tags = { ...(config.tags ?? {}) };
@@ -188,8 +193,9 @@ function buildTags(config, provider, requestArgs) {
188
193
  if (native !== undefined)
189
194
  tags.userId = native;
190
195
  }
191
- // withPrompt() の内側なら prompt タグ({name}@v{version})を自動付与。
192
- // 明示 tags.prompt が優先(上書きしない)。版別の品質/コスト比較の基盤(2026-07-02 #4)
196
+ // Inside withPrompt(), the prompt tag ({name}@v{version}) is attached
197
+ // automatically. An explicit tags.prompt takes priority (never overwritten).
198
+ // This is the basis for per-version quality/cost comparison (2026-07-02).
193
199
  if (tags.prompt === undefined) {
194
200
  const ambientPrompt = getAmbientPromptTag();
195
201
  if (ambientPrompt !== undefined)
@@ -198,22 +204,23 @@ function buildTags(config, provider, requestArgs) {
198
204
  return tags;
199
205
  }
200
206
  /**
201
- * Pro+ 平文保存機能の SDK opt-in 抽出 helper。
207
+ * SDK-side opt-in extraction helpers for the Pro+ plaintext storage feature.
202
208
  *
203
- * captureContent true のときに wrapper が呼んで、 provider 毎の
204
- * request / response 形状から prompt / completion / tool calls 文字列
205
- * (および JSON 文字列)として抽出する。 PII redaction Recorder 側で
206
- * 一括して適用されるので、 ここでは生の文字列を返すのみ。
209
+ * Called by the wrappers when captureContent is true; extracts prompt /
210
+ * completion / tool calls as strings (and JSON strings) from each provider's
211
+ * request / response shapes. PII redaction is applied centrally in the
212
+ * Recorder, so these helpers only return the raw strings.
207
213
  *
208
- * 対応 provider(2026-07-02 パリティ解消で全 4 provider の非ストリーミングに拡大):
209
- * - openai (Chat Completions + Responses) = prompt + completion + tool calls
210
- * - anthropic (Messages) = prompt + completion
211
- * - mistral (chat.complete) = prompt + completion
212
- * - gemini (legacy generateContent + SDK models.generateContent) = prompt + completion
213
- * - streaming 経路(2026-07 対応)= 既存 stream wrapper finalize で promptBody
214
- * (入口 snapshot)+ completionBody(テキスト delta 蓄積、256KB 上限)を付与。
215
- * ツール呼び出し引数の streaming 組み立ては対象外。OpenAI Responses streaming
216
- * stream wrapper 未実装のため未対応。
214
+ * Supported providers (extended 2026-07-02 to cover non-streaming for all 4):
215
+ * - openai (Chat Completions + Responses): prompt + completion + tool calls
216
+ * - anthropic (Messages): prompt + completion
217
+ * - mistral (chat.complete): prompt + completion
218
+ * - gemini (legacy generateContent + new SDK models.generateContent): prompt + completion
219
+ * - streaming paths (added 2026-07): the existing stream wrappers' finalize
220
+ * step attaches promptBody (snapshotted at entry) + completionBody
221
+ * (accumulated text deltas, 256KB cap). Streaming assembly of tool-call
222
+ * arguments is out of scope. OpenAI Responses streaming is unsupported
223
+ * because it has no stream wrapper.
217
224
  */
218
225
  function extractOpenAIChatPromptBody(requestArgs) {
219
226
  if (!Array.isArray(requestArgs.messages) || requestArgs.messages.length === 0) {
@@ -236,7 +243,7 @@ function extractOpenAIChatCompletionBody(response) {
236
243
  const content = first?.message?.content;
237
244
  if (typeof content === "string")
238
245
  return content;
239
- // content array (multi-modal の場合) の場合は JSON 文字列で carry
246
+ // When content is an array (multi-modal), forward it as a JSON string
240
247
  if (Array.isArray(content)) {
241
248
  try {
242
249
  return JSON.stringify(content);
@@ -273,14 +280,16 @@ function extractOpenAIChatToolCalls(response) {
273
280
  return result.length > 0 ? result : undefined;
274
281
  }
275
282
  /**
276
- * v0.3.0-alpha.2 段階9 拡張 = OpenAI Responses API captureContent 抽出。
283
+ * captureContent extraction for the OpenAI Responses API (added in v0.3.0-alpha.2).
277
284
  *
278
- * - request.input string (= 単純 prompt) or array (= multi-modal / structured)
279
- * のどちらか。 string ならそのまま、 array なら JSON 文字列化して carry。
280
- * - response.output_text (= aggregated text、 SDK 0.4.x で導入) を 第一候補、
281
- * なければ response.output (= 配列) walk して output_text type の text を join。
282
- * - tool calls response.output 内の type === "function_call" item から
283
- * name + arguments を抽出。
285
+ * - request.input is either a string (a simple prompt) or an array
286
+ * (multi-modal / structured). Strings are forwarded as-is; arrays are
287
+ * JSON-stringified.
288
+ * - response.output_text (aggregated text, introduced in SDK 0.4.x) is the
289
+ * first choice; otherwise walk the response.output array and join the text
290
+ * of output_text-type items.
291
+ * - Tool calls are extracted (name + arguments) from items with
292
+ * type === "function_call" inside response.output.
284
293
  */
285
294
  function extractOpenAIResponsesPromptBody(requestArgs) {
286
295
  if (requestArgs.input === undefined || requestArgs.input === null)
@@ -385,7 +394,7 @@ function extractAnthropicCompletionBody(response) {
385
394
  }
386
395
  if (texts.length > 0)
387
396
  return texts.join("\n");
388
- // text block がなければ全 content JSON 文字列として carry
397
+ // If there is no text block, forward the whole content as a JSON string
389
398
  try {
390
399
  return JSON.stringify(content);
391
400
  }
@@ -396,12 +405,12 @@ function extractAnthropicCompletionBody(response) {
396
405
  return undefined;
397
406
  }
398
407
  /**
399
- * 2026-07-02 パリティ解消 = Mistral / Gemini の非ストリーミング success path にも
400
- * captureContent 抽出を追加(OpenAI / Anthropic と同水準)。PII redaction
401
- * 従来どおり Recorder 側で一括適用される。
408
+ * Parity fix (2026-07-02): captureContent extraction was added to the
409
+ * non-streaming success paths of Mistral / Gemini as well (same level as
410
+ * OpenAI / Anthropic). PII redaction is still applied centrally in the Recorder.
402
411
  */
403
412
  function extractMistralPromptBody(requestArgs) {
404
- // Mistral chat.complete OpenAI Chat 互換の messages 配列。
413
+ // Mistral chat.complete takes an OpenAI-Chat-compatible messages array.
405
414
  if (!Array.isArray(requestArgs.messages) || requestArgs.messages.length === 0) {
406
415
  return undefined;
407
416
  }
@@ -417,15 +426,16 @@ function extractMistralCompletionBody(response) {
417
426
  ?.message?.content;
418
427
  if (typeof content === "string")
419
428
  return content;
420
- // ContentChunk[](multi-modal) JSON 文字列で carry
429
+ // ContentChunk[] (multi-modal) is forwarded as a JSON string
421
430
  if (Array.isArray(content))
422
431
  return safeStringify(content);
423
432
  return undefined;
424
433
  }
425
434
  /**
426
- * Gemini prompt 抽出。legacy `@google/generative-ai` generateContent
427
- * string または { contents } を受け、新 `@google/genai` は { model, contents }。
428
- * どちらも contents(または生の string)を文字列化して carry する。
435
+ * Gemini prompt extraction. The legacy `@google/generative-ai` generateContent
436
+ * accepts a string or { contents }; the new `@google/genai` takes
437
+ * { model, contents }. In both cases the contents (or the raw string) are
438
+ * stringified and forwarded.
429
439
  */
430
440
  function extractGeminiPromptBody(requestArgs) {
431
441
  if (typeof requestArgs === "string")
@@ -440,10 +450,10 @@ function extractGeminiPromptBody(requestArgs) {
440
450
  return safeStringify(contents);
441
451
  }
442
452
  /**
443
- * Gemini completion 抽出。legacy result.response、新 SDK result 自体が
444
- * { candidates: [{ content: { parts: [{ text }] } }] } を持つので、caller が
445
- * candidates を持つ側のオブジェクトを渡す。text part join、無ければ parts
446
- * JSON 文字列で carry。
453
+ * Gemini completion extraction. On legacy it is result.response; on the new SDK
454
+ * the result itself holds { candidates: [{ content: { parts: [{ text }] } }] },
455
+ * so the caller passes whichever object carries candidates. Text parts are
456
+ * joined; if there are none, the parts are forwarded as a JSON string.
447
457
  */
448
458
  function extractGeminiCompletionBody(response) {
449
459
  if (!response || typeof response !== "object")
@@ -474,22 +484,25 @@ function safeStringify(value) {
474
484
  }
475
485
  }
476
486
  // ============================================================
477
- // streaming 平文キャプチャ(2026-07 設計、 docs/handoff/streaming-capture-design-2026-07.md)
487
+ // Streaming plaintext capture (designed 2026-07)
478
488
  //
479
- // promptBody は既存の extract*PromptBody stream 入口で snapshot し、
480
- // completionBody は既存 stream wrapper(usage 集計で全チャンク観測済み)に
481
- // テキスト delta の蓄積を足すだけで得る(= 新しい傍受点は作らない)。
482
- // redaction は従来どおり Recorder flush 時に一括適用される。
489
+ // promptBody is snapshotted at stream entry via the existing extract*PromptBody
490
+ // helpers; completionBody is obtained by simply adding text-delta accumulation
491
+ // to the existing stream wrappers (which already observe every chunk for usage
492
+ // aggregation) no new interception points are introduced.
493
+ // Redaction is still applied centrally at Recorder flush time.
483
494
  // ============================================================
484
495
  /**
485
- * 蓄積バッファの上限(= 262,144 bytes = 256KB)ingest 側の本文上限と整合。
486
- * 暴走ストリームで wrapper がホストアプリのメモリを食う事故を構造的に防ぐ。
496
+ * Accumulation buffer cap (262,144 bytes = 256KB), aligned with the ingest-side
497
+ * body limit. Structurally prevents a runaway stream from letting the wrapper
498
+ * eat the host application's memory.
487
499
  */
488
500
  const STREAM_CAPTURE_MAX_BYTES = 262_144;
489
501
  const STREAM_TRUNCATED_MARKER = "…[truncated]";
490
502
  /**
491
- * UTF-16 code unit 走査で UTF-8 バイト長を数える(TextEncoder の毎 delta 割り当てを
492
- * 避ける)surrogate pair 4 bytes で 1 code point として数える。
503
+ * Count the UTF-8 byte length by scanning UTF-16 code units (avoids a
504
+ * TextEncoder allocation per delta). A surrogate pair counts as one code point
505
+ * of 4 bytes.
493
506
  */
494
507
  function utf8ByteLength(text) {
495
508
  let bytes = 0;
@@ -500,7 +513,7 @@ function utf8ByteLength(text) {
500
513
  else if (code < 0x800)
501
514
  bytes += 2;
502
515
  else if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
503
- // surrogate pair = 4 bytes(後続の low surrogate を 1 つ消費)
516
+ // Surrogate pair: 4 bytes (consumes the following low surrogate)
504
517
  bytes += 4;
505
518
  i++;
506
519
  }
@@ -510,8 +523,8 @@ function utf8ByteLength(text) {
510
523
  return bytes;
511
524
  }
512
525
  /**
513
- * streaming テキスト delta の蓄積バッファ。256KB 上限で超過分は捨てて、
514
- * result() が末尾に truncated マーカーを付けた本文を返す。
526
+ * Accumulation buffer for streaming text deltas. Anything beyond the 256KB cap
527
+ * is dropped, and result() returns the body with a truncated marker appended.
515
528
  */
516
529
  class StreamTextAccumulator {
517
530
  parts = [];
@@ -526,7 +539,7 @@ class StreamTextAccumulator {
526
539
  this.bytes += size;
527
540
  return;
528
541
  }
529
- // 上限をまたぐ delta は入る分だけ code point 境界で継ぎ足し、以降は捨てる。
542
+ // A delta that crosses the cap is appended up to a code point boundary as far as it fits; the rest is dropped.
530
543
  let remaining = STREAM_CAPTURE_MAX_BYTES - this.bytes;
531
544
  let head = "";
532
545
  for (const ch of text) {
@@ -543,8 +556,9 @@ class StreamTextAccumulator {
543
556
  this.truncated = true;
544
557
  }
545
558
  /**
546
- * 蓄積本文を返す(空なら undefined = record に載せない)
547
- * interrupted = 例外 / 途中打ち切りで「そこまでの本文」を返すとき、marker を強制付与。
559
+ * Return the accumulated body (undefined if empty, i.e. not placed on the record).
560
+ * interrupted: when returning the body captured so far after an exception or
561
+ * early termination, the truncated marker is always appended.
548
562
  */
549
563
  result(interrupted = false) {
550
564
  if (this.parts.length === 0)
@@ -554,8 +568,9 @@ class StreamTextAccumulator {
554
568
  }
555
569
  }
556
570
  /**
557
- * stream 入口での capture 状態生成。captureContent でないときは undefined を返して
558
- * 抽出コストも蓄積コストもゼロに保つ(promptBody 抽出は thunk で遅延)。
571
+ * Create the capture state at stream entry. When captureContent is not enabled,
572
+ * returns undefined so both extraction and accumulation cost stay at zero
573
+ * (promptBody extraction is deferred behind a thunk).
559
574
  */
560
575
  function buildStreamCapture(config, extractPromptBody) {
561
576
  if (config.captureContent !== true)
@@ -567,8 +582,9 @@ function buildStreamCapture(config, extractPromptBody) {
567
582
  return capture;
568
583
  }
569
584
  /**
570
- * stream finalize / error record promptBody / completionBody を付与する。
571
- * redaction はここではなく Recorder flush 時に一括適用される( stream と同方針)。
585
+ * Attach promptBody / completionBody to a stream finalize / error record.
586
+ * Redaction happens not here but centrally at Recorder flush time (same policy
587
+ * as the non-streaming path).
572
588
  */
573
589
  function applyStreamCapture(record, capture, interrupted) {
574
590
  if (!capture)
@@ -579,16 +595,17 @@ function applyStreamCapture(record, capture, interrupted) {
579
595
  if (completionBody !== undefined)
580
596
  record.completionBody = completionBody;
581
597
  }
582
- /** OpenAI Chat stream チャンクのテキスト delta(choices[0].delta.content、string のみ) */
598
+ /** Text delta of an OpenAI Chat stream chunk (choices[0].delta.content, strings only). */
583
599
  function extractOpenAIStreamDeltaText(chunk) {
584
600
  const delta = chunk.choices?.[0]?.delta;
585
601
  const content = delta?.content;
586
602
  return typeof content === "string" && content.length > 0 ? content : undefined;
587
603
  }
588
604
  /**
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 のみ)。
605
+ * Text delta of a Gemini stream chunk. The new SDK exposes chunk.text (a string
606
+ * property); legacy exposes chunk.text() (a function that may throw, e.g. on a
607
+ * safety block). If neither yields text, concatenate
608
+ * candidates[0].content.parts[].text (no JSON fallback — deltas only).
592
609
  */
593
610
  function extractGeminiStreamChunkText(chunk) {
594
611
  if (!chunk || typeof chunk !== "object")
@@ -603,7 +620,7 @@ function extractGeminiStreamChunkText(chunk) {
603
620
  return v;
604
621
  }
605
622
  catch {
606
- // legacy chunk.text() candidate なし等で throw する。 蓄積は諦めて続行。
623
+ // Legacy chunk.text() throws when there is no candidate etc. Give up on accumulating and continue.
607
624
  }
608
625
  }
609
626
  const candidates = chunk.candidates;
@@ -648,8 +665,9 @@ function isOpenAIChatLike(client) {
648
665
  return typeof c?.chat?.completions?.create === "function";
649
666
  }
650
667
  /**
651
- * 監査 Tier 2 = OpenAI usage から推論 / 音声トークンを取り出す(無ければ undefined)
652
- * audio は入力(prompt)+ 出力(completion)の合算。 0 のときは記録しない(列を NULL に保つ)。
668
+ * Extract reasoning / audio tokens from OpenAI usage (undefined when absent).
669
+ * Audio is the sum of input (prompt) and output (completion) tokens. Zero
670
+ * values are not recorded (keeps the column NULL).
653
671
  */
654
672
  function extractRichTokens(usage) {
655
673
  const out = {};
@@ -664,8 +682,9 @@ function extractRichTokens(usage) {
664
682
  return out;
665
683
  }
666
684
  /**
667
- * 監査 Tier 2 = streaming チャンクが「初回トークン」(本文 / ツール / 音声 / refusal)を
668
- * 載せているか。 先頭の role のみ delta では立てない(TTFT を正しく測る)。
685
+ * Whether a streaming chunk carries the "first token" (content / tool / audio /
686
+ * refusal). Not triggered by the leading role-only delta (so TTFT is measured
687
+ * correctly).
669
688
  */
670
689
  function chunkHasStreamedOutput(chunk) {
671
690
  const choices = chunk.choices;
@@ -681,14 +700,15 @@ function chunkHasStreamedOutput(chunk) {
681
700
  return delta.tool_calls != null || delta.function_call != null || delta.audio != null;
682
701
  }
683
702
  /**
684
- * audit round2 M27 = 高レベル streaming helper (Anthropic messages.stream() /
685
- * OpenAI chat.completions.stream()) wrap 対象外で budget/policy gate を素通りする。
686
- * これらは同期戻り (event-emitter) async gate を内部で安全に挟むのが難しいため、 ゲートを
687
- * 使う設定のときだけ wrap 時に一度警告し、 .create({ stream: true }) (= gate される経路) への
688
- * 切替を促す。 .create stream:true Responses streaming は既に gate 済。
703
+ * The high-level streaming helpers (Anthropic messages.stream() /
704
+ * OpenAI chat.completions.stream()) are not wrapped and bypass the budget/policy
705
+ * gate. They return synchronously (event-emitter style), which makes it hard to
706
+ * safely insert an async gate internally, so when a gate is configured we warn
707
+ * once at wrap time and suggest switching to .create({ stream: true }) (the
708
+ * gated path). .create stream:true and Responses streaming are already gated.
689
709
  */
690
710
  let warnedGateStreamHelper = false;
691
- /** Test-only: reset the M27 stream-helper warn-once flag. Do not call from production. */
711
+ /** Test-only: reset the stream-helper warn-once flag. Do not call from production. */
692
712
  export function __resetStreamHelperWarning() {
693
713
  warnedGateStreamHelper = false;
694
714
  }
@@ -700,7 +720,7 @@ function warnIfGatedStreamHelper(config, hasStreamHelper, label) {
700
720
  warnedGateStreamHelper = true;
701
721
  // eslint-disable-next-line no-console
702
722
  console.warn(`[argosvix] ${label} bypasses the budget/policy gate (not wrapped). ` +
703
- `Use .create({ stream: true }) for gated streaming. (audit M27)`);
723
+ `Use .create({ stream: true }) for gated streaming.`);
704
724
  }
705
725
  function hasTee(v) {
706
726
  return (v !== null &&
@@ -713,21 +733,23 @@ function isThenable(v) {
713
733
  typeof v.then === "function");
714
734
  }
715
735
  /**
716
- * 完全互換 wrap deferred 返り値(2026-07 full-compat)
736
+ * Deferred return value of the full-compat wrap (2026-07).
717
737
  *
718
- * gate 待ち / stream tee 変換がある経路では元の APIPromise をそのまま返せない。
719
- * その場合でも then/catch/finally に加えて .withResponse() を委譲で温存する
720
- * (data mapData 経由 = stream なら tee のユーザー枝に差し替え)。
721
- * .asResponse()(生 Response の直接読み)だけは観測(parse)と構造的に両立しない
722
- * ため非対応のまま(docs 開示済み)
738
+ * Paths involving a gate wait or a stream tee transform cannot return the
739
+ * original APIPromise as-is. Even then, .withResponse() is preserved via
740
+ * delegation in addition to then/catch/finally (data goes through mapData —
741
+ * for streams it is swapped for the user branch of the tee). Only
742
+ * .asResponse() (direct reads of the raw Response) remains unsupported, since
743
+ * it is structurally incompatible with observation (parsing); this is
744
+ * disclosed in the docs.
723
745
  */
724
746
  function makeCompatAPIPromise(result, sourceBox, mapData) {
725
- // 本物の Promise を返す(instanceof Promise / util.types.isPromise 互換。Codex LOW)
747
+ // Return a real Promise (compatible with instanceof Promise / util.types.isPromise).
726
748
  const out = result.then((v) => v);
727
- // 犠牲 catch: record 用に常時 attach される settled チェーンの rejection が、
728
- // ユーザーが withResponse() だけ使う/全く await しない場合に Node
729
- // unhandledRejection にならないようにする(Codex HIGH)。ユーザーの await には
730
- // 同じ rejection がそのまま届く。
749
+ // Sacrificial catch: prevents the rejection of the always-attached settled
750
+ // chain (used for recording) from becoming a Node unhandledRejection when
751
+ // the user only uses withResponse() or never awaits at all. The user's own
752
+ // await still receives the same rejection.
731
753
  void out.catch(() => { });
732
754
  out.withResponse = () => sourceBox.then((box) => {
733
755
  const api = box.api;
@@ -748,28 +770,32 @@ function makeCompatAPIPromise(result, sourceBox, mapData) {
748
770
  return out;
749
771
  }
750
772
  /**
751
- * deferred compat オブジェクトの asResponse() 用エラー。生 Response body
752
- * ユーザーが直接読む API は記録のための parse と構造的に両立しない(body
753
- * 二重消費)ため、compat 側では静かに壊れるのでなく明確に失敗させる。
754
- * identity fast path で返す元の APIPromise asResponse には触らない:
755
- * openai-node withResponse() が内部で asResponse() を呼ぶため、上書きすると
756
- * withResponse まで壊れる(実パッケージスモークで検出)。そちらは docs 開示で対応。
773
+ * Error for asResponse() on the deferred compat object. An API that lets the
774
+ * user read the raw Response body directly is structurally incompatible with
775
+ * the parse needed for recording (the body would be consumed twice), so on the
776
+ * compat side we fail loudly instead of breaking silently.
777
+ * Warning: do not touch asResponse on the original APIPromise returned by the
778
+ * identity fast path — the real openai-node withResponse() calls asResponse()
779
+ * internally, so overriding it would break withResponse too (caught in a smoke
780
+ * test against the real package). That case is handled by docs disclosure.
757
781
  */
758
782
  const UNSUPPORTED_AS_RESPONSE = "[argosvix] asResponse() is not supported on wrapped clients (the raw body " +
759
783
  "would be consumed twice). Use withResponse(), or call this endpoint on an " +
760
784
  "unwrapped client.";
761
785
  /**
762
- * 記録用ジェネレータ(wrapOpenAIStream )を裏で完走させる。tee の観測枝の消費専用。
763
- * 記録・エラー記録はジェネレータ内部で完結しているので、ここでは黙って回すだけ。
786
+ * Run a recording generator (wrapOpenAIStream etc.) to completion in the
787
+ * background. Used solely to consume the observation branch of a tee.
788
+ * Recording and error recording are fully handled inside the generator, so
789
+ * this just spins it silently.
764
790
  */
765
791
  async function drainRecordingStream(gen) {
766
792
  try {
767
793
  for await (const _chunk of gen) {
768
- /* 観測枝の消費のみ(yield 値は捨てる) */
794
+ /* Consume the observation branch only (yielded values are discarded) */
769
795
  }
770
796
  }
771
797
  catch {
772
- /* エラー記録はジェネレータ側で済んでいる */
798
+ /* Error recording is already done inside the generator */
773
799
  }
774
800
  }
775
801
  function wrapOpenAIChat(client, recorder, config) {
@@ -782,12 +808,13 @@ function wrapOpenAIChat(client, recorder, config) {
782
808
  const callTags = buildTags(config, "openai", requestArgs);
783
809
  const id = generateId();
784
810
  const isStream = requestArgs.stream === true;
785
- // 監査 2026-06-28 HIGH = OpenAI stream 時に stream_options.include_usage を
786
- // 付けないと usage を一切返さず、 streaming 呼び出しが 0 token / $0 で記録され
787
- // コストが黙って過少計上(+ budget gate streaming で素通り)になる。 stream の
788
- // とき include_usage を補う(client が明示設定済みならそれを尊重)
789
- // 我々が注入した場合、 OpenAI が末尾に足す usage-only チャンク(choices=[])は
790
- // host に yield しない(choices[0] 前提の素朴な消費側を壊さないため。Codex 指摘)。
811
+ // Without stream_options.include_usage, OpenAI returns no usage at all for
812
+ // streams, so streaming calls would be recorded as 0 tokens / $0 — silently
813
+ // under-counting cost (and letting streaming pass the budget gate). Inject
814
+ // include_usage for streams (respecting an explicit client setting).
815
+ // Warning: when we injected it, the trailing usage-only chunk OpenAI appends
816
+ // (choices=[]) is not yielded to the host, so naive consumers that assume
817
+ // choices[0] don't break. (2026-06-28)
791
818
  const usageInjected = isStream && requestArgs.stream_options?.include_usage === undefined;
792
819
  if (usageInjected) {
793
820
  args = [...args];
@@ -804,7 +831,7 @@ function wrapOpenAIChat(client, recorder, config) {
804
831
  const model = r.model || requestArgs.model || "unknown";
805
832
  const promptTokens = r.usage?.prompt_tokens ?? 0;
806
833
  const completionTokens = r.usage?.completion_tokens ?? 0;
807
- // OpenAI: prompt_tokens cached を含む合計。cached は内数。
834
+ // OpenAI: prompt_tokens is the total including cached tokens; cached is a subset.
808
835
  const cachedReadTokens = r.usage?.prompt_tokens_details?.cached_tokens ?? 0;
809
836
  const cost = calculateCostWithCache("openai", model, promptTokens, completionTokens, cachedReadTokens, 0);
810
837
  const record = {
@@ -838,7 +865,7 @@ function wrapOpenAIChat(client, recorder, config) {
838
865
  recorder.record(record);
839
866
  }
840
867
  catch {
841
- /* 記録は best-effort = host の呼び出しを壊さない */
868
+ /* Recording is best-effort never break the host's call */
842
869
  }
843
870
  };
844
871
  const recordFailure = (err) => {
@@ -862,12 +889,12 @@ function wrapOpenAIChat(client, recorder, config) {
862
889
  });
863
890
  }
864
891
  catch {
865
- /* 記録は best-effort */
892
+ /* Recording is best-effort */
866
893
  }
867
894
  };
868
- // ---- stream + gate 無効 = identity fast path ----
869
- // 元の APIPromise をそのまま返す(= .withResponse() / .asResponse() が本物のまま)
870
- // 記録は observer .then で行う(同一 promise の複数 then は安全)
895
+ // ---- Non-stream + gate disabled: identity fast path ----
896
+ // Return the original APIPromise as-is (.withResponse() / .asResponse() stay real).
897
+ // Recording is done via an observer .then (multiple thens on the same promise are safe).
871
898
  if (!isStream && !recorder.budgetGate.isActive) {
872
899
  let ret;
873
900
  try {
@@ -884,9 +911,10 @@ function wrapOpenAIChat(client, recorder, config) {
884
911
  recordSuccess(ret);
885
912
  return ret;
886
913
  }
887
- // ---- deferred 経路(gate 有効 or stream)----
888
- // async 関数の return thenable を自動 flatten して APIPromise が消えるため、
889
- // { api } の箱で包んで識別を保持する(withResponse 委譲用)。
914
+ // ---- Deferred path (gate enabled or stream) ----
915
+ // An async function's return auto-flattens thenables, which would erase the
916
+ // APIPromise, so wrap it in an { api } box to preserve its identity (for
917
+ // withResponse delegation).
890
918
  const sourceBox = (async () => {
891
919
  await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
892
920
  return { api: originalCreate(...args) };
@@ -906,32 +934,35 @@ function wrapOpenAIChat(client, recorder, config) {
906
934
  // ---- stream ----
907
935
  const capture = buildStreamCapture(config, () => extractOpenAIChatPromptBody(requestArgs));
908
936
  const wrapForRecord = (raw) => wrapOpenAIStream(raw, recorder, requestArgs, start, id, callTags, traceMeta, usageInjected, capture);
909
- // tee 変換は経路(await / withResponse().data)をまたいで 1 回だけ。
937
+ // The tee transform runs exactly once across paths (await / withResponse().data).
910
938
  let transformDone = false;
911
939
  let transformedValue;
912
940
  const transformOnce = (raw) => {
913
941
  if (transformDone)
914
942
  return transformedValue;
915
- // usage 注入時は tee しない: 注入した usage-only チャンク(choices=[])が
916
- // ユーザー枝に素通りして素朴な消費側を壊すため(2026-06-28 監査の非回帰)。
943
+ // Do not tee when usage was injected: the injected usage-only chunk
944
+ // (choices=[]) would pass straight through to the user branch and break
945
+ // naive consumers (regression guard, 2026-06-28).
917
946
  if (!usageInjected && hasTee(raw)) {
918
947
  try {
919
948
  const [obs, user] = raw.tee();
920
- // フラグ確定は tee 成功後(同期 throw するカスタム stream で毒らない。Codex)
949
+ // Commit the flag only after tee succeeds (so a custom stream that throws synchronously cannot poison it).
921
950
  transformDone = true;
922
951
  transformedValue = user;
923
- // 観測枝を既存の記録ジェネレータで drain(記録ロジックを複製しない)。
924
- // ユーザー枝は本物の Stream のまま = .tee() / .toReadableStream() /
925
- // .controller が全て生きる。ユーザーが途中 break しても観測枝は完走する
926
- // ので usage は完全に記録される。
927
- // メモリ特性: 観測枝の先行 drain 中、未消費のユーザー枝には tee の内部
928
- // キューに全チャンクが滞留する(上限 = レスポンス全体)。確実な記録との
929
- // トレードオフとして許容(docs に開示)。
952
+ // Drain the observation branch with the existing recording generator
953
+ // (no duplication of recording logic). The user branch stays a real
954
+ // Stream, so .tee() / .toReadableStream() / .controller all keep
955
+ // working. Even if the user breaks early, the observation branch runs
956
+ // to completion, so usage is fully recorded.
957
+ // Memory note: while the observation branch drains ahead, all chunks
958
+ // for the unconsumed user branch pile up in tee's internal queue
959
+ // (bounded by the whole response). Accepted as a trade-off for
960
+ // reliable recording (disclosed in the docs).
930
961
  void drainRecordingStream(wrapForRecord(obs));
931
962
  return user;
932
963
  }
933
964
  catch {
934
- /* tee 失敗 従来経路へ */
965
+ /* tee failed fall back to the legacy path */
935
966
  }
936
967
  }
937
968
  transformDone = true;
@@ -948,16 +979,19 @@ function wrapOpenAIChat(client, recorder, config) {
948
979
  };
949
980
  }
950
981
  async function* wrapOpenAIStream(stream, recorder, requestArgs, start, id,
951
- // Codex r2 HIGH: stream record userId generator 消費開始時ではなく
952
- // wrapper 入口で確定した callTags を使う (= await 後の requestArgs 再読みを排除)。
982
+ // The stream record's userId must come from the callTags resolved at the
983
+ // wrapper entry, not at generator consumption start (eliminates re-reading
984
+ // requestArgs after an await).
953
985
  callTags,
954
- // Codex #1 R1: trace 軸も入口で snapshot した meta を使う (= stream withTrace の外で
955
- // 消費しても ambient 消失 / trace への取り違えを防ぐ)。
986
+ // The trace fields also use the meta snapshotted at entry, so consuming the
987
+ // stream outside withTrace can neither lose the ambient context nor attribute
988
+ // the call to the wrong trace.
956
989
  traceMeta,
957
- // 我々が include_usage を注入したか。 true のとき末尾 usage-only チャンクは host に
958
- // yield せず観測だけする(consumer stream 形状を変えない)。
990
+ // Whether we injected include_usage. When true, the trailing usage-only chunk
991
+ // is observed but not yielded to the host (keeps the consumer's stream shape
992
+ // unchanged).
959
993
  usageInjected = false,
960
- // streaming 平文キャプチャ(captureContent=true のときのみ渡る)
994
+ // Streaming plaintext capture (passed only when captureContent=true).
961
995
  capture) {
962
996
  let finalModel = requestArgs.model || "unknown";
963
997
  let promptTokens = 0;
@@ -965,14 +999,16 @@ capture) {
965
999
  let cachedReadTokens = 0;
966
1000
  let reportedTotal;
967
1001
  let lastUsage;
968
- // 監査 Tier 2 = TTFT。 最初に本文 delta を載せたチャンク到達時刻 - 開始時刻。
1002
+ // TTFT: arrival time of the first chunk carrying a content delta, minus start time.
969
1003
  let ttftMs;
970
- // 正常完走したか(早期 break / abandon では false のまま 本文に truncated marker)
1004
+ // Whether the stream ran to completion (stays false on early break / abandon the body gets the truncated marker).
971
1005
  let completed = false;
972
- // 監査 2026-06-28 = 消費側が早期 break / abandon すると generator yield
973
- // .return() され、 ループ後の記録が skip されて課金済み呼び出しが record されない
974
- // (budget gate spend も漏れる)。 finally + recorded フラグで「正常完走 / error /
975
- // 途中打ち切り」のいずれでも観測済みトークンで 1 回だけ記録する(Python SDK と同等)。
1006
+ // If the consumer breaks early or abandons the stream, the generator gets
1007
+ // .return()ed at the yield and the post-loop recording is skipped, so a
1008
+ // billed call would never be recorded (and the budget gate's spend would
1009
+ // leak too). With finally + the recorded flag, the call is recorded exactly
1010
+ // once with the observed tokens on normal completion, error, and early
1011
+ // termination alike (matches the Python SDK). (2026-06-28)
976
1012
  let recorded = false;
977
1013
  const recordOnce = () => {
978
1014
  if (recorded)
@@ -1003,8 +1039,9 @@ capture) {
1003
1039
  try {
1004
1040
  for await (const chunk of stream) {
1005
1041
  if (ttftMs === undefined && chunkHasStreamedOutput(chunk)) {
1006
- // 最初に「本文 / ツール / 音声」を載せたチャンク = 初回トークン。 role だけの
1007
- // 先頭チャンクでは立てない(= TTFT を過小評価しない、 Codex SHOULD_FIX)。
1042
+ // The first chunk carrying content / tool / audio counts as the first
1043
+ // token. Not triggered by the leading role-only chunk (avoids
1044
+ // underestimating TTFT).
1008
1045
  ttftMs = Date.now() - start;
1009
1046
  }
1010
1047
  capture?.acc.push(extractOpenAIStreamDeltaText(chunk));
@@ -1021,23 +1058,25 @@ capture) {
1021
1058
  cachedReadTokens = chunk.usage.prompt_tokens_details.cached_tokens;
1022
1059
  }
1023
1060
  }
1024
- // 我々が注入した include_usage の末尾 usage-only チャンク(choices 空)は観測のみで
1025
- // host に渡さない(注入していない=client が自分で付けた場合はそのまま yield)。
1061
+ // The trailing usage-only chunk (empty choices) produced by the
1062
+ // include_usage we injected is observed only and not passed to the host
1063
+ // (if we did not inject it — the client set it themselves — it is
1064
+ // yielded as-is).
1026
1065
  if (usageInjected && chunk.usage != null && (chunk.choices?.length ?? 0) === 0) {
1027
1066
  continue;
1028
1067
  }
1029
1068
  yield chunk;
1030
1069
  }
1031
- // controller.abort() openai-node Stream は握って正常終了扱いにするため、
1032
- // ここに到達する。clean success ではなく途中打ち切り(truncated)として記録する
1033
- // (Codex MEDIUM: 過少 usage の成功記録を防ぐ)
1070
+ // The openai-node Stream swallows controller.abort() and treats it as a
1071
+ // normal end, so execution reaches here. Record it as truncated rather than
1072
+ // a clean success (prevents recording under-counted usage as a success).
1034
1073
  const aborted = stream.controller
1035
1074
  ?.signal?.aborted === true;
1036
1075
  completed = !aborted;
1037
1076
  recordOnce();
1038
1077
  }
1039
1078
  catch (err) {
1040
- recorded = true; // error record で確定(finally recordOnce を抑止)
1079
+ recorded = true; // finalized by the error record (suppresses recordOnce in finally)
1041
1080
  const errorDetails = extractErrorDetails(err);
1042
1081
  const errorRecord = {
1043
1082
  id,
@@ -1055,13 +1094,13 @@ capture) {
1055
1094
  ...(errorDetails ? { errorDetails } : {}),
1056
1095
  requestMeta: buildOpenAIRequestMeta(requestArgs),
1057
1096
  };
1058
- // 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
1097
+ // When cut off by an exception, put the body captured so far + the truncated marker on the record.
1059
1098
  applyStreamCapture(errorRecord, capture, true);
1060
1099
  recorder.record(errorRecord);
1061
1100
  throw err;
1062
1101
  }
1063
1102
  finally {
1064
- // 早期 break / abandon の経路。 正常完走・error 済みなら no-op。
1103
+ // Early break / abandon path. No-op if the stream already completed normally or errored.
1065
1104
  recordOnce();
1066
1105
  }
1067
1106
  }
@@ -1111,7 +1150,7 @@ function wrapOpenAIResponses(client, recorder, config) {
1111
1150
  });
1112
1151
  }
1113
1152
  catch {
1114
- /* 記録は best-effort */
1153
+ /* Recording is best-effort */
1115
1154
  }
1116
1155
  };
1117
1156
  const recordSuccess = (response0) => {
@@ -1153,10 +1192,10 @@ function wrapOpenAIResponses(client, recorder, config) {
1153
1192
  recorder.record(record);
1154
1193
  }
1155
1194
  catch {
1156
- /* 記録は best-effort */
1195
+ /* Recording is best-effort */
1157
1196
  }
1158
1197
  };
1159
- // ---- stream + gate 無効 = identity fast path(chat と同型)----
1198
+ // ---- Non-stream + gate disabled: identity fast path (same shape as chat) ----
1160
1199
  if (!isStream && !recorder.budgetGate.isActive) {
1161
1200
  let ret;
1162
1201
  try {
@@ -1173,7 +1212,7 @@ function wrapOpenAIResponses(client, recorder, config) {
1173
1212
  recordSuccess(ret);
1174
1213
  return ret;
1175
1214
  }
1176
- // ---- deferred 経路(gate 有効 or stream)----
1215
+ // ---- Deferred path (gate enabled or stream) ----
1177
1216
  const sourceBox = (async () => {
1178
1217
  await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
1179
1218
  return { api: originalCreate(...args) };
@@ -1207,7 +1246,7 @@ function wrapOpenAIResponses(client, recorder, config) {
1207
1246
  return user;
1208
1247
  }
1209
1248
  catch {
1210
- /* tee 失敗 従来経路へ */
1249
+ /* tee failed fall back to the legacy path */
1211
1250
  }
1212
1251
  }
1213
1252
  transformDone = true;
@@ -1224,10 +1263,12 @@ function wrapOpenAIResponses(client, recorder, config) {
1224
1263
  };
1225
1264
  }
1226
1265
  /**
1227
- * Responses API stream の記録ジェネレータ(2026-07 inline から抽出)。
1228
- * response.output_text.delta / refusal.delta を蓄積し、response.completed の usage で
1229
- * 確定。error / response.failed / response.incomplete イベントも処理(Codex 済)。
1230
- * 完全互換経路では tee の観測枝をこれで drain し、ユーザー枝は本物の Stream を返す。
1266
+ * Recording generator for Responses API streams (extracted from inline code in
1267
+ * 2026-07). Accumulates response.output_text.delta / refusal.delta and
1268
+ * finalizes with the usage from response.completed. Also handles error /
1269
+ * response.failed / response.incomplete events. On the full-compat path this
1270
+ * drains the tee's observation branch while the user branch stays the real
1271
+ * Stream that is returned.
1231
1272
  */
1232
1273
  async function* wrapOpenAIResponsesStream(stream, recorder, requestArgs, start, id, callTags, traceMeta, capture) {
1233
1274
  {
@@ -1237,7 +1278,7 @@ async function* wrapOpenAIResponsesStream(stream, recorder, requestArgs, start,
1237
1278
  let usage;
1238
1279
  let completed = false;
1239
1280
  let recorded = false;
1240
- // Codex: stream イベントとして来る terminal failure / 完了未観測を扱う。
1281
+ // Handle terminal failures arriving as stream events, and the case where completion was never observed.
1241
1282
  let sawCompleted = false;
1242
1283
  let incomplete = false;
1243
1284
  let streamError;
@@ -1245,7 +1286,7 @@ async function* wrapOpenAIResponsesStream(stream, recorder, requestArgs, start,
1245
1286
  if (recorded)
1246
1287
  return;
1247
1288
  recorded = true;
1248
- // stream イベントの error/failed error record にして success を抑止(Codex HIGH)。
1289
+ // Turn error/failed stream events into an error record and suppress the success record.
1249
1290
  if (streamError !== undefined) {
1250
1291
  const errRecord = {
1251
1292
  id,
@@ -1293,7 +1334,7 @@ async function* wrapOpenAIResponsesStream(stream, recorder, requestArgs, start,
1293
1334
  try {
1294
1335
  for await (const event of stream) {
1295
1336
  const ev = event;
1296
- // 本文 delta = output_text refusal の両方(Codex MEDIUM: refusal が抜けていた)
1337
+ // Content deltas include both output_text and refusal (refusal was missing originally).
1297
1338
  if ((ev.type === "response.output_text.delta" ||
1298
1339
  ev.type === "response.refusal.delta") &&
1299
1340
  typeof ev.delta === "string") {
@@ -1309,7 +1350,7 @@ async function* wrapOpenAIResponsesStream(stream, recorder, requestArgs, start,
1309
1350
  incomplete = ev.type === "response.incomplete";
1310
1351
  }
1311
1352
  else if (ev.type === "error" || ev.type === "response.failed") {
1312
- // stream イベントとして来る terminal error を記録(Codex HIGH)。
1353
+ // Record terminal errors that arrive as stream events.
1313
1354
  const respErr = ev.response?.error?.message;
1314
1355
  streamError =
1315
1356
  typeof respErr === "string"
@@ -1392,8 +1433,8 @@ function wrapAnthropic(client, recorder, config) {
1392
1433
  const r = response;
1393
1434
  const latencyMs = Date.now() - start;
1394
1435
  const model = r.model || requestArgs.model || "unknown";
1395
- // Anthropic: input_tokens は「非キャッシュ input」。cache 読取/書込は別計上なので
1396
- // promptTokens(= input 合計)に足し戻す。
1436
+ // Anthropic: input_tokens counts only non-cached input. Cache reads/writes
1437
+ // are reported separately, so add them back into promptTokens (total input).
1397
1438
  const cachedReadTokens = r.usage?.cache_read_input_tokens ?? 0;
1398
1439
  const cachedWriteTokens = r.usage?.cache_creation_input_tokens ?? 0;
1399
1440
  const promptTokens = (r.usage?.input_tokens ?? 0) + cachedReadTokens + cachedWriteTokens;
@@ -1450,17 +1491,17 @@ function wrapAnthropic(client, recorder, config) {
1450
1491
  };
1451
1492
  }
1452
1493
  async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, callTags, traceMeta,
1453
- // streaming 平文キャプチャ(captureContent=true のときのみ渡る)
1494
+ // Streaming plaintext capture (passed only when captureContent=true).
1454
1495
  capture) {
1455
1496
  let model = requestArgs.model || "unknown";
1456
- // Anthropic: input_tokens は「非キャッシュ input」。 cache 読取/書込は別計上。
1497
+ // Anthropic: input_tokens counts only non-cached input. Cache reads/writes are reported separately.
1457
1498
  let inputTokens = 0;
1458
1499
  let cachedReadTokens = 0;
1459
1500
  let cachedWriteTokens = 0;
1460
1501
  let completionTokens = 0;
1461
- // 正常完走したか(早期 break / abandon では false のまま 本文に truncated marker)
1502
+ // Whether the stream ran to completion (stays false on early break / abandon the body gets the truncated marker).
1462
1503
  let completed = false;
1463
- // 監査 2026-06-28 = 早期 break でも観測済みトークンで 1 回だけ記録(OpenAI と同様)
1504
+ // Record exactly once with the observed tokens even on early break (same as OpenAI; 2026-06-28).
1464
1505
  let recorded = false;
1465
1506
  const recordOnce = () => {
1466
1507
  if (recorded)
@@ -1494,7 +1535,7 @@ capture) {
1494
1535
  event.type === "content_block_delta" &&
1495
1536
  event.delta?.type === "text_delta" &&
1496
1537
  typeof event.delta.text === "string") {
1497
- // テキスト delta のみ蓄積(ツール引数の input_json_delta 等は対象外)
1538
+ // Accumulate text deltas only (tool-argument input_json_delta etc. are out of scope).
1498
1539
  capture.acc.push(event.delta.text);
1499
1540
  }
1500
1541
  if (event.type === "message_start" && event.message) {
@@ -1537,7 +1578,7 @@ capture) {
1537
1578
  ...(errorDetails ? { errorDetails } : {}),
1538
1579
  requestMeta: buildAnthropicRequestMeta(requestArgs),
1539
1580
  };
1540
- // 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
1581
+ // When cut off by an exception, put the body captured so far + the truncated marker on the record.
1541
1582
  applyStreamCapture(errorRecord, capture, true);
1542
1583
  recorder.record(errorRecord);
1543
1584
  throw err;
@@ -1634,7 +1675,7 @@ function wrapMistral(client, recorder, config) {
1634
1675
  return wrapMistralStream(stream, recorder, requestArgs, start, id, callTags, buildTraceMeta(config), buildStreamCapture(config, () => extractMistralPromptBody(requestArgs)));
1635
1676
  }
1636
1677
  catch (err) {
1637
- // H-2: stream initialization error (= auth/validation/connection)
1678
+ // Stream initialization error (auth/validation/connection)
1638
1679
  const errorDetails = extractErrorDetails(err);
1639
1680
  recorder.record({
1640
1681
  id,
@@ -1658,15 +1699,15 @@ function wrapMistral(client, recorder, config) {
1658
1699
  }
1659
1700
  }
1660
1701
  async function* wrapMistralStream(stream, recorder, requestArgs, start, id, callTags, traceMeta,
1661
- // streaming 平文キャプチャ(captureContent=true のときのみ渡る)
1702
+ // Streaming plaintext capture (passed only when captureContent=true).
1662
1703
  capture) {
1663
1704
  let model = requestArgs.model || "unknown";
1664
1705
  let promptTokens = 0;
1665
1706
  let completionTokens = 0;
1666
1707
  let reportedTotal;
1667
- // 正常完走したか(早期 break / abandon では false のまま 本文に truncated marker)
1708
+ // Whether the stream ran to completion (stays false on early break / abandon the body gets the truncated marker).
1668
1709
  let completed = false;
1669
- // 監査 2026-06-28 = 早期 break でも観測済みトークンで 1 回だけ記録(OpenAI と同様)
1710
+ // Record exactly once with the observed tokens even on early break (same as OpenAI; 2026-06-28).
1670
1711
  let recorded = false;
1671
1712
  const recordOnce = () => {
1672
1713
  if (recorded)
@@ -1693,7 +1734,7 @@ capture) {
1693
1734
  for await (const chunk of stream) {
1694
1735
  const inner = chunk.data ?? chunk;
1695
1736
  if (capture) {
1696
- // Mistral OpenAI Chat 互換の delta 形状(choices[0].delta.content、string のみ)
1737
+ // Mistral uses the OpenAI-Chat-compatible delta shape (choices[0].delta.content, strings only).
1697
1738
  const delta = inner.choices?.[0]?.delta;
1698
1739
  if (typeof delta?.content === "string")
1699
1740
  capture.acc.push(delta.content);
@@ -1731,7 +1772,7 @@ capture) {
1731
1772
  ...(errorDetails ? { errorDetails } : {}),
1732
1773
  requestMeta: buildMistralRequestMeta(requestArgs),
1733
1774
  };
1734
- // 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
1775
+ // When cut off by an exception, put the body captured so far + the truncated marker on the record.
1735
1776
  applyStreamCapture(errorRecord, capture, true);
1736
1777
  recorder.record(errorRecord);
1737
1778
  throw err;
@@ -1843,20 +1884,24 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
1843
1884
  const result = (await originalStream(...args));
1844
1885
  if (!result.stream)
1845
1886
  return result;
1846
- // 消費側が早期 break / abandon すると generator .return() され、ループ後の
1847
- // 記録が skip されて課金済み呼び出しが record されない(他 3 provider と同じく
1848
- // recordOnce()+finally で「完走 / error / 途中打ち切り」いずれでも 1 回だけ記録)。
1887
+ // If the consumer breaks early or abandons the stream, the generator
1888
+ // gets .return()ed and the post-loop recording is skipped, so a billed
1889
+ // call would never be recorded (as with the other 3 providers,
1890
+ // recordOnce() + finally record exactly once on completion, error, or
1891
+ // early termination).
1849
1892
  const originalStreamRef = result.stream;
1850
- // #1 R1: trace 軸は stream 生成時(= 入口、 ambient 有効)に snapshot する。
1893
+ // The trace fields are snapshotted at stream creation (the entry point, where the ambient context is still active).
1851
1894
  const traceMeta = buildTraceMeta(config);
1852
- // streaming 平文キャプチャ = promptBody 入口 snapshot + delta 蓄積。
1895
+ // Streaming plaintext capture: promptBody snapshotted at entry + delta accumulation.
1853
1896
  const capture = buildStreamCapture(config, () => extractGeminiPromptBody(requestArgs));
1854
1897
  const wrappedStream = (async function* () {
1855
1898
  let completed = false;
1856
1899
  let recorded = false;
1857
- // legacy Gemini usage を完走後の result.response からしか読めないため、
1858
- // 早期 break では 0 トークン(+ truncated マーカー)で記録する。完走時だけ
1859
- // result.response await する(早期 break で await するとハング/追加消費)。
1900
+ // Legacy Gemini only exposes usage via result.response after the
1901
+ // stream completes, so on early break we record 0 tokens (+ the
1902
+ // truncated marker). result.response is awaited only on full
1903
+ // completion (awaiting it after an early break can hang or consume
1904
+ // extra).
1860
1905
  let promptTokens = 0;
1861
1906
  let completionTokens = 0;
1862
1907
  let cachedReadTokens = 0;
@@ -1918,13 +1963,13 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
1918
1963
  ...(errorDetails ? { errorDetails } : {}),
1919
1964
  requestMeta: buildGeminiRequestMeta(requestArgs),
1920
1965
  };
1921
- // 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
1966
+ // When cut off by an exception, put the body captured so far + the truncated marker on the record.
1922
1967
  applyStreamCapture(errorRecord, capture, true);
1923
1968
  recorder.record(errorRecord);
1924
1969
  throw err;
1925
1970
  }
1926
1971
  finally {
1927
- // 早期 break / abandon の経路。 正常完走・error 済みなら no-op。
1972
+ // Early break / abandon path. No-op if the stream already completed normally or errored.
1928
1973
  recordOnce();
1929
1974
  }
1930
1975
  })();
@@ -2033,10 +2078,10 @@ function wrapGeminiNew(client, recorder, config) {
2033
2078
  try {
2034
2079
  await recorder.budgetGate.check({ model: modelName, payload: requestArgs });
2035
2080
  const stream = (await originalStream(...args));
2036
- // C-2 + H-3 fix: AsyncGenerator wrap with finally-record + error tied to consumption
2037
- // #1 R1: trace 軸は stream 生成時(= 入口、 ambient 有効)に snapshot する。
2081
+ // Fix: AsyncGenerator wrap with finally-record + error handling tied to consumption
2082
+ // The trace fields are snapshotted at stream creation (the entry point, where the ambient context is still active).
2038
2083
  const traceMeta = buildTraceMeta(config);
2039
- // streaming 平文キャプチャ = promptBody 入口 snapshot + delta 蓄積。
2084
+ // Streaming plaintext capture: promptBody snapshotted at entry + delta accumulation.
2040
2085
  const capture = buildStreamCapture(config, () => extractGeminiPromptBody(requestArgs));
2041
2086
  return (async function* () {
2042
2087
  let lastUsage;
@@ -2100,13 +2145,13 @@ function wrapGeminiNew(client, recorder, config) {
2100
2145
  ...(errorDetails ? { errorDetails } : {}),
2101
2146
  requestMeta: buildGeminiNewRequestMeta(requestArgs),
2102
2147
  };
2103
- // 例外で切れた場合は「そこまでの本文 + truncated マーカー」を record に載せる。
2148
+ // When cut off by an exception, put the body captured so far + the truncated marker on the record.
2104
2149
  applyStreamCapture(errorRecord, capture, true);
2105
2150
  recorder.record(errorRecord);
2106
2151
  throw err;
2107
2152
  }
2108
2153
  finally {
2109
- // 早期 break / abandon の経路。 正常完走・error 済みなら no-op。
2154
+ // Early break / abandon path. No-op if the stream already completed normally or errored.
2110
2155
  recordOnce();
2111
2156
  }
2112
2157
  })();
@@ -2161,11 +2206,15 @@ function buildGeminiNewRequestMeta(requestArgs) {
2161
2206
  /**
2162
2207
  * Time-ordered record ID generator.
2163
2208
  *
2164
- * audit round2 M26 fix = 旧実装は `Date.now()` prefix + `Math.random().toString(36).slice(2,10)`
2165
- * suffix で、 (1) 非暗号乱数 (2) 値次第で suffix 8 文字未満になり実効エントロピーが
2166
- * 41bit を大きく下回る、 という二重の弱さがあった。 backend
2167
- * `INSERT ... ON CONFLICT(account_id, id) DO NOTHING` で衝突を無音 skip するため、
2168
- * 衝突した本物の call が課金・quota・観測から消える。 暗号乱数 (crypto.getRandomValues /
2169
- * randomUUID) 128bit 級の衝突耐性に引き上げる。 先頭に時刻 prefix を残して時系列順も保つ。
2209
+ * Fix: the previous implementation used a `Date.now()` prefix + a
2210
+ * `Math.random().toString(36).slice(2,10)` suffix, which had two weaknesses:
2211
+ * (1) non-cryptographic randomness, and (2) depending on the value the suffix
2212
+ * could be shorter than 8 characters, dropping the effective entropy well
2213
+ * below 41 bits. The backend silently skips collisions via
2214
+ * `INSERT ... ON CONFLICT(account_id, id) DO NOTHING`, so a real call that
2215
+ * collides would vanish from billing, quota, and observation. Cryptographic
2216
+ * randomness (crypto.getRandomValues / randomUUID) raises collision resistance
2217
+ * to the 128-bit class. The leading time prefix is kept so ordering stays
2218
+ * chronological.
2170
2219
  */
2171
2220
  //# sourceMappingURL=client.js.map