@argosvix/sdk 0.1.2 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to `@argosvix/sdk` are documented in this file.
4
4
  The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.0-alpha.1] - 2026-06-01
8
+
9
+ Pre-release adding the foundation for the **Pro+ plaintext content capture**
10
+ feature. The SDK can now optionally send prompt and completion bodies in
11
+ addition to metadata when the user explicitly opts in via dashboard consent.
12
+ The backend gate (`PLAINTEXT_STORAGE_ENABLED` env + per-account
13
+ `plaintext_storage_optin` column) is still closed by default, so this release
14
+ does not actually persist plaintext anywhere until the feature ships in
15
+ production.
16
+
17
+ ### Added
18
+ - `ArgosvixConfig.captureContent` (default `false`). When set to `true`, the
19
+ SDK extracts prompt / completion text from supported providers and forwards
20
+ them on the ingest payload. Without this flag, no plaintext is ever sent.
21
+ - `ArgosvixConfig.disablePiiRedaction` (default `false`). When `captureContent`
22
+ is `true`, the SDK runs a built-in PII redaction pass (email, credit card,
23
+ phone, マイナンバー, IPv4, IPv6) over the captured bodies before send.
24
+ Setting this flag disables the filter at the user's own risk.
25
+ - `LlmCallRecord.promptBody`, `completionBody`, and `toolCalls` fields populated
26
+ only when `captureContent` is `true`.
27
+ - New `redaction.ts` module with `redactPii()` and `redactToolCall()` helpers.
28
+ - Double safety net inside `Recorder.record()`: even if a wrapper accidentally
29
+ attaches `promptBody` while `captureContent` is `false`, the recorder strips
30
+ the field before buffering. This makes wrapper bugs non-leaky.
31
+
32
+ ### Provider coverage in this release
33
+ - OpenAI Chat Completions (non-stream success path): prompt body, completion
34
+ body, and tool calls captured.
35
+ - Anthropic Messages (non-stream success path): prompt body and completion body
36
+ captured (tool calls in a follow-up).
37
+ - OpenAI Responses, Mistral, Gemini (Legacy and New): metadata still recorded,
38
+ but `promptBody` / `completionBody` are omitted. A one-time `console.warn`
39
+ fires when `captureContent` is enabled to make this visible.
40
+ - All streaming paths across every provider: same as above. Coverage will
41
+ expand in `0.3.0-alpha.2+`.
42
+
43
+ ### Notes
44
+ - This is an `alpha` pre-release. Do not enable `captureContent` in production
45
+ until the corresponding dashboard opt-in UI ships and the legal documents
46
+ (v2.1) flip to production. See https://argosvix.com/ja/docs/plaintext for the
47
+ rollout status.
48
+ - Existing users who do not set `captureContent` are unaffected. The default
49
+ behavior — metadata-only ingest — is byte-identical to 0.2.0.
50
+
7
51
  ## [0.1.2] - 2026-05-22
8
52
 
9
53
  Documentation-only release that aligns the npm README with global SaaS norms.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 jissocyu
3
+ Copyright (c) 2026 Yuto Makihara
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -140,6 +140,108 @@ The [`examples/`](../../examples/) directory contains minimum-viable integration
140
140
  - [`examples/lambda-gemini/`](../../examples/lambda-gemini/) — AWS Lambda with `@google/genai` (note: container-reuse semantics).
141
141
  - [`examples/workers-anthropic/`](../../examples/workers-anthropic/) — Cloudflare Workers with the `flushClient` pattern.
142
142
 
143
+ ## Alert webhooks
144
+
145
+ Argosvix can deliver alert notifications to any HTTPS endpoint of your choice (in addition to email and Slack). Configure a webhook channel from <https://dashboard.argosvix.com/en/alerts>; this section documents the public contract so you can implement a receiver.
146
+
147
+ ### Delivery
148
+
149
+ - Method: `POST`
150
+ - Content-Type: `application/json`
151
+ - Timeout: 5 seconds (no retry — the next evaluator cycle will re-fire if the condition still holds)
152
+ - Redirects: disabled (`fetch redirect: "error"`)
153
+ - URL constraints: HTTPS only; private / loopback / link-local / cloud-metadata IPs and URLs with credentials are rejected at registration time
154
+
155
+ ### Headers
156
+
157
+ | Header | Value |
158
+ |---|---|
159
+ | `User-Agent` | `Argosvix-Webhook/1.0` |
160
+ | `X-Argosvix-Event` | `alert.triggered` (only event type for now) |
161
+ | `X-Argosvix-Signature` | `sha256=<hex>` — present only when a signing secret is configured |
162
+ | `X-Argosvix-Timestamp` | ISO-8601 UTC — present only when a signing secret is configured |
163
+
164
+ ### Payload
165
+
166
+ ```jsonc
167
+ {
168
+ "alertId": "01HXZ...",
169
+ "name": "monthly cost > $50",
170
+ "alertType": "monthly_budget", // cost_threshold | error_rate | latency_degradation | monthly_budget
171
+ "thresholdValue": 50,
172
+ "observedValue": 64.20,
173
+ "windowMinutes": 60,
174
+ "windowDescription": "60 min",
175
+ "filterProvider": "openai", // or null
176
+ "filterModel": null, // or string
177
+ "comparison": "exceeded threshold",
178
+ "triggeredAt": "2026-05-25T10:23:45.678Z",
179
+ "dashboardUrl": "https://dashboard.argosvix.com/en/alerts?alertId=01HXZ..."
180
+ }
181
+ ```
182
+
183
+ `thresholdValue` and `observedValue` units are USD for cost alerts, percent for `error_rate`, and milliseconds for `latency_degradation`. Schema is stable: new fields may be added (additive only); existing fields keep their names and semantics.
184
+
185
+ ### Verifying the signature
186
+
187
+ When a signing secret is configured, the signature header is computed as:
188
+
189
+ ```
190
+ X-Argosvix-Signature = "sha256=" + hex(HMAC_SHA256(secret, X-Argosvix-Timestamp + "." + body))
191
+ ```
192
+
193
+ Reject any request whose signature does not match (and optionally drop requests with a timestamp older than your replay window).
194
+
195
+ ```typescript
196
+ // Node 18+ — verifyArgosvixSignature.ts
197
+ import { createHmac, timingSafeEqual } from "node:crypto";
198
+
199
+ export function verifyArgosvixSignature(
200
+ secret: string,
201
+ body: string, // raw request body string
202
+ signatureHeader: string, // "sha256=<hex>"
203
+ timestampHeader: string, // ISO-8601 string
204
+ maxAgeMs = 5 * 60 * 1000,
205
+ ): boolean {
206
+ const ts = Date.parse(timestampHeader);
207
+ if (Number.isNaN(ts) || Math.abs(Date.now() - ts) > maxAgeMs) return false;
208
+
209
+ const expected = "sha256=" +
210
+ createHmac("sha256", secret).update(`${timestampHeader}.${body}`).digest("hex");
211
+ const a = Buffer.from(signatureHeader, "utf8");
212
+ const b = Buffer.from(expected, "utf8");
213
+ return a.length === b.length && timingSafeEqual(a, b);
214
+ }
215
+ ```
216
+
217
+ ```python
218
+ # Python 3 — verify_argosvix_signature.py
219
+ import hashlib, hmac, time
220
+ from datetime import datetime, timezone
221
+
222
+ def verify_argosvix_signature(secret: str, body: str,
223
+ signature_header: str, timestamp_header: str,
224
+ max_age_seconds: int = 300) -> bool:
225
+ try:
226
+ ts = datetime.fromisoformat(timestamp_header.replace("Z", "+00:00"))
227
+ except ValueError:
228
+ return False
229
+ if abs(time.time() - ts.timestamp()) > max_age_seconds:
230
+ return False
231
+ expected = "sha256=" + hmac.new(
232
+ secret.encode(), f"{timestamp_header}.{body}".encode(), hashlib.sha256
233
+ ).hexdigest()
234
+ return hmac.compare_digest(signature_header, expected)
235
+ ```
236
+
237
+ ### Tag-key conventions
238
+
239
+ Tag keys used in queries (`channelTargets.webhook.url` request paths, dashboard filters, etc.) must match `^[A-Za-z0-9_](?:[A-Za-z0-9_-]{0,62}[A-Za-z0-9_])?$` — alphanumeric plus `_` `-`, no leading/trailing hyphen, 1–64 characters. Sticking to this in the SDK `tags: { ... }` map keeps every downstream filter / aggregation usable.
240
+
241
+ ### Residual SSRF risk (DNS rebinding)
242
+
243
+ Hostname validation happens at registration and at delivery time, but Cloudflare Workers does not expose connect-level IP control, so an attacker-controlled DNS that flips a public name to an RFC1918 / metadata IP between checks can still bypass the policy. Treat webhook receivers as part of your trust boundary; do not host them inside private networks that your perimeter would otherwise protect.
244
+
143
245
  ## API
144
246
 
145
247
  ### `wrap<T>(client: T, config?: ArgosvixConfig): T`
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,YAAY,CAAC;AAE1E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAKzC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAE,cAAmB,GAAG,CAAC,CAiChF;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAE3D"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,YAAY,CAAC;AAE1E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAKzC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAE,cAAmB,GAAG,CAAC,CA6ChF;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAE3D"}
package/dist/client.js CHANGED
@@ -56,6 +56,20 @@ export function wrap(client, config = {}) {
56
56
  break;
57
57
  }
58
58
  }
59
+ // 平文 capture が有効でも、未対応 provider / 経路では promptBody や
60
+ // completionBody が乗らないことを 1 回だけ console.warn で明示する。
61
+ // v0.3.0-alpha.1 対応 = openai (Chat Completions) と anthropic (Messages)
62
+ // の非ストリーミング success path のみ。 OpenAI Responses や Mistral、
63
+ // Gemini、 全 streaming 経路は段階追加。
64
+ if (config.captureContent === true) {
65
+ if (provider === "openai" && isOpenAIResponsesLike(client)) {
66
+ warnUnsupportedCaptureProvider("openai (Responses API)");
67
+ }
68
+ if (provider === "mistral")
69
+ warnUnsupportedCaptureProvider("mistral");
70
+ if (provider === "gemini")
71
+ warnUnsupportedCaptureProvider("gemini");
72
+ }
59
73
  wrappedClients.set(client, recorder);
60
74
  return client;
61
75
  }
@@ -95,6 +109,153 @@ function detectProvider(client, override) {
95
109
  return "openai";
96
110
  return "unknown";
97
111
  }
112
+ /**
113
+ * F-2 carry = wrap config に渡された trace 軸 (traceId / spanId /
114
+ * parentSpanId) を record に乗せる helper。 全 provider の record()
115
+ * 呼び出しで spread して 一律 carry する。 未指定 field は object に
116
+ * 出さない (= JSON 送信 payload 軽量化 + backend で undefined と NULL
117
+ * の区別 不要)。
118
+ */
119
+ function buildTraceMeta(config) {
120
+ const meta = {};
121
+ if (config.traceId)
122
+ meta.traceId = config.traceId;
123
+ if (config.spanId)
124
+ meta.spanId = config.spanId;
125
+ if (config.parentSpanId)
126
+ meta.parentSpanId = config.parentSpanId;
127
+ return meta;
128
+ }
129
+ /**
130
+ * Pro+ 平文保存機能の SDK 側 opt-in 抽出 helper。
131
+ *
132
+ * captureContent が true のときに wrapper が呼んで、 provider 毎の
133
+ * request / response 形状から prompt / completion / tool calls を 文字列
134
+ * (および JSON 文字列)として抽出する。 PII redaction は Recorder 側で
135
+ * 一括して適用されるので、 ここでは生の文字列を返すのみ。
136
+ *
137
+ * v0.3.0-alpha.1 段階の対応 provider:
138
+ * - openai (Chat Completions) = 全項目対応
139
+ * - anthropic (Messages) = prompt + completion 対応(tool call は段階追加)
140
+ * - 他 provider と全 streaming 経路 = 未対応、 captureContent が true でも
141
+ * promptBody / completionBody は付与されない(段階4 以降で順次追加)。
142
+ * console.warn で明示する。
143
+ */
144
+ function extractOpenAIChatPromptBody(requestArgs) {
145
+ if (!Array.isArray(requestArgs.messages) || requestArgs.messages.length === 0) {
146
+ return undefined;
147
+ }
148
+ try {
149
+ return JSON.stringify(requestArgs.messages);
150
+ }
151
+ catch {
152
+ return undefined;
153
+ }
154
+ }
155
+ function extractOpenAIChatCompletionBody(response) {
156
+ if (!response || typeof response !== "object")
157
+ return undefined;
158
+ const choices = response.choices;
159
+ if (!Array.isArray(choices) || choices.length === 0)
160
+ return undefined;
161
+ const first = choices[0];
162
+ const content = first?.message?.content;
163
+ if (typeof content === "string")
164
+ return content;
165
+ // content が array (multi-modal の場合) の場合は JSON 文字列で carry
166
+ if (Array.isArray(content)) {
167
+ try {
168
+ return JSON.stringify(content);
169
+ }
170
+ catch {
171
+ return undefined;
172
+ }
173
+ }
174
+ return undefined;
175
+ }
176
+ function extractOpenAIChatToolCalls(response) {
177
+ if (!response || typeof response !== "object")
178
+ return undefined;
179
+ const choices = response.choices;
180
+ if (!Array.isArray(choices) || choices.length === 0)
181
+ return undefined;
182
+ const first = choices[0];
183
+ const toolCalls = first?.message?.tool_calls;
184
+ if (!Array.isArray(toolCalls) || toolCalls.length === 0)
185
+ return undefined;
186
+ const result = [];
187
+ for (const tc of toolCalls) {
188
+ if (!tc || typeof tc !== "object")
189
+ continue;
190
+ const fn = tc.function;
191
+ const name = typeof fn?.name === "string" ? fn.name : "unknown";
192
+ const args = typeof fn?.arguments === "string"
193
+ ? fn.arguments
194
+ : fn?.arguments !== undefined
195
+ ? safeStringify(fn.arguments)
196
+ : undefined;
197
+ result.push(args !== undefined ? { name, arguments: args } : { name });
198
+ }
199
+ return result.length > 0 ? result : undefined;
200
+ }
201
+ function extractAnthropicPromptBody(requestArgs) {
202
+ if (!Array.isArray(requestArgs.messages) || requestArgs.messages.length === 0) {
203
+ return undefined;
204
+ }
205
+ try {
206
+ return JSON.stringify(requestArgs.messages);
207
+ }
208
+ catch {
209
+ return undefined;
210
+ }
211
+ }
212
+ function extractAnthropicCompletionBody(response) {
213
+ if (!response || typeof response !== "object")
214
+ return undefined;
215
+ // Anthropic response shape = { content: [{ type: "text", text: "..." }, ...] }
216
+ const content = response.content;
217
+ if (Array.isArray(content)) {
218
+ const texts = [];
219
+ for (const block of content) {
220
+ if (block && typeof block === "object") {
221
+ const t = block.type;
222
+ const txt = block.text;
223
+ if (t === "text" && typeof txt === "string") {
224
+ texts.push(txt);
225
+ }
226
+ }
227
+ }
228
+ if (texts.length > 0)
229
+ return texts.join("\n");
230
+ // text block がなければ全 content を JSON 文字列として carry
231
+ try {
232
+ return JSON.stringify(content);
233
+ }
234
+ catch {
235
+ return undefined;
236
+ }
237
+ }
238
+ return undefined;
239
+ }
240
+ function safeStringify(value) {
241
+ try {
242
+ return JSON.stringify(value);
243
+ }
244
+ catch {
245
+ return undefined;
246
+ }
247
+ }
248
+ let captureContentWarnedProviders = null;
249
+ function warnUnsupportedCaptureProvider(label) {
250
+ if (captureContentWarnedProviders === null) {
251
+ captureContentWarnedProviders = new Set();
252
+ }
253
+ if (captureContentWarnedProviders.has(label))
254
+ return;
255
+ captureContentWarnedProviders.add(label);
256
+ // eslint-disable-next-line no-console
257
+ console.warn(`[argosvix] captureContent is enabled but ${label} body extraction is not yet supported in this SDK version. Metadata is still recorded; plaintext is omitted. Coverage will expand in subsequent releases.`);
258
+ }
98
259
  function extractErrorDetails(err) {
99
260
  if (!err || typeof err !== "object")
100
261
  return undefined;
@@ -139,7 +300,7 @@ function wrapOpenAIChat(client, recorder, config) {
139
300
  const model = r.model || requestArgs.model || "unknown";
140
301
  const promptTokens = r.usage?.prompt_tokens ?? 0;
141
302
  const completionTokens = r.usage?.completion_tokens ?? 0;
142
- recorder.record({
303
+ const record = {
143
304
  id,
144
305
  provider: "openai",
145
306
  model,
@@ -150,8 +311,21 @@ function wrapOpenAIChat(client, recorder, config) {
150
311
  latencyMs,
151
312
  timestamp: new Date().toISOString(),
152
313
  tags: { ...(config.tags ?? {}) },
314
+ ...buildTraceMeta(config),
153
315
  requestMeta: buildOpenAIRequestMeta(requestArgs),
154
- });
316
+ };
317
+ if (config.captureContent === true) {
318
+ const promptBody = extractOpenAIChatPromptBody(requestArgs);
319
+ if (promptBody !== undefined)
320
+ record.promptBody = promptBody;
321
+ const completionBody = extractOpenAIChatCompletionBody(response);
322
+ if (completionBody !== undefined)
323
+ record.completionBody = completionBody;
324
+ const toolCalls = extractOpenAIChatToolCalls(response);
325
+ if (toolCalls !== undefined)
326
+ record.toolCalls = toolCalls;
327
+ }
328
+ recorder.record(record);
155
329
  return response;
156
330
  }
157
331
  catch (err) {
@@ -167,6 +341,7 @@ function wrapOpenAIChat(client, recorder, config) {
167
341
  latencyMs: Date.now() - start,
168
342
  timestamp: new Date().toISOString(),
169
343
  tags: { ...(config.tags ?? {}) },
344
+ ...buildTraceMeta(config),
170
345
  error: err instanceof Error ? err.message : String(err),
171
346
  ...(errorDetails ? { errorDetails } : {}),
172
347
  requestMeta: buildOpenAIRequestMeta(requestArgs),
@@ -204,6 +379,7 @@ async function* wrapOpenAIStream(stream, recorder, requestArgs, start, id, confi
204
379
  latencyMs: Date.now() - start,
205
380
  timestamp: new Date().toISOString(),
206
381
  tags: { ...(config.tags ?? {}) },
382
+ ...buildTraceMeta(config),
207
383
  requestMeta: buildOpenAIRequestMeta(requestArgs),
208
384
  });
209
385
  }
@@ -220,6 +396,7 @@ async function* wrapOpenAIStream(stream, recorder, requestArgs, start, id, confi
220
396
  latencyMs: Date.now() - start,
221
397
  timestamp: new Date().toISOString(),
222
398
  tags: { ...(config.tags ?? {}) },
399
+ ...buildTraceMeta(config),
223
400
  error: err instanceof Error ? err.message : String(err),
224
401
  ...(errorDetails ? { errorDetails } : {}),
225
402
  requestMeta: buildOpenAIRequestMeta(requestArgs),
@@ -273,6 +450,7 @@ function wrapOpenAIResponses(client, recorder, config) {
273
450
  latencyMs,
274
451
  timestamp: new Date().toISOString(),
275
452
  tags: { ...(config.tags ?? {}) },
453
+ ...buildTraceMeta(config),
276
454
  requestMeta: buildResponsesRequestMeta(requestArgs),
277
455
  });
278
456
  return response;
@@ -290,6 +468,7 @@ function wrapOpenAIResponses(client, recorder, config) {
290
468
  latencyMs: Date.now() - start,
291
469
  timestamp: new Date().toISOString(),
292
470
  tags: { ...(config.tags ?? {}) },
471
+ ...buildTraceMeta(config),
293
472
  error: err instanceof Error ? err.message : String(err),
294
473
  ...(errorDetails ? { errorDetails } : {}),
295
474
  requestMeta: buildResponsesRequestMeta(requestArgs),
@@ -332,7 +511,7 @@ function wrapAnthropic(client, recorder, config) {
332
511
  const model = r.model || requestArgs.model || "unknown";
333
512
  const promptTokens = r.usage?.input_tokens ?? 0;
334
513
  const completionTokens = r.usage?.output_tokens ?? 0;
335
- recorder.record({
514
+ const record = {
336
515
  id,
337
516
  provider: "anthropic",
338
517
  model,
@@ -343,8 +522,18 @@ function wrapAnthropic(client, recorder, config) {
343
522
  latencyMs,
344
523
  timestamp: new Date().toISOString(),
345
524
  tags: { ...(config.tags ?? {}) },
525
+ ...buildTraceMeta(config),
346
526
  requestMeta: buildAnthropicRequestMeta(requestArgs),
347
- });
527
+ };
528
+ if (config.captureContent === true) {
529
+ const promptBody = extractAnthropicPromptBody(requestArgs);
530
+ if (promptBody !== undefined)
531
+ record.promptBody = promptBody;
532
+ const completionBody = extractAnthropicCompletionBody(response);
533
+ if (completionBody !== undefined)
534
+ record.completionBody = completionBody;
535
+ }
536
+ recorder.record(record);
348
537
  return response;
349
538
  }
350
539
  catch (err) {
@@ -360,6 +549,7 @@ function wrapAnthropic(client, recorder, config) {
360
549
  latencyMs: Date.now() - start,
361
550
  timestamp: new Date().toISOString(),
362
551
  tags: { ...(config.tags ?? {}) },
552
+ ...buildTraceMeta(config),
363
553
  error: err instanceof Error ? err.message : String(err),
364
554
  ...(errorDetails ? { errorDetails } : {}),
365
555
  requestMeta: buildAnthropicRequestMeta(requestArgs),
@@ -398,6 +588,7 @@ async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, co
398
588
  latencyMs: Date.now() - start,
399
589
  timestamp: new Date().toISOString(),
400
590
  tags: { ...(config.tags ?? {}) },
591
+ ...buildTraceMeta(config),
401
592
  requestMeta: buildAnthropicRequestMeta(requestArgs),
402
593
  });
403
594
  }
@@ -414,6 +605,7 @@ async function* wrapAnthropicStream(stream, recorder, requestArgs, start, id, co
414
605
  latencyMs: Date.now() - start,
415
606
  timestamp: new Date().toISOString(),
416
607
  tags: { ...(config.tags ?? {}) },
608
+ ...buildTraceMeta(config),
417
609
  error: err instanceof Error ? err.message : String(err),
418
610
  ...(errorDetails ? { errorDetails } : {}),
419
611
  requestMeta: buildAnthropicRequestMeta(requestArgs),
@@ -459,6 +651,7 @@ function wrapMistral(client, recorder, config) {
459
651
  latencyMs,
460
652
  timestamp: new Date().toISOString(),
461
653
  tags: { ...(config.tags ?? {}) },
654
+ ...buildTraceMeta(config),
462
655
  requestMeta: buildMistralRequestMeta(requestArgs),
463
656
  });
464
657
  return response;
@@ -476,6 +669,7 @@ function wrapMistral(client, recorder, config) {
476
669
  latencyMs: Date.now() - start,
477
670
  timestamp: new Date().toISOString(),
478
671
  tags: { ...(config.tags ?? {}) },
672
+ ...buildTraceMeta(config),
479
673
  error: err instanceof Error ? err.message : String(err),
480
674
  ...(errorDetails ? { errorDetails } : {}),
481
675
  requestMeta: buildMistralRequestMeta(requestArgs),
@@ -507,6 +701,7 @@ function wrapMistral(client, recorder, config) {
507
701
  latencyMs: Date.now() - start,
508
702
  timestamp: new Date().toISOString(),
509
703
  tags: { ...(config.tags ?? {}) },
704
+ ...buildTraceMeta(config),
510
705
  error: err instanceof Error ? err.message : String(err),
511
706
  ...(errorDetails ? { errorDetails } : {}),
512
707
  requestMeta: buildMistralRequestMeta(requestArgs),
@@ -546,6 +741,7 @@ async function* wrapMistralStream(stream, recorder, requestArgs, start, id, conf
546
741
  latencyMs: Date.now() - start,
547
742
  timestamp: new Date().toISOString(),
548
743
  tags: { ...(config.tags ?? {}) },
744
+ ...buildTraceMeta(config),
549
745
  requestMeta: buildMistralRequestMeta(requestArgs),
550
746
  });
551
747
  }
@@ -562,6 +758,7 @@ async function* wrapMistralStream(stream, recorder, requestArgs, start, id, conf
562
758
  latencyMs: Date.now() - start,
563
759
  timestamp: new Date().toISOString(),
564
760
  tags: { ...(config.tags ?? {}) },
761
+ ...buildTraceMeta(config),
565
762
  error: err instanceof Error ? err.message : String(err),
566
763
  ...(errorDetails ? { errorDetails } : {}),
567
764
  requestMeta: buildMistralRequestMeta(requestArgs),
@@ -619,6 +816,7 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
619
816
  latencyMs: Date.now() - start,
620
817
  timestamp: new Date().toISOString(),
621
818
  tags: { ...(config.tags ?? {}) },
819
+ ...buildTraceMeta(config),
622
820
  requestMeta: buildGeminiRequestMeta(requestArgs),
623
821
  });
624
822
  return result;
@@ -636,6 +834,7 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
636
834
  latencyMs: Date.now() - start,
637
835
  timestamp: new Date().toISOString(),
638
836
  tags: { ...(config.tags ?? {}) },
837
+ ...buildTraceMeta(config),
639
838
  error: err instanceof Error ? err.message : String(err),
640
839
  ...(errorDetails ? { errorDetails } : {}),
641
840
  requestMeta: buildGeminiRequestMeta(requestArgs),
@@ -675,6 +874,7 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
675
874
  latencyMs: Date.now() - start,
676
875
  timestamp: new Date().toISOString(),
677
876
  tags: { ...(config.tags ?? {}) },
877
+ ...buildTraceMeta(config),
678
878
  requestMeta: buildGeminiRequestMeta(requestArgs),
679
879
  });
680
880
  }
@@ -691,6 +891,7 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
691
891
  latencyMs: Date.now() - start,
692
892
  timestamp: new Date().toISOString(),
693
893
  tags: { ...(config.tags ?? {}) },
894
+ ...buildTraceMeta(config),
694
895
  error: err instanceof Error ? err.message : String(err),
695
896
  ...(errorDetails ? { errorDetails } : {}),
696
897
  requestMeta: buildGeminiRequestMeta(requestArgs),
@@ -713,6 +914,7 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
713
914
  latencyMs: Date.now() - start,
714
915
  timestamp: new Date().toISOString(),
715
916
  tags: { ...(config.tags ?? {}) },
917
+ ...buildTraceMeta(config),
716
918
  error: err instanceof Error ? err.message : String(err),
717
919
  ...(errorDetails ? { errorDetails } : {}),
718
920
  requestMeta: buildGeminiRequestMeta(requestArgs),
@@ -750,6 +952,7 @@ function wrapGeminiNew(client, recorder, config) {
750
952
  latencyMs: Date.now() - start,
751
953
  timestamp: new Date().toISOString(),
752
954
  tags: { ...(config.tags ?? {}) },
955
+ ...buildTraceMeta(config),
753
956
  requestMeta: buildGeminiNewRequestMeta(requestArgs),
754
957
  });
755
958
  return result;
@@ -767,6 +970,7 @@ function wrapGeminiNew(client, recorder, config) {
767
970
  latencyMs: Date.now() - start,
768
971
  timestamp: new Date().toISOString(),
769
972
  tags: { ...(config.tags ?? {}) },
973
+ ...buildTraceMeta(config),
770
974
  error: err instanceof Error ? err.message : String(err),
771
975
  ...(errorDetails ? { errorDetails } : {}),
772
976
  requestMeta: buildGeminiNewRequestMeta(requestArgs),
@@ -805,6 +1009,7 @@ function wrapGeminiNew(client, recorder, config) {
805
1009
  latencyMs: Date.now() - start,
806
1010
  timestamp: new Date().toISOString(),
807
1011
  tags: { ...(config.tags ?? {}) },
1012
+ ...buildTraceMeta(config),
808
1013
  requestMeta: buildGeminiNewRequestMeta(requestArgs),
809
1014
  });
810
1015
  }
@@ -823,6 +1028,7 @@ function wrapGeminiNew(client, recorder, config) {
823
1028
  latencyMs: Date.now() - start,
824
1029
  timestamp: new Date().toISOString(),
825
1030
  tags: { ...(config.tags ?? {}) },
1031
+ ...buildTraceMeta(config),
826
1032
  error: err instanceof Error ? err.message : String(err),
827
1033
  ...(errorDetails ? { errorDetails } : {}),
828
1034
  requestMeta: buildGeminiNewRequestMeta(requestArgs),
@@ -844,6 +1050,7 @@ function wrapGeminiNew(client, recorder, config) {
844
1050
  latencyMs: Date.now() - start,
845
1051
  timestamp: new Date().toISOString(),
846
1052
  tags: { ...(config.tags ?? {}) },
1053
+ ...buildTraceMeta(config),
847
1054
  error: err instanceof Error ? err.message : String(err),
848
1055
  ...(errorDetails ? { errorDetails } : {}),
849
1056
  requestMeta: buildGeminiNewRequestMeta(requestArgs),