@voightxyz/anthropic 0.1.0-beta.1 → 0.1.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/CHANGELOG.md +28 -0
- package/README.md +49 -20
- package/dist/index.cjs +7 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.0] — 2026-05-16
|
|
6
|
+
|
|
7
|
+
First stable release. Consolidates beta.1 and beta.2.
|
|
8
|
+
|
|
9
|
+
### Capabilities
|
|
10
|
+
|
|
11
|
+
- `wrapAnthropic(client, options)` — two-layer Proxy (`client → messages → create`). Everything outside `messages.create` passes through untouched.
|
|
12
|
+
- Non-streaming Messages capture: text extracted from `content[].text` blocks, tool calls flattened from `content[].tool_use` blocks (input JSON-stringified to the same `arguments: string` shape `@voightxyz/openai` produces — dashboards render both providers identically).
|
|
13
|
+
- Streaming Messages capture: state machine over the typed event sequence (`message_start` / `content_block_start` / `content_block_delta` / `content_block_stop` / `message_delta` / `message_stop`). Per-index aggregator concatenates text deltas (`text_delta.text`) and tool-use deltas (`input_json_delta.partial_json`). Initial usage from `message_start` carries `input_tokens` + cache fields; final `output_tokens` lands on `message_delta`.
|
|
14
|
+
- Path-A token breakdown: `cache_read_input_tokens` → `metadata.tokens.cache_read`, `cache_creation_input_tokens` → `metadata.tokens.cache_creation`. Both emitted only when strictly positive. `input` + `output` + `total` always present. The backend Anthropic pricing engine applies the 0.10× cache_read and 1.25× cache_creation multipliers automatically.
|
|
15
|
+
- First tool call's name mirrored into top-level `toolExecuted` so the audit-log DETAIL column renders meaningfully for LLM events (same shape used for hook events).
|
|
16
|
+
- `sessionId` emission: each wrapper instance resolves a UUID v4 once (or accepts an explicit override) and stamps it on `metadata.sessionId` for every event. Dashboards group events sharing a sessionId into a single trace timeline.
|
|
17
|
+
- Three-level privacy redaction (`minimal` / `standard` / `full`) over prompts, response text, and tool arguments via a 12-pattern catalogue. Function-call names always survive as tags.
|
|
18
|
+
- Fire-and-forget HTTP ingest to `https://api.voight.xyz/v1/events`. Never throws, never blocks the caller.
|
|
19
|
+
- API key + agent identity resolution: `voightApiKey` option → `VOIGHT_KEY` env → `null`. `agent` option → `VOIGHT_AGENT` env → `HOSTNAME` env → `'unknown-agent'`.
|
|
20
|
+
- Non-fatal failure modes: `enabled: false` and missing API key both return the original client untouched.
|
|
21
|
+
|
|
22
|
+
### Tests
|
|
23
|
+
|
|
24
|
+
- 65 unit tests across privacy, identity, ingest, messages, and wrap surfaces. All green.
|
|
25
|
+
- End-to-end smoke verified against real Anthropic + real Voight backend: text, streaming text, tool use (both transports), `cache_read` on cached prompts.
|
|
26
|
+
|
|
27
|
+
## [0.1.0-beta.2] — 2026-05-16
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `sessionId` is now stamped on every emitted event under `metadata.sessionId`. The wrapper auto-generates a UUID v4 once per `wrapAnthropic()` call and reuses it for the life of the wrapped client. An explicit `options.sessionId` overrides the auto value so callers can scope a trace per-user / per-conversation / per-request. The Voight dashboard groups events with the same `sessionId` into a single trace timeline.
|
|
32
|
+
|
|
5
33
|
## [0.1.0-beta.1] — 2026-05-16
|
|
6
34
|
|
|
7
35
|
### Added
|
package/README.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# @voightxyz/anthropic
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Voight observability for the Anthropic SDK. Wrap your Anthropic client and capture every Messages call — prompts, tokens, cache reads, cache creations, tool use, costs, latency, errors — surfaced live in the [Voight dashboard](https://voight.xyz).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Same author and same backend as [`@voightxyz/openai`](https://www.npmjs.com/package/@voightxyz/openai). Drop in whichever provider your app uses — the events land side-by-side in your Voight dashboard.
|
|
5
|
+
Same backend and dashboard as [`@voightxyz/openai`](https://www.npmjs.com/package/@voightxyz/openai). Drop in whichever provider your app uses; events from both land side-by-side under the same agent.
|
|
8
6
|
|
|
9
7
|
## Install
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
|
-
npm install @anthropic-ai/sdk @voightxyz/anthropic
|
|
10
|
+
npm install @anthropic-ai/sdk @voightxyz/anthropic
|
|
13
11
|
```
|
|
14
12
|
|
|
15
13
|
## Quick start
|
|
@@ -24,7 +22,7 @@ const client = wrapAnthropic(new Anthropic(), {
|
|
|
24
22
|
})
|
|
25
23
|
|
|
26
24
|
const response = await client.messages.create({
|
|
27
|
-
model: 'claude-
|
|
25
|
+
model: 'claude-haiku-4-5',
|
|
28
26
|
max_tokens: 1024,
|
|
29
27
|
messages: [{ role: 'user', content: 'Hello' }],
|
|
30
28
|
})
|
|
@@ -32,22 +30,53 @@ const response = await client.messages.create({
|
|
|
32
30
|
|
|
33
31
|
That's it — every call is captured automatically. Visit your [Voight dashboard](https://voight.xyz) to see them in real time.
|
|
34
32
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
Beta scaffold — the public `wrapAnthropic` entrypoint is a pass-through today. The Messages instrument lands in the next release. Track:
|
|
33
|
+
## What's captured
|
|
38
34
|
|
|
39
|
-
| Signal |
|
|
35
|
+
| Signal | Where it lands |
|
|
40
36
|
|---|---|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
| `
|
|
44
|
-
| Token counts (input
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
| Model id (with version suffix) | `model` |
|
|
38
|
+
| Prompt messages | `input.messages` |
|
|
39
|
+
| Response text (aggregated from `content[].text` blocks) | `metadata.responseText` |
|
|
40
|
+
| Token counts (input / output / total) | `metadata.tokens` |
|
|
41
|
+
| Cache reads (`cache_read_input_tokens`) | `metadata.tokens.cache_read` |
|
|
42
|
+
| Cache creations (`cache_creation_input_tokens`) | `metadata.tokens.cache_creation` |
|
|
43
|
+
| Tool use (full array) | `metadata.toolCalls` + `toolExecuted` |
|
|
44
|
+
| Streaming flag | `metadata.streaming` |
|
|
45
|
+
| Trace grouping (auto UUID or explicit) | `metadata.sessionId` |
|
|
46
|
+
| Stop reason | `metadata.finishReason` |
|
|
47
|
+
| Latency (ms) | `durationMs` |
|
|
48
|
+
| Errors (re-thrown to the caller) | `errorMessage` + `outcome: 'failed'` |
|
|
49
|
+
|
|
50
|
+
## Supported endpoints
|
|
51
|
+
|
|
52
|
+
- `client.messages.create` — Messages API (non-streaming + streaming, tool use, cache breakpoints)
|
|
53
|
+
|
|
54
|
+
The wrapper passes everything else through untouched. Bedrock and Vertex clients are on the [0.2.0 roadmap](./CHANGELOG.md).
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
| Option | Type | Default | Purpose |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| `voightApiKey` | string | `process.env.VOIGHT_KEY` | Your Voight key from the dashboard |
|
|
61
|
+
| `agent` | string | `process.env.VOIGHT_AGENT` → `HOSTNAME` → `'unknown-agent'` | Stable identifier surfaced in the dashboard |
|
|
62
|
+
| `apiBase` | string | `https://api.voight.xyz` | Override for self-hosted deployments |
|
|
63
|
+
| `privacy` | `'minimal' \| 'standard' \| 'full'` | `'standard'` | Capture aggressiveness |
|
|
64
|
+
| `sessionId` | string | auto UUID v4 | Trace grouping. Stable across calls of one wrapper instance |
|
|
65
|
+
| `enabled` | boolean | `true` | Kill switch — returns the original client untouched |
|
|
66
|
+
|
|
67
|
+
## Privacy
|
|
68
|
+
|
|
69
|
+
Three levels apply to prompts, response text, and tool-call arguments. The function name in `toolExecuted` always survives as a tag (not user content).
|
|
70
|
+
|
|
71
|
+
| Level | Prompts | Response text | Tool arguments | Tokens / timing / model |
|
|
72
|
+
| --- | --- | --- | --- | --- |
|
|
73
|
+
| `minimal` | dropped | dropped | dropped | kept |
|
|
74
|
+
| `standard` (default) | scrubbed | scrubbed | scrubbed | kept |
|
|
75
|
+
| `full` | verbatim | verbatim | verbatim | kept |
|
|
76
|
+
|
|
77
|
+
Standard scrubs 12 patterns: PEM private keys, JWTs, Anthropic / OpenAI / Stripe live / GitHub / AWS / Slack / Voight API keys, emails, E.164 phones, and Luhn-validated credit cards.
|
|
78
|
+
|
|
79
|
+
See [CHANGELOG.md](./CHANGELOG.md) for release notes.
|
|
51
80
|
|
|
52
81
|
## License
|
|
53
82
|
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,9 @@ __export(index_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(index_exports);
|
|
26
26
|
|
|
27
|
+
// src/wrap.ts
|
|
28
|
+
var import_node_crypto = require("crypto");
|
|
29
|
+
|
|
27
30
|
// src/identity.ts
|
|
28
31
|
function nonBlank(value) {
|
|
29
32
|
if (typeof value !== "string") return null;
|
|
@@ -235,7 +238,8 @@ function assembleEvent(args) {
|
|
|
235
238
|
const metadata = {
|
|
236
239
|
source: "anthropic-sdk",
|
|
237
240
|
privacyLevel: ctx.privacy,
|
|
238
|
-
streaming
|
|
241
|
+
streaming,
|
|
242
|
+
sessionId: ctx.sessionId
|
|
239
243
|
};
|
|
240
244
|
if (tokens) metadata.tokens = tokens;
|
|
241
245
|
if (args.finishReason !== void 0 && args.finishReason !== null) {
|
|
@@ -451,9 +455,11 @@ function wrapAnthropic(client, options = {}) {
|
|
|
451
455
|
apiKey,
|
|
452
456
|
fetch: opts._fetch
|
|
453
457
|
});
|
|
458
|
+
const sessionId = typeof opts.sessionId === "string" && opts.sessionId.trim().length > 0 ? opts.sessionId.trim() : (0, import_node_crypto.randomUUID)();
|
|
454
459
|
const ctx = {
|
|
455
460
|
agentId,
|
|
456
461
|
privacy: opts.privacy ?? "standard",
|
|
462
|
+
sessionId,
|
|
457
463
|
ingest,
|
|
458
464
|
now: () => Date.now()
|
|
459
465
|
};
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/identity.ts","../src/ingest.ts","../src/privacy.ts","../src/instruments/messages.ts","../src/wrap.ts"],"sourcesContent":["// Public surface of @voightxyz/anthropic.\n//\n// The package's contract is intentionally tiny: one function that\n// takes an Anthropic client and returns a wrapped client with the\n// same shape. Everything else (ingest transport, privacy redaction,\n// identity resolution) is an implementation detail and not part of\n// the public API.\n\nexport { wrapAnthropic } from './wrap.js'\nexport type { WrapOptions, PrivacyLevel } from './types.js'\n","/**\n * Identity resolution: API key + agent label.\n *\n * Both helpers are pure — they take the user's options and an `env`\n * record, and return a result. `env` defaults to `process.env` when\n * called bare; passing it explicitly lets tests exercise every\n * fallback path without mutating global state.\n *\n * Resolution priority is fixed and documented at each call site:\n *\n * resolveApiKey: options.voightApiKey → env.VOIGHT_KEY → null\n * resolveAgent: options.agent → env.VOIGHT_AGENT\n * → env.HOSTNAME → 'unknown-agent'\n *\n * Empty and whitespace-only values are treated as missing so a\n * misconfigured env (`VOIGHT_KEY=`) falls through cleanly instead\n * of being mistaken for a real key.\n */\n\nexport interface IdentityOptions {\n voightApiKey?: string | undefined\n agent?: string | undefined\n}\n\ntype Env = Record<string, string | undefined>\n\n/**\n * Trim and return the value, or `null` if it's missing / blank.\n * Centralises the \"empty counts as missing\" rule that both helpers\n * apply at every layer.\n */\nfunction nonBlank(value: string | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length === 0 ? null : trimmed\n}\n\n/**\n * Resolve the Voight API key for outgoing events.\n *\n * Returns `null` when nothing usable is configured. Callers decide\n * how to react — `wrapOpenAI` logs a one-time warning and falls\n * back to a no-op transport, so a missing key never crashes the\n * host app.\n */\nexport function resolveApiKey(\n options: IdentityOptions = {},\n env: Env = process.env,\n): string | null {\n return nonBlank(options.voightApiKey) ?? nonBlank(env.VOIGHT_KEY)\n}\n\n/**\n * Resolve the agent label that groups events in the dashboard.\n *\n * Always returns a non-empty string: when nothing else resolves we\n * emit `'unknown-agent'` so the event is still ingestable and the\n * misconfiguration shows up on screen instead of being silently\n * dropped.\n */\nexport function resolveAgent(\n options: IdentityOptions = {},\n env: Env = process.env,\n): string {\n return (\n nonBlank(options.agent) ??\n nonBlank(env.VOIGHT_AGENT) ??\n nonBlank(env.HOSTNAME) ??\n 'unknown-agent'\n )\n}\n","/**\n * Fire-and-forget HTTP client for the Voight ingest endpoint.\n *\n * Why \"fire and forget\":\n *\n * The wrapper is in the user's app's hot path. A failing or slow\n * Voight backend must NEVER turn into a failing or slow OpenAI\n * call for the user. `send` returns synchronously; the actual\n * POST happens on the microtask queue and any error reaches the\n * optional `onError` hook rather than the caller's stack.\n *\n * We intentionally do not retry, batch, or buffer in beta.1.\n * Those add state and complexity — we'd rather drop the\n * occasional event than ship a half-implemented queue. Retry +\n * buffer arrive in 0.2.0 once we have real-world failure-mode\n * data to design against.\n *\n * Why inject `fetch`:\n *\n * Lets unit tests assert request shape (URL, headers, body)\n * without going to the network and without monkey-patching\n * the global. In production the option is omitted and the\n * runtime's `fetch` (Node 18+) is used.\n */\n\nimport type { EventPayload } from './types.js'\n\nexport interface IngestOptions {\n apiBase: string\n apiKey: string\n /**\n * Optional override for the network call. Tests inject a mock;\n * production callers leave this unset and `globalThis.fetch` is\n * used at dispatch time.\n */\n fetch?: typeof fetch | undefined\n /**\n * Called when a network error or non-2xx response would otherwise\n * be silently dropped. Useful for surfacing misconfiguration\n * (bad key, wrong apiBase) during development. Defaults to a\n * no-op so production stays quiet.\n */\n onError?: ((err: unknown) => void) | undefined\n}\n\nexport interface IngestClient {\n send: (event: EventPayload) => void\n}\n\n/**\n * Build a client bound to a single apiBase + apiKey pair.\n *\n * The returned `send` is synchronous and never throws. Errors are\n * routed to `onError` (a no-op by default).\n */\nexport function createIngestClient(opts: IngestOptions): IngestClient {\n // Normalise the base URL once so callers can pass it with or\n // without a trailing slash and we don't end up with `//v1/events`.\n const base = opts.apiBase.replace(/\\/+$/, '')\n const url = `${base}/v1/events`\n const headers = {\n 'content-type': 'application/json',\n authorization: `Bearer ${opts.apiKey}`,\n }\n const fetchImpl = opts.fetch ?? globalThis.fetch\n const onError = opts.onError ?? (() => {})\n\n return {\n send(event) {\n // Wrap the dispatch in an IIFE so a synchronous throw inside\n // `JSON.stringify` (e.g. circular structure) still ends up\n // at `onError` instead of bubbling to the user's hot path.\n void (async () => {\n try {\n const body = JSON.stringify(event)\n const res = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n })\n if (!res.ok) {\n onError(\n new Error(\n `voight ingest failed: ${res.status} ${res.statusText}`,\n ),\n )\n }\n } catch (err) {\n onError(err)\n }\n })()\n },\n }\n}\n","/**\n * PII scrubbing utilities. Ports the regex catalogue proven in\n * @voightxyz/sdk's privacy.ts (12 patterns + Luhn-validated cards),\n * trimmed to the surface this package needs.\n *\n * `scrubPii` and `scrubAnyValue` are pure: same input produces same\n * output, no I/O, no randomness. Safe to call in a hot path.\n *\n * Why duplicate the catalogue here (rather than depend on the SDK)?\n *\n * v0.1.0-beta.1 ships standalone — no runtime dependency on\n * `@voightxyz/sdk` — so a user installing only `@voightxyz/openai`\n * gets a complete package. If duplication ever becomes painful\n * (a third copy under @voightxyz/anthropic, say), the right move\n * is to extract a private `@voightxyz/core` package; until then,\n * the cost of a 12-pattern table is lower than the cost of a\n * peer-dep + version-skew matrix.\n */\n\n// ─── PII patterns ──────────────────────────────────────────────────\n//\n// Order matters. Multi-line / most-specific patterns run FIRST so\n// that once a span is consumed (e.g. a PEM block), later patterns\n// can't re-match its insides. JWTs run before generic API-key\n// patterns to avoid partial matches.\n//\n// Each pattern has a `name` for unit-test traceability and a `re`\n// that uses the global flag (so `replaceAll` semantics apply).\n\ntype Pattern = {\n name: string\n re: RegExp\n replacement: string\n}\n\nconst RE_PEM_PRIVATE_KEY =\n /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g\n\nconst RE_JWT =\n /\\beyJ[A-Za-z0-9_-]{10,}\\.eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g\n\n// Anthropic must precede the generic OpenAI rule so `sk-ant-...`\n// doesn't get partially consumed as `sk-...`.\nconst RE_ANTHROPIC = /\\bsk-ant-[A-Za-z0-9_-]{40,}\\b/g\n\nconst RE_OPENAI = /\\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\\b/g\n\nconst RE_STRIPE_LIVE = /\\b(?:sk|pk)_live_[A-Za-z0-9]{20,}\\b/g\n\nconst RE_GITHUB_FINE = /\\bgithub_pat_[A-Za-z0-9_]{20,}\\b/g\nconst RE_GITHUB_CLASSIC = /\\bghp_[A-Za-z0-9]{36}\\b/g\n\nconst RE_AWS_ACCESS_KEY = /\\bAKIA[A-Z0-9]{16}\\b/g\n\nconst RE_SLACK = /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g\n\n// Voight's own keys. Defense-in-depth: even if the user accidentally\n// pastes their `vk_…` into a prompt, it never leaves the process in\n// the clear under standard privacy.\nconst RE_VOIGHT = /\\bvk_[A-Za-z0-9_-]{32,}\\b/g\n\n// Strict email: requires a TLD with ≥2 letters. `support@app`\n// (no TLD) and `email_template` (no @) do not match.\nconst RE_EMAIL = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g\n\n// Phone in E.164. Looser variants produce too many false positives\n// over order numbers and identifiers.\nconst RE_PHONE_E164 = /\\+\\d{10,15}\\b/g\n\nconst KEY_PATTERNS: readonly Pattern[] = [\n { name: 'pem-private-key', re: RE_PEM_PRIVATE_KEY, replacement: '[REDACTED-PRIVATE-KEY]' },\n { name: 'jwt', re: RE_JWT, replacement: '[REDACTED-JWT]' },\n { name: 'anthropic-key', re: RE_ANTHROPIC, replacement: '[REDACTED-API-KEY]' },\n { name: 'openai-key', re: RE_OPENAI, replacement: '[REDACTED-API-KEY]' },\n { name: 'stripe-live-key', re: RE_STRIPE_LIVE, replacement: '[REDACTED-API-KEY]' },\n { name: 'github-fine-pat', re: RE_GITHUB_FINE, replacement: '[REDACTED-API-KEY]' },\n { name: 'github-classic-pat', re: RE_GITHUB_CLASSIC, replacement: '[REDACTED-API-KEY]' },\n { name: 'aws-access-key', re: RE_AWS_ACCESS_KEY, replacement: '[REDACTED-API-KEY]' },\n { name: 'slack-token', re: RE_SLACK, replacement: '[REDACTED-API-KEY]' },\n { name: 'voight-key', re: RE_VOIGHT, replacement: '[REDACTED-API-KEY]' },\n { name: 'email', re: RE_EMAIL, replacement: '[REDACTED-EMAIL]' },\n { name: 'phone-e164', re: RE_PHONE_E164, replacement: '[REDACTED-PHONE]' },\n]\n\n// ─── Credit cards ─────────────────────────────────────────────────\n//\n// Credit-card numbers need Luhn validation to avoid false positives\n// over long numeric ID strings, so they don't fit the simple\n// {re, replacement} table.\n\n/**\n * Validate a digit string against the Luhn checksum used by all\n * major card brands. Returns false on empty / non-digit input.\n */\nexport function luhnValid(digits: string): boolean {\n if (digits.length === 0) return false\n let sum = 0\n let alternate = false\n for (let i = digits.length - 1; i >= 0; i--) {\n const ch = digits.charCodeAt(i)\n if (ch < 48 || ch > 57) return false\n let n = ch - 48\n if (alternate) {\n n *= 2\n if (n > 9) n -= 9\n }\n sum += n\n alternate = !alternate\n }\n return sum > 0 && sum % 10 === 0\n}\n\n// 13–19 digits, optionally with single spaces or dashes between\n// groups. Anchored with \\b so we don't match the middle of a longer\n// digit string.\nconst RE_CARD_CANDIDATE = /\\b(?:\\d[ -]?){12,18}\\d\\b/g\n\nfunction scrubCreditCards(input: string): string {\n return input.replace(RE_CARD_CANDIDATE, (match) => {\n const digits = match.replace(/[ -]/g, '')\n if (digits.length < 13 || digits.length > 19) return match\n if (!luhnValid(digits)) return match\n return '[REDACTED-CARD]'\n })\n}\n\n/**\n * Run the credential / PII patterns over a string. Pure, idempotent\n * (already-scrubbed text stays stable), no I/O. Non-string input\n * is returned unchanged so this function is safe to call inside\n * the recursive walker.\n */\nexport function scrubPii(input: string): string {\n // The non-string branch matters: `scrubAnyValue` calls this on\n // unknown values, and we'd rather return the raw value than crash.\n if (typeof input !== 'string' || input.length === 0) return input\n let out = input\n for (const { re, replacement } of KEY_PATTERNS) {\n // Pin a fresh RegExp instance per call: `g`-flagged regexes\n // carry mutable `lastIndex` state, and although `replace` does\n // not depend on it, future Worker-based callers might.\n out = out.replace(new RegExp(re.source, re.flags), replacement)\n }\n out = scrubCreditCards(out)\n return out\n}\n\n/**\n * Recursively scrub every string leaf in a JSON-like value.\n * Non-string primitives, undefined, arrays, and plain objects are\n * walked structurally. Anything else (Date, Map, Set, etc.) is\n * returned unchanged — this package never serialises those.\n *\n * Returns a fresh value; the input is never mutated.\n */\nexport function scrubAnyValue(value: unknown): unknown {\n if (typeof value === 'string') return scrubPii(value)\n if (Array.isArray(value)) return value.map(scrubAnyValue)\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = scrubAnyValue(v)\n }\n return out\n }\n return value\n}\n","/**\n * Instrument for `client.messages.create`.\n *\n * Wraps the Anthropic SDK's primary Messages endpoint. Handles both\n * transports:\n *\n * - Non-streaming: pulls text from `content[].text` blocks, tool\n * calls from `content[].tool_use` blocks (input is unknown JSON,\n * we serialise via JSON.stringify so the wire format matches the\n * openai wrapper's `arguments` string).\n *\n * - Streaming: a state machine over the event sequence\n * `message_start` → `content_block_start` → many\n * `content_block_delta` → `content_block_stop` → `message_delta`\n * → `message_stop`. We maintain a per-block aggregator keyed by\n * `index`. Text deltas concatenate `delta.text`. Tool-use deltas\n * carry `delta.partial_json` fragments that we append to that\n * block's `arguments` string. The final `message_delta` carries\n * the output_tokens; the initial `message_start` carries the\n * input_tokens + the two cache fields.\n *\n * Token surface emitted on `metadata.tokens`:\n * - input, output, total: always\n * - cache_read: only when `cache_read_input_tokens > 0`\n * - cache_creation: only when `cache_creation_input_tokens > 0`\n *\n * Privacy fan-out: identical contract to @voightxyz/openai\n * (`minimal` → tags only, `standard` → scrubbed content,\n * `full` → verbatim). The first tool call's name flows into\n * `toolExecuted` at every level so the audit-log DETAIL column\n * keeps rendering meaningfully even under minimal.\n */\n\nimport type { EventPayload, PrivacyLevel } from '../types.js'\nimport { scrubAnyValue, scrubPii } from '../privacy.js'\n\n// ─── Loose Anthropic types ────────────────────────────────────────\n//\n// We model only the surface this instrument actually reads. Keeping\n// the types loose insulates us from upstream SDK changes that don't\n// affect our wire shape.\n\ninterface MessageCreateParams {\n model: string\n max_tokens: number\n messages: Array<Record<string, unknown>>\n stream?: boolean\n [k: string]: unknown\n}\n\ninterface AnthropicUsage {\n input_tokens?: number\n output_tokens?: number\n cache_creation_input_tokens?: number | null\n cache_read_input_tokens?: number | null\n [k: string]: unknown\n}\n\ninterface TextContentBlock {\n type: 'text'\n text: string\n}\n\ninterface ToolUseContentBlock {\n type: 'tool_use'\n id: string\n name: string\n input: unknown\n}\n\ntype ContentBlock =\n | TextContentBlock\n | ToolUseContentBlock\n | { type: string; [k: string]: unknown }\n\ninterface NonStreamingMessage {\n id?: string\n model?: string\n content?: ContentBlock[]\n stop_reason?: string | null\n usage?: AnthropicUsage\n [k: string]: unknown\n}\n\n// Streaming events\ninterface StreamMessageStart {\n type: 'message_start'\n message: NonStreamingMessage\n}\ninterface StreamContentBlockStart {\n type: 'content_block_start'\n index: number\n content_block: ContentBlock\n}\ninterface StreamContentBlockDelta {\n type: 'content_block_delta'\n index: number\n delta:\n | { type: 'text_delta'; text: string }\n | { type: 'input_json_delta'; partial_json: string }\n | { type: string; [k: string]: unknown }\n}\ninterface StreamContentBlockStop {\n type: 'content_block_stop'\n index: number\n}\ninterface StreamMessageDelta {\n type: 'message_delta'\n delta: { stop_reason?: string | null; [k: string]: unknown }\n usage?: { output_tokens?: number; [k: string]: unknown }\n}\ninterface StreamMessageStop {\n type: 'message_stop'\n}\n\ntype StreamEvent =\n | StreamMessageStart\n | StreamContentBlockStart\n | StreamContentBlockDelta\n | StreamContentBlockStop\n | StreamMessageDelta\n | StreamMessageStop\n | { type: string; [k: string]: unknown }\n\ntype CreateFn = (\n params: MessageCreateParams,\n) => Promise<NonStreamingMessage | AsyncIterable<StreamEvent>>\n\n/**\n * Flat tool-call shape we emit. Mirrors the openai wrapper's\n * `metadata.toolCalls[*]` schema so dashboard rendering doesn't\n * need to know which provider produced the event.\n */\ninterface CapturedToolCall {\n id: string\n name: string\n arguments: string\n}\n\ninterface NormalisedTokens {\n input: number\n output: number\n total: number\n cache_read?: number\n cache_creation?: number\n}\n\nexport interface EventSink {\n send: (event: EventPayload) => void\n}\n\nexport interface InstrumentContext {\n agentId: string\n privacy: PrivacyLevel\n ingest: EventSink\n /** Time source in ms; injected so tests can produce deterministic `durationMs`. */\n now: () => number\n}\n\nexport function instrumentMessages(\n original: CreateFn,\n ctx: InstrumentContext,\n): CreateFn {\n return async function wrappedCreate(params: MessageCreateParams) {\n const startedAt = ctx.now()\n const isStream = params.stream === true\n\n let result: NonStreamingMessage | AsyncIterable<StreamEvent>\n try {\n result = await original(params)\n } catch (err) {\n ctx.ingest.send(\n buildFailureEvent({ ctx, params, startedAt, error: err }),\n )\n throw err\n }\n\n if (!isStream) {\n ctx.ingest.send(\n buildSuccessEvent({\n ctx,\n params,\n startedAt,\n response: result as NonStreamingMessage,\n }),\n )\n return result\n }\n\n return wrapStream(\n result as AsyncIterable<StreamEvent>,\n ctx,\n params,\n startedAt,\n )\n }\n}\n\n// ─── Event builders ──────────────────────────────────────────────\n\nfunction buildSuccessEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n response: NonStreamingMessage\n}): EventPayload {\n const { ctx, params, startedAt, response } = args\n const responseText = extractText(response.content ?? [])\n const toolCalls = extractToolCalls(response.content ?? [])\n const tokens = normaliseTokens(response.usage)\n const durationMs = ctx.now() - startedAt\n\n return assembleEvent({\n ctx,\n params,\n durationMs,\n outcome: 'success',\n responseText: responseText.length > 0 ? responseText : undefined,\n tokens,\n toolCalls,\n streaming: false,\n finishReason: response.stop_reason ?? null,\n modelFromResponse: response.model,\n })\n}\n\nfunction buildFailureEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n error: unknown\n}): EventPayload {\n const { ctx, params, startedAt, error } = args\n const durationMs = ctx.now() - startedAt\n const message = error instanceof Error ? error.message : String(error)\n return assembleEvent({\n ctx,\n params,\n durationMs,\n outcome: 'failed',\n streaming: params.stream === true,\n errorMessage: message,\n })\n}\n\nfunction buildStreamEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n aggregatedText: string\n tokens: NormalisedTokens | null\n toolCalls: CapturedToolCall[] | null\n modelFromResponse: string | undefined\n finishReason: string | null\n}): EventPayload {\n return assembleEvent({\n ctx: args.ctx,\n params: args.params,\n durationMs: args.ctx.now() - args.startedAt,\n outcome: 'success',\n responseText:\n args.aggregatedText.length > 0 ? args.aggregatedText : undefined,\n tokens: args.tokens,\n toolCalls: args.toolCalls,\n streaming: true,\n finishReason: args.finishReason,\n modelFromResponse: args.modelFromResponse,\n })\n}\n\n/**\n * Single assembler — privacy fan-out + payload shape in one place\n * so the three callers above can't drift apart.\n */\nfunction assembleEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n durationMs: number\n outcome: 'success' | 'failed'\n responseText?: string | undefined\n tokens?: NormalisedTokens | null\n toolCalls?: CapturedToolCall[] | null\n streaming: boolean\n finishReason?: string | null\n errorMessage?: string\n modelFromResponse?: string | undefined\n}): EventPayload {\n const { ctx, params, durationMs, outcome, streaming, errorMessage } = args\n const tokens = args.tokens ?? null\n const toolCalls = args.toolCalls ?? null\n const model = args.modelFromResponse ?? params.model\n\n const metadata: Record<string, unknown> = {\n source: 'anthropic-sdk',\n privacyLevel: ctx.privacy,\n streaming,\n }\n if (tokens) metadata.tokens = tokens\n if (args.finishReason !== undefined && args.finishReason !== null) {\n metadata.finishReason = args.finishReason\n }\n\n const firstToolName =\n toolCalls && toolCalls.length > 0 ? toolCalls[0]!.name : undefined\n\n if (ctx.privacy === 'minimal') {\n return {\n agentId: ctx.agentId,\n type: 'reasoning',\n model,\n durationMs,\n outcome,\n ...(firstToolName ? { toolExecuted: firstToolName } : {}),\n metadata,\n ...(errorMessage ? { errorMessage } : {}),\n }\n }\n\n const messages =\n ctx.privacy === 'standard'\n ? (scrubAnyValue(params.messages) as MessageCreateParams['messages'])\n : params.messages\n\n const responseText = args.responseText\n const scrubbedResponse =\n responseText !== undefined\n ? ctx.privacy === 'standard'\n ? scrubPii(responseText)\n : responseText\n : undefined\n if (scrubbedResponse !== undefined) {\n metadata.responseText = scrubbedResponse\n }\n\n if (toolCalls && toolCalls.length > 0) {\n metadata.toolCalls =\n ctx.privacy === 'standard'\n ? toolCalls.map((t) => ({\n id: t.id,\n name: t.name,\n arguments: scrubPii(t.arguments),\n }))\n : toolCalls\n }\n\n return {\n agentId: ctx.agentId,\n type: 'reasoning',\n model,\n durationMs,\n outcome,\n ...(firstToolName ? { toolExecuted: firstToolName } : {}),\n input: { messages },\n metadata,\n ...(errorMessage ? { errorMessage } : {}),\n }\n}\n\n// ─── Non-streaming helpers ──────────────────────────────────────\n\nfunction extractText(content: ContentBlock[]): string {\n let out = ''\n for (const block of content) {\n if (block.type === 'text' && typeof (block as TextContentBlock).text === 'string') {\n out += (block as TextContentBlock).text\n }\n }\n return out\n}\n\nfunction extractToolCalls(content: ContentBlock[]): CapturedToolCall[] | null {\n const out: CapturedToolCall[] = []\n for (const block of content) {\n if (block.type !== 'tool_use') continue\n const t = block as ToolUseContentBlock\n out.push({\n id: typeof t.id === 'string' ? t.id : '',\n name: typeof t.name === 'string' ? t.name : '',\n // Serialise the model's chosen tool input to a JSON string so\n // the wire shape matches openai's `arguments: string` exactly.\n // Anthropic gives us a parsed object; openai gives us the raw\n // string. We normalise here.\n arguments: safeStringify(t.input),\n })\n }\n return out.length > 0 ? out : null\n}\n\nfunction safeStringify(v: unknown): string {\n if (typeof v === 'string') return v\n try {\n return JSON.stringify(v ?? {})\n } catch {\n return ''\n }\n}\n\nfunction normaliseTokens(u: AnthropicUsage | undefined): NormalisedTokens | null {\n if (!u) return null\n const input = numberOrZero(u.input_tokens)\n const output = numberOrZero(u.output_tokens)\n const total = input + output\n const cacheRead = numberOrZero(u.cache_read_input_tokens)\n const cacheCreation = numberOrZero(u.cache_creation_input_tokens)\n const base: NormalisedTokens = { input, output, total }\n if (cacheRead > 0) base.cache_read = cacheRead\n if (cacheCreation > 0) base.cache_creation = cacheCreation\n return base\n}\n\nfunction numberOrZero(v: unknown): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : 0\n}\n\n// ─── Streaming wrapper ──────────────────────────────────────────\n\n/**\n * Mutating accumulator for the streaming state machine. We hold a\n * single source of truth across chunks: text aggregator, per-index\n * tool-call entries, latest usage seen, final stop_reason. The wrap\n * function yields each event to the user untouched and only mutates\n * this struct as bytes pass through.\n */\ninterface StreamState {\n aggregatedText: string\n toolBlocks: Map<number, CapturedToolCall>\n usage: AnthropicUsage | null\n modelFromResponse: string | undefined\n finishReason: string | null\n}\n\nfunction wrapStream(\n source: AsyncIterable<StreamEvent>,\n ctx: InstrumentContext,\n params: MessageCreateParams,\n startedAt: number,\n): AsyncIterable<StreamEvent> {\n const state: StreamState = {\n aggregatedText: '',\n toolBlocks: new Map(),\n usage: null,\n modelFromResponse: undefined,\n finishReason: null,\n }\n let emitted = false\n\n function emit() {\n if (emitted) return\n emitted = true\n ctx.ingest.send(\n buildStreamEvent({\n ctx,\n params,\n startedAt,\n aggregatedText: state.aggregatedText,\n tokens: normaliseTokens(state.usage ?? undefined),\n toolCalls: snapshotTools(state.toolBlocks),\n modelFromResponse: state.modelFromResponse,\n finishReason: state.finishReason,\n }),\n )\n }\n\n return {\n async *[Symbol.asyncIterator]() {\n try {\n for await (const ev of source) {\n applyEvent(state, ev)\n yield ev\n }\n } catch (err) {\n ctx.ingest.send(\n buildFailureEvent({ ctx, params, startedAt, error: err }),\n )\n emitted = true\n throw err\n } finally {\n emit()\n }\n },\n }\n}\n\n/**\n * Step the streaming state machine one event forward. Pure on the\n * `state` argument (mutates it in place). Unknown event types pass\n * through silently — Anthropic adds new event types over time and\n * we don't want a new event class to break capture.\n */\nfunction applyEvent(state: StreamState, ev: StreamEvent): void {\n switch (ev.type) {\n case 'message_start': {\n const e = ev as StreamMessageStart\n if (e.message?.model && !state.modelFromResponse) {\n state.modelFromResponse = e.message.model\n }\n if (e.message?.usage) {\n // message_start carries input_tokens + cache fields. We\n // seed `usage` here so the final tokens object includes\n // cache numbers even if message_delta only overwrites\n // output_tokens.\n state.usage = { ...(state.usage ?? {}), ...e.message.usage }\n }\n return\n }\n case 'content_block_start': {\n const e = ev as StreamContentBlockStart\n if (e.content_block?.type === 'tool_use') {\n const tu = e.content_block as ToolUseContentBlock\n state.toolBlocks.set(e.index, {\n id: typeof tu.id === 'string' ? tu.id : '',\n name: typeof tu.name === 'string' ? tu.name : '',\n arguments: '',\n })\n }\n return\n }\n case 'content_block_delta': {\n const e = ev as StreamContentBlockDelta\n const d = e.delta as { type: string; [k: string]: unknown }\n if (d.type === 'text_delta' && typeof d.text === 'string') {\n state.aggregatedText += d.text\n } else if (\n d.type === 'input_json_delta' &&\n typeof d.partial_json === 'string'\n ) {\n const entry = state.toolBlocks.get(e.index)\n if (entry) entry.arguments += d.partial_json\n }\n return\n }\n case 'message_delta': {\n const e = ev as StreamMessageDelta\n if (e.delta?.stop_reason && state.finishReason === null) {\n state.finishReason = e.delta.stop_reason\n }\n if (e.usage) {\n // message_delta only carries the final output_tokens. Merge,\n // don't replace, so we keep input_tokens + cache fields from\n // message_start.\n state.usage = { ...(state.usage ?? {}), ...e.usage }\n }\n return\n }\n // content_block_stop, message_stop, and unknown future event\n // types pass through without state updates.\n }\n}\n\nfunction snapshotTools(\n acc: Map<number, CapturedToolCall>,\n): CapturedToolCall[] | null {\n if (acc.size === 0) return null\n const entries = [...acc.entries()].sort(([a], [b]) => a - b)\n const out = entries.map(([, v]) => v).filter((t) => t.name.length > 0)\n return out.length > 0 ? out : null\n}\n","/**\n * `wrapAnthropic` — the public entrypoint of @voightxyz/anthropic.\n *\n * Layered `Proxy`: level 0 intercepts the `messages` property,\n * level 1 intercepts the `create` function on it. Everything outside\n * the `client.messages.create` path passes through untouched via\n * `Reflect.get`, so the legacy `completions` namespace, the model\n * list endpoints, batch endpoints, and any future SDK additions\n * keep working with zero special-casing.\n *\n * The proxy is one level shallower than the openai port because\n * Anthropic exposes `messages` at the top level (no intermediate\n * `chat` namespace).\n *\n * Failure modes are intentionally non-fatal — same contract as\n * @voightxyz/openai:\n *\n * - `enabled: false` → return the original client.\n * - no API key resolves → log a one-line warning and return\n * the original client.\n *\n * Internal `_fetch` and `_env` options exist so tests can drive\n * the network + environment surface without touching globals.\n */\n\nimport type { WrapOptions } from './types.js'\nimport { resolveApiKey, resolveAgent } from './identity.js'\nimport { createIngestClient } from './ingest.js'\nimport {\n instrumentMessages,\n type InstrumentContext,\n} from './instruments/messages.js'\n\ninterface InternalOptions extends WrapOptions {\n _fetch?: typeof fetch\n _env?: Record<string, string | undefined>\n}\n\nconst DEFAULT_API_BASE = 'https://api.voight.xyz'\n\nexport function wrapAnthropic<T extends object>(\n client: T,\n options: WrapOptions = {},\n): T {\n const opts = options as InternalOptions\n\n if (opts.enabled === false) return client\n\n const env = opts._env ?? process.env\n const apiKey = resolveApiKey(\n { voightApiKey: opts.voightApiKey, agent: opts.agent },\n env,\n )\n\n if (apiKey === null) {\n console.warn(\n '[voight] no VOIGHT_KEY resolved — wrapper is a pass-through. ' +\n 'Set process.env.VOIGHT_KEY or pass `voightApiKey` to wrapAnthropic() to enable capture.',\n )\n return client\n }\n\n const agentId = resolveAgent(\n { voightApiKey: opts.voightApiKey, agent: opts.agent },\n env,\n )\n\n const ingest = createIngestClient({\n apiBase: opts.apiBase ?? DEFAULT_API_BASE,\n apiKey,\n fetch: opts._fetch,\n })\n\n const ctx: InstrumentContext = {\n agentId,\n privacy: opts.privacy ?? 'standard',\n ingest,\n now: () => Date.now(),\n }\n\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === 'messages') {\n const messages = Reflect.get(target, prop, receiver)\n return wrapMessages(messages as object, ctx)\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n\nfunction wrapMessages<M extends object>(\n messages: M,\n ctx: InstrumentContext,\n): M {\n return new Proxy(messages, {\n get(target, prop, receiver) {\n if (prop === 'create') {\n const original = Reflect.get(target, prop, receiver) as (\n params: never,\n ) => Promise<unknown>\n // .bind so `this` inside the SDK's `create` stays the real\n // messages instance, not the proxy. Without this the\n // Anthropic SDK loses access to its internal http client.\n return instrumentMessages(\n original.bind(target) as never,\n ctx,\n )\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/wrap.ts","../src/identity.ts","../src/ingest.ts","../src/privacy.ts","../src/instruments/messages.ts"],"sourcesContent":["// Public surface of @voightxyz/anthropic.\n//\n// The package's contract is intentionally tiny: one function that\n// takes an Anthropic client and returns a wrapped client with the\n// same shape. Everything else (ingest transport, privacy redaction,\n// identity resolution) is an implementation detail and not part of\n// the public API.\n\nexport { wrapAnthropic } from './wrap.js'\nexport type { WrapOptions, PrivacyLevel } from './types.js'\n","/**\n * `wrapAnthropic` — the public entrypoint of @voightxyz/anthropic.\n *\n * Layered `Proxy`: level 0 intercepts the `messages` property,\n * level 1 intercepts the `create` function on it. Everything outside\n * the `client.messages.create` path passes through untouched via\n * `Reflect.get`, so the legacy `completions` namespace, the model\n * list endpoints, batch endpoints, and any future SDK additions\n * keep working with zero special-casing.\n *\n * The proxy is one level shallower than the openai port because\n * Anthropic exposes `messages` at the top level (no intermediate\n * `chat` namespace).\n *\n * Failure modes are intentionally non-fatal — same contract as\n * @voightxyz/openai:\n *\n * - `enabled: false` → return the original client.\n * - no API key resolves → log a one-line warning and return\n * the original client.\n *\n * Internal `_fetch` and `_env` options exist so tests can drive\n * the network + environment surface without touching globals.\n */\n\nimport { randomUUID } from 'node:crypto'\n\nimport type { WrapOptions } from './types.js'\nimport { resolveApiKey, resolveAgent } from './identity.js'\nimport { createIngestClient } from './ingest.js'\nimport {\n instrumentMessages,\n type InstrumentContext,\n} from './instruments/messages.js'\n\ninterface InternalOptions extends WrapOptions {\n _fetch?: typeof fetch\n _env?: Record<string, string | undefined>\n}\n\nconst DEFAULT_API_BASE = 'https://api.voight.xyz'\n\nexport function wrapAnthropic<T extends object>(\n client: T,\n options: WrapOptions = {},\n): T {\n const opts = options as InternalOptions\n\n if (opts.enabled === false) return client\n\n const env = opts._env ?? process.env\n const apiKey = resolveApiKey(\n { voightApiKey: opts.voightApiKey, agent: opts.agent },\n env,\n )\n\n if (apiKey === null) {\n console.warn(\n '[voight] no VOIGHT_KEY resolved — wrapper is a pass-through. ' +\n 'Set process.env.VOIGHT_KEY or pass `voightApiKey` to wrapAnthropic() to enable capture.',\n )\n return client\n }\n\n const agentId = resolveAgent(\n { voightApiKey: opts.voightApiKey, agent: opts.agent },\n env,\n )\n\n const ingest = createIngestClient({\n apiBase: opts.apiBase ?? DEFAULT_API_BASE,\n apiKey,\n fetch: opts._fetch,\n })\n\n // sessionId is generated once per wrapper instance. Explicit\n // override wins so callers can scope by user / conversation /\n // request without us second-guessing them.\n const sessionId =\n typeof opts.sessionId === 'string' && opts.sessionId.trim().length > 0\n ? opts.sessionId.trim()\n : randomUUID()\n\n const ctx: InstrumentContext = {\n agentId,\n privacy: opts.privacy ?? 'standard',\n sessionId,\n ingest,\n now: () => Date.now(),\n }\n\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === 'messages') {\n const messages = Reflect.get(target, prop, receiver)\n return wrapMessages(messages as object, ctx)\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n\nfunction wrapMessages<M extends object>(\n messages: M,\n ctx: InstrumentContext,\n): M {\n return new Proxy(messages, {\n get(target, prop, receiver) {\n if (prop === 'create') {\n const original = Reflect.get(target, prop, receiver) as (\n params: never,\n ) => Promise<unknown>\n // .bind so `this` inside the SDK's `create` stays the real\n // messages instance, not the proxy. Without this the\n // Anthropic SDK loses access to its internal http client.\n return instrumentMessages(\n original.bind(target) as never,\n ctx,\n )\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n","/**\n * Identity resolution: API key + agent label.\n *\n * Both helpers are pure — they take the user's options and an `env`\n * record, and return a result. `env` defaults to `process.env` when\n * called bare; passing it explicitly lets tests exercise every\n * fallback path without mutating global state.\n *\n * Resolution priority is fixed and documented at each call site:\n *\n * resolveApiKey: options.voightApiKey → env.VOIGHT_KEY → null\n * resolveAgent: options.agent → env.VOIGHT_AGENT\n * → env.HOSTNAME → 'unknown-agent'\n *\n * Empty and whitespace-only values are treated as missing so a\n * misconfigured env (`VOIGHT_KEY=`) falls through cleanly instead\n * of being mistaken for a real key.\n */\n\nexport interface IdentityOptions {\n voightApiKey?: string | undefined\n agent?: string | undefined\n}\n\ntype Env = Record<string, string | undefined>\n\n/**\n * Trim and return the value, or `null` if it's missing / blank.\n * Centralises the \"empty counts as missing\" rule that both helpers\n * apply at every layer.\n */\nfunction nonBlank(value: string | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length === 0 ? null : trimmed\n}\n\n/**\n * Resolve the Voight API key for outgoing events.\n *\n * Returns `null` when nothing usable is configured. Callers decide\n * how to react — `wrapOpenAI` logs a one-time warning and falls\n * back to a no-op transport, so a missing key never crashes the\n * host app.\n */\nexport function resolveApiKey(\n options: IdentityOptions = {},\n env: Env = process.env,\n): string | null {\n return nonBlank(options.voightApiKey) ?? nonBlank(env.VOIGHT_KEY)\n}\n\n/**\n * Resolve the agent label that groups events in the dashboard.\n *\n * Always returns a non-empty string: when nothing else resolves we\n * emit `'unknown-agent'` so the event is still ingestable and the\n * misconfiguration shows up on screen instead of being silently\n * dropped.\n */\nexport function resolveAgent(\n options: IdentityOptions = {},\n env: Env = process.env,\n): string {\n return (\n nonBlank(options.agent) ??\n nonBlank(env.VOIGHT_AGENT) ??\n nonBlank(env.HOSTNAME) ??\n 'unknown-agent'\n )\n}\n","/**\n * Fire-and-forget HTTP client for the Voight ingest endpoint.\n *\n * Why \"fire and forget\":\n *\n * The wrapper is in the user's app's hot path. A failing or slow\n * Voight backend must NEVER turn into a failing or slow OpenAI\n * call for the user. `send` returns synchronously; the actual\n * POST happens on the microtask queue and any error reaches the\n * optional `onError` hook rather than the caller's stack.\n *\n * We intentionally do not retry, batch, or buffer in beta.1.\n * Those add state and complexity — we'd rather drop the\n * occasional event than ship a half-implemented queue. Retry +\n * buffer arrive in 0.2.0 once we have real-world failure-mode\n * data to design against.\n *\n * Why inject `fetch`:\n *\n * Lets unit tests assert request shape (URL, headers, body)\n * without going to the network and without monkey-patching\n * the global. In production the option is omitted and the\n * runtime's `fetch` (Node 18+) is used.\n */\n\nimport type { EventPayload } from './types.js'\n\nexport interface IngestOptions {\n apiBase: string\n apiKey: string\n /**\n * Optional override for the network call. Tests inject a mock;\n * production callers leave this unset and `globalThis.fetch` is\n * used at dispatch time.\n */\n fetch?: typeof fetch | undefined\n /**\n * Called when a network error or non-2xx response would otherwise\n * be silently dropped. Useful for surfacing misconfiguration\n * (bad key, wrong apiBase) during development. Defaults to a\n * no-op so production stays quiet.\n */\n onError?: ((err: unknown) => void) | undefined\n}\n\nexport interface IngestClient {\n send: (event: EventPayload) => void\n}\n\n/**\n * Build a client bound to a single apiBase + apiKey pair.\n *\n * The returned `send` is synchronous and never throws. Errors are\n * routed to `onError` (a no-op by default).\n */\nexport function createIngestClient(opts: IngestOptions): IngestClient {\n // Normalise the base URL once so callers can pass it with or\n // without a trailing slash and we don't end up with `//v1/events`.\n const base = opts.apiBase.replace(/\\/+$/, '')\n const url = `${base}/v1/events`\n const headers = {\n 'content-type': 'application/json',\n authorization: `Bearer ${opts.apiKey}`,\n }\n const fetchImpl = opts.fetch ?? globalThis.fetch\n const onError = opts.onError ?? (() => {})\n\n return {\n send(event) {\n // Wrap the dispatch in an IIFE so a synchronous throw inside\n // `JSON.stringify` (e.g. circular structure) still ends up\n // at `onError` instead of bubbling to the user's hot path.\n void (async () => {\n try {\n const body = JSON.stringify(event)\n const res = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n })\n if (!res.ok) {\n onError(\n new Error(\n `voight ingest failed: ${res.status} ${res.statusText}`,\n ),\n )\n }\n } catch (err) {\n onError(err)\n }\n })()\n },\n }\n}\n","/**\n * PII scrubbing utilities. Ports the regex catalogue proven in\n * @voightxyz/sdk's privacy.ts (12 patterns + Luhn-validated cards),\n * trimmed to the surface this package needs.\n *\n * `scrubPii` and `scrubAnyValue` are pure: same input produces same\n * output, no I/O, no randomness. Safe to call in a hot path.\n *\n * Why duplicate the catalogue here (rather than depend on the SDK)?\n *\n * v0.1.0-beta.1 ships standalone — no runtime dependency on\n * `@voightxyz/sdk` — so a user installing only `@voightxyz/openai`\n * gets a complete package. If duplication ever becomes painful\n * (a third copy under @voightxyz/anthropic, say), the right move\n * is to extract a private `@voightxyz/core` package; until then,\n * the cost of a 12-pattern table is lower than the cost of a\n * peer-dep + version-skew matrix.\n */\n\n// ─── PII patterns ──────────────────────────────────────────────────\n//\n// Order matters. Multi-line / most-specific patterns run FIRST so\n// that once a span is consumed (e.g. a PEM block), later patterns\n// can't re-match its insides. JWTs run before generic API-key\n// patterns to avoid partial matches.\n//\n// Each pattern has a `name` for unit-test traceability and a `re`\n// that uses the global flag (so `replaceAll` semantics apply).\n\ntype Pattern = {\n name: string\n re: RegExp\n replacement: string\n}\n\nconst RE_PEM_PRIVATE_KEY =\n /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g\n\nconst RE_JWT =\n /\\beyJ[A-Za-z0-9_-]{10,}\\.eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g\n\n// Anthropic must precede the generic OpenAI rule so `sk-ant-...`\n// doesn't get partially consumed as `sk-...`.\nconst RE_ANTHROPIC = /\\bsk-ant-[A-Za-z0-9_-]{40,}\\b/g\n\nconst RE_OPENAI = /\\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\\b/g\n\nconst RE_STRIPE_LIVE = /\\b(?:sk|pk)_live_[A-Za-z0-9]{20,}\\b/g\n\nconst RE_GITHUB_FINE = /\\bgithub_pat_[A-Za-z0-9_]{20,}\\b/g\nconst RE_GITHUB_CLASSIC = /\\bghp_[A-Za-z0-9]{36}\\b/g\n\nconst RE_AWS_ACCESS_KEY = /\\bAKIA[A-Z0-9]{16}\\b/g\n\nconst RE_SLACK = /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g\n\n// Voight's own keys. Defense-in-depth: even if the user accidentally\n// pastes their `vk_…` into a prompt, it never leaves the process in\n// the clear under standard privacy.\nconst RE_VOIGHT = /\\bvk_[A-Za-z0-9_-]{32,}\\b/g\n\n// Strict email: requires a TLD with ≥2 letters. `support@app`\n// (no TLD) and `email_template` (no @) do not match.\nconst RE_EMAIL = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g\n\n// Phone in E.164. Looser variants produce too many false positives\n// over order numbers and identifiers.\nconst RE_PHONE_E164 = /\\+\\d{10,15}\\b/g\n\nconst KEY_PATTERNS: readonly Pattern[] = [\n { name: 'pem-private-key', re: RE_PEM_PRIVATE_KEY, replacement: '[REDACTED-PRIVATE-KEY]' },\n { name: 'jwt', re: RE_JWT, replacement: '[REDACTED-JWT]' },\n { name: 'anthropic-key', re: RE_ANTHROPIC, replacement: '[REDACTED-API-KEY]' },\n { name: 'openai-key', re: RE_OPENAI, replacement: '[REDACTED-API-KEY]' },\n { name: 'stripe-live-key', re: RE_STRIPE_LIVE, replacement: '[REDACTED-API-KEY]' },\n { name: 'github-fine-pat', re: RE_GITHUB_FINE, replacement: '[REDACTED-API-KEY]' },\n { name: 'github-classic-pat', re: RE_GITHUB_CLASSIC, replacement: '[REDACTED-API-KEY]' },\n { name: 'aws-access-key', re: RE_AWS_ACCESS_KEY, replacement: '[REDACTED-API-KEY]' },\n { name: 'slack-token', re: RE_SLACK, replacement: '[REDACTED-API-KEY]' },\n { name: 'voight-key', re: RE_VOIGHT, replacement: '[REDACTED-API-KEY]' },\n { name: 'email', re: RE_EMAIL, replacement: '[REDACTED-EMAIL]' },\n { name: 'phone-e164', re: RE_PHONE_E164, replacement: '[REDACTED-PHONE]' },\n]\n\n// ─── Credit cards ─────────────────────────────────────────────────\n//\n// Credit-card numbers need Luhn validation to avoid false positives\n// over long numeric ID strings, so they don't fit the simple\n// {re, replacement} table.\n\n/**\n * Validate a digit string against the Luhn checksum used by all\n * major card brands. Returns false on empty / non-digit input.\n */\nexport function luhnValid(digits: string): boolean {\n if (digits.length === 0) return false\n let sum = 0\n let alternate = false\n for (let i = digits.length - 1; i >= 0; i--) {\n const ch = digits.charCodeAt(i)\n if (ch < 48 || ch > 57) return false\n let n = ch - 48\n if (alternate) {\n n *= 2\n if (n > 9) n -= 9\n }\n sum += n\n alternate = !alternate\n }\n return sum > 0 && sum % 10 === 0\n}\n\n// 13–19 digits, optionally with single spaces or dashes between\n// groups. Anchored with \\b so we don't match the middle of a longer\n// digit string.\nconst RE_CARD_CANDIDATE = /\\b(?:\\d[ -]?){12,18}\\d\\b/g\n\nfunction scrubCreditCards(input: string): string {\n return input.replace(RE_CARD_CANDIDATE, (match) => {\n const digits = match.replace(/[ -]/g, '')\n if (digits.length < 13 || digits.length > 19) return match\n if (!luhnValid(digits)) return match\n return '[REDACTED-CARD]'\n })\n}\n\n/**\n * Run the credential / PII patterns over a string. Pure, idempotent\n * (already-scrubbed text stays stable), no I/O. Non-string input\n * is returned unchanged so this function is safe to call inside\n * the recursive walker.\n */\nexport function scrubPii(input: string): string {\n // The non-string branch matters: `scrubAnyValue` calls this on\n // unknown values, and we'd rather return the raw value than crash.\n if (typeof input !== 'string' || input.length === 0) return input\n let out = input\n for (const { re, replacement } of KEY_PATTERNS) {\n // Pin a fresh RegExp instance per call: `g`-flagged regexes\n // carry mutable `lastIndex` state, and although `replace` does\n // not depend on it, future Worker-based callers might.\n out = out.replace(new RegExp(re.source, re.flags), replacement)\n }\n out = scrubCreditCards(out)\n return out\n}\n\n/**\n * Recursively scrub every string leaf in a JSON-like value.\n * Non-string primitives, undefined, arrays, and plain objects are\n * walked structurally. Anything else (Date, Map, Set, etc.) is\n * returned unchanged — this package never serialises those.\n *\n * Returns a fresh value; the input is never mutated.\n */\nexport function scrubAnyValue(value: unknown): unknown {\n if (typeof value === 'string') return scrubPii(value)\n if (Array.isArray(value)) return value.map(scrubAnyValue)\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = scrubAnyValue(v)\n }\n return out\n }\n return value\n}\n","/**\n * Instrument for `client.messages.create`.\n *\n * Wraps the Anthropic SDK's primary Messages endpoint. Handles both\n * transports:\n *\n * - Non-streaming: pulls text from `content[].text` blocks, tool\n * calls from `content[].tool_use` blocks (input is unknown JSON,\n * we serialise via JSON.stringify so the wire format matches the\n * openai wrapper's `arguments` string).\n *\n * - Streaming: a state machine over the event sequence\n * `message_start` → `content_block_start` → many\n * `content_block_delta` → `content_block_stop` → `message_delta`\n * → `message_stop`. We maintain a per-block aggregator keyed by\n * `index`. Text deltas concatenate `delta.text`. Tool-use deltas\n * carry `delta.partial_json` fragments that we append to that\n * block's `arguments` string. The final `message_delta` carries\n * the output_tokens; the initial `message_start` carries the\n * input_tokens + the two cache fields.\n *\n * Token surface emitted on `metadata.tokens`:\n * - input, output, total: always\n * - cache_read: only when `cache_read_input_tokens > 0`\n * - cache_creation: only when `cache_creation_input_tokens > 0`\n *\n * Privacy fan-out: identical contract to @voightxyz/openai\n * (`minimal` → tags only, `standard` → scrubbed content,\n * `full` → verbatim). The first tool call's name flows into\n * `toolExecuted` at every level so the audit-log DETAIL column\n * keeps rendering meaningfully even under minimal.\n */\n\nimport type { EventPayload, PrivacyLevel } from '../types.js'\nimport { scrubAnyValue, scrubPii } from '../privacy.js'\n\n// ─── Loose Anthropic types ────────────────────────────────────────\n//\n// We model only the surface this instrument actually reads. Keeping\n// the types loose insulates us from upstream SDK changes that don't\n// affect our wire shape.\n\ninterface MessageCreateParams {\n model: string\n max_tokens: number\n messages: Array<Record<string, unknown>>\n stream?: boolean\n [k: string]: unknown\n}\n\ninterface AnthropicUsage {\n input_tokens?: number\n output_tokens?: number\n cache_creation_input_tokens?: number | null\n cache_read_input_tokens?: number | null\n [k: string]: unknown\n}\n\ninterface TextContentBlock {\n type: 'text'\n text: string\n}\n\ninterface ToolUseContentBlock {\n type: 'tool_use'\n id: string\n name: string\n input: unknown\n}\n\ntype ContentBlock =\n | TextContentBlock\n | ToolUseContentBlock\n | { type: string; [k: string]: unknown }\n\ninterface NonStreamingMessage {\n id?: string\n model?: string\n content?: ContentBlock[]\n stop_reason?: string | null\n usage?: AnthropicUsage\n [k: string]: unknown\n}\n\n// Streaming events\ninterface StreamMessageStart {\n type: 'message_start'\n message: NonStreamingMessage\n}\ninterface StreamContentBlockStart {\n type: 'content_block_start'\n index: number\n content_block: ContentBlock\n}\ninterface StreamContentBlockDelta {\n type: 'content_block_delta'\n index: number\n delta:\n | { type: 'text_delta'; text: string }\n | { type: 'input_json_delta'; partial_json: string }\n | { type: string; [k: string]: unknown }\n}\ninterface StreamContentBlockStop {\n type: 'content_block_stop'\n index: number\n}\ninterface StreamMessageDelta {\n type: 'message_delta'\n delta: { stop_reason?: string | null; [k: string]: unknown }\n usage?: { output_tokens?: number; [k: string]: unknown }\n}\ninterface StreamMessageStop {\n type: 'message_stop'\n}\n\ntype StreamEvent =\n | StreamMessageStart\n | StreamContentBlockStart\n | StreamContentBlockDelta\n | StreamContentBlockStop\n | StreamMessageDelta\n | StreamMessageStop\n | { type: string; [k: string]: unknown }\n\ntype CreateFn = (\n params: MessageCreateParams,\n) => Promise<NonStreamingMessage | AsyncIterable<StreamEvent>>\n\n/**\n * Flat tool-call shape we emit. Mirrors the openai wrapper's\n * `metadata.toolCalls[*]` schema so dashboard rendering doesn't\n * need to know which provider produced the event.\n */\ninterface CapturedToolCall {\n id: string\n name: string\n arguments: string\n}\n\ninterface NormalisedTokens {\n input: number\n output: number\n total: number\n cache_read?: number\n cache_creation?: number\n}\n\nexport interface EventSink {\n send: (event: EventPayload) => void\n}\n\nexport interface InstrumentContext {\n agentId: string\n privacy: PrivacyLevel\n /**\n * Trace grouping identifier stamped on every emitted event under\n * `metadata.sessionId`. The wrapper resolves it once per instance\n * (explicit option or auto-generated UUID v4).\n */\n sessionId: string\n ingest: EventSink\n /** Time source in ms; injected so tests can produce deterministic `durationMs`. */\n now: () => number\n}\n\nexport function instrumentMessages(\n original: CreateFn,\n ctx: InstrumentContext,\n): CreateFn {\n return async function wrappedCreate(params: MessageCreateParams) {\n const startedAt = ctx.now()\n const isStream = params.stream === true\n\n let result: NonStreamingMessage | AsyncIterable<StreamEvent>\n try {\n result = await original(params)\n } catch (err) {\n ctx.ingest.send(\n buildFailureEvent({ ctx, params, startedAt, error: err }),\n )\n throw err\n }\n\n if (!isStream) {\n ctx.ingest.send(\n buildSuccessEvent({\n ctx,\n params,\n startedAt,\n response: result as NonStreamingMessage,\n }),\n )\n return result\n }\n\n return wrapStream(\n result as AsyncIterable<StreamEvent>,\n ctx,\n params,\n startedAt,\n )\n }\n}\n\n// ─── Event builders ──────────────────────────────────────────────\n\nfunction buildSuccessEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n response: NonStreamingMessage\n}): EventPayload {\n const { ctx, params, startedAt, response } = args\n const responseText = extractText(response.content ?? [])\n const toolCalls = extractToolCalls(response.content ?? [])\n const tokens = normaliseTokens(response.usage)\n const durationMs = ctx.now() - startedAt\n\n return assembleEvent({\n ctx,\n params,\n durationMs,\n outcome: 'success',\n responseText: responseText.length > 0 ? responseText : undefined,\n tokens,\n toolCalls,\n streaming: false,\n finishReason: response.stop_reason ?? null,\n modelFromResponse: response.model,\n })\n}\n\nfunction buildFailureEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n error: unknown\n}): EventPayload {\n const { ctx, params, startedAt, error } = args\n const durationMs = ctx.now() - startedAt\n const message = error instanceof Error ? error.message : String(error)\n return assembleEvent({\n ctx,\n params,\n durationMs,\n outcome: 'failed',\n streaming: params.stream === true,\n errorMessage: message,\n })\n}\n\nfunction buildStreamEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n aggregatedText: string\n tokens: NormalisedTokens | null\n toolCalls: CapturedToolCall[] | null\n modelFromResponse: string | undefined\n finishReason: string | null\n}): EventPayload {\n return assembleEvent({\n ctx: args.ctx,\n params: args.params,\n durationMs: args.ctx.now() - args.startedAt,\n outcome: 'success',\n responseText:\n args.aggregatedText.length > 0 ? args.aggregatedText : undefined,\n tokens: args.tokens,\n toolCalls: args.toolCalls,\n streaming: true,\n finishReason: args.finishReason,\n modelFromResponse: args.modelFromResponse,\n })\n}\n\n/**\n * Single assembler — privacy fan-out + payload shape in one place\n * so the three callers above can't drift apart.\n */\nfunction assembleEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n durationMs: number\n outcome: 'success' | 'failed'\n responseText?: string | undefined\n tokens?: NormalisedTokens | null\n toolCalls?: CapturedToolCall[] | null\n streaming: boolean\n finishReason?: string | null\n errorMessage?: string\n modelFromResponse?: string | undefined\n}): EventPayload {\n const { ctx, params, durationMs, outcome, streaming, errorMessage } = args\n const tokens = args.tokens ?? null\n const toolCalls = args.toolCalls ?? null\n const model = args.modelFromResponse ?? params.model\n\n const metadata: Record<string, unknown> = {\n source: 'anthropic-sdk',\n privacyLevel: ctx.privacy,\n streaming,\n sessionId: ctx.sessionId,\n }\n if (tokens) metadata.tokens = tokens\n if (args.finishReason !== undefined && args.finishReason !== null) {\n metadata.finishReason = args.finishReason\n }\n\n const firstToolName =\n toolCalls && toolCalls.length > 0 ? toolCalls[0]!.name : undefined\n\n if (ctx.privacy === 'minimal') {\n return {\n agentId: ctx.agentId,\n type: 'reasoning',\n model,\n durationMs,\n outcome,\n ...(firstToolName ? { toolExecuted: firstToolName } : {}),\n metadata,\n ...(errorMessage ? { errorMessage } : {}),\n }\n }\n\n const messages =\n ctx.privacy === 'standard'\n ? (scrubAnyValue(params.messages) as MessageCreateParams['messages'])\n : params.messages\n\n const responseText = args.responseText\n const scrubbedResponse =\n responseText !== undefined\n ? ctx.privacy === 'standard'\n ? scrubPii(responseText)\n : responseText\n : undefined\n if (scrubbedResponse !== undefined) {\n metadata.responseText = scrubbedResponse\n }\n\n if (toolCalls && toolCalls.length > 0) {\n metadata.toolCalls =\n ctx.privacy === 'standard'\n ? toolCalls.map((t) => ({\n id: t.id,\n name: t.name,\n arguments: scrubPii(t.arguments),\n }))\n : toolCalls\n }\n\n return {\n agentId: ctx.agentId,\n type: 'reasoning',\n model,\n durationMs,\n outcome,\n ...(firstToolName ? { toolExecuted: firstToolName } : {}),\n input: { messages },\n metadata,\n ...(errorMessage ? { errorMessage } : {}),\n }\n}\n\n// ─── Non-streaming helpers ──────────────────────────────────────\n\nfunction extractText(content: ContentBlock[]): string {\n let out = ''\n for (const block of content) {\n if (block.type === 'text' && typeof (block as TextContentBlock).text === 'string') {\n out += (block as TextContentBlock).text\n }\n }\n return out\n}\n\nfunction extractToolCalls(content: ContentBlock[]): CapturedToolCall[] | null {\n const out: CapturedToolCall[] = []\n for (const block of content) {\n if (block.type !== 'tool_use') continue\n const t = block as ToolUseContentBlock\n out.push({\n id: typeof t.id === 'string' ? t.id : '',\n name: typeof t.name === 'string' ? t.name : '',\n // Serialise the model's chosen tool input to a JSON string so\n // the wire shape matches openai's `arguments: string` exactly.\n // Anthropic gives us a parsed object; openai gives us the raw\n // string. We normalise here.\n arguments: safeStringify(t.input),\n })\n }\n return out.length > 0 ? out : null\n}\n\nfunction safeStringify(v: unknown): string {\n if (typeof v === 'string') return v\n try {\n return JSON.stringify(v ?? {})\n } catch {\n return ''\n }\n}\n\nfunction normaliseTokens(u: AnthropicUsage | undefined): NormalisedTokens | null {\n if (!u) return null\n const input = numberOrZero(u.input_tokens)\n const output = numberOrZero(u.output_tokens)\n const total = input + output\n const cacheRead = numberOrZero(u.cache_read_input_tokens)\n const cacheCreation = numberOrZero(u.cache_creation_input_tokens)\n const base: NormalisedTokens = { input, output, total }\n if (cacheRead > 0) base.cache_read = cacheRead\n if (cacheCreation > 0) base.cache_creation = cacheCreation\n return base\n}\n\nfunction numberOrZero(v: unknown): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : 0\n}\n\n// ─── Streaming wrapper ──────────────────────────────────────────\n\n/**\n * Mutating accumulator for the streaming state machine. We hold a\n * single source of truth across chunks: text aggregator, per-index\n * tool-call entries, latest usage seen, final stop_reason. The wrap\n * function yields each event to the user untouched and only mutates\n * this struct as bytes pass through.\n */\ninterface StreamState {\n aggregatedText: string\n toolBlocks: Map<number, CapturedToolCall>\n usage: AnthropicUsage | null\n modelFromResponse: string | undefined\n finishReason: string | null\n}\n\nfunction wrapStream(\n source: AsyncIterable<StreamEvent>,\n ctx: InstrumentContext,\n params: MessageCreateParams,\n startedAt: number,\n): AsyncIterable<StreamEvent> {\n const state: StreamState = {\n aggregatedText: '',\n toolBlocks: new Map(),\n usage: null,\n modelFromResponse: undefined,\n finishReason: null,\n }\n let emitted = false\n\n function emit() {\n if (emitted) return\n emitted = true\n ctx.ingest.send(\n buildStreamEvent({\n ctx,\n params,\n startedAt,\n aggregatedText: state.aggregatedText,\n tokens: normaliseTokens(state.usage ?? undefined),\n toolCalls: snapshotTools(state.toolBlocks),\n modelFromResponse: state.modelFromResponse,\n finishReason: state.finishReason,\n }),\n )\n }\n\n return {\n async *[Symbol.asyncIterator]() {\n try {\n for await (const ev of source) {\n applyEvent(state, ev)\n yield ev\n }\n } catch (err) {\n ctx.ingest.send(\n buildFailureEvent({ ctx, params, startedAt, error: err }),\n )\n emitted = true\n throw err\n } finally {\n emit()\n }\n },\n }\n}\n\n/**\n * Step the streaming state machine one event forward. Pure on the\n * `state` argument (mutates it in place). Unknown event types pass\n * through silently — Anthropic adds new event types over time and\n * we don't want a new event class to break capture.\n */\nfunction applyEvent(state: StreamState, ev: StreamEvent): void {\n switch (ev.type) {\n case 'message_start': {\n const e = ev as StreamMessageStart\n if (e.message?.model && !state.modelFromResponse) {\n state.modelFromResponse = e.message.model\n }\n if (e.message?.usage) {\n // message_start carries input_tokens + cache fields. We\n // seed `usage` here so the final tokens object includes\n // cache numbers even if message_delta only overwrites\n // output_tokens.\n state.usage = { ...(state.usage ?? {}), ...e.message.usage }\n }\n return\n }\n case 'content_block_start': {\n const e = ev as StreamContentBlockStart\n if (e.content_block?.type === 'tool_use') {\n const tu = e.content_block as ToolUseContentBlock\n state.toolBlocks.set(e.index, {\n id: typeof tu.id === 'string' ? tu.id : '',\n name: typeof tu.name === 'string' ? tu.name : '',\n arguments: '',\n })\n }\n return\n }\n case 'content_block_delta': {\n const e = ev as StreamContentBlockDelta\n const d = e.delta as { type: string; [k: string]: unknown }\n if (d.type === 'text_delta' && typeof d.text === 'string') {\n state.aggregatedText += d.text\n } else if (\n d.type === 'input_json_delta' &&\n typeof d.partial_json === 'string'\n ) {\n const entry = state.toolBlocks.get(e.index)\n if (entry) entry.arguments += d.partial_json\n }\n return\n }\n case 'message_delta': {\n const e = ev as StreamMessageDelta\n if (e.delta?.stop_reason && state.finishReason === null) {\n state.finishReason = e.delta.stop_reason\n }\n if (e.usage) {\n // message_delta only carries the final output_tokens. Merge,\n // don't replace, so we keep input_tokens + cache fields from\n // message_start.\n state.usage = { ...(state.usage ?? {}), ...e.usage }\n }\n return\n }\n // content_block_stop, message_stop, and unknown future event\n // types pass through without state updates.\n }\n}\n\nfunction snapshotTools(\n acc: Map<number, CapturedToolCall>,\n): CapturedToolCall[] | null {\n if (acc.size === 0) return null\n const entries = [...acc.entries()].sort(([a], [b]) => a - b)\n const out = entries.map(([, v]) => v).filter((t) => t.name.length > 0)\n return out.length > 0 ? out : null\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -20,6 +20,18 @@ interface WrapOptions {
|
|
|
20
20
|
agent?: string;
|
|
21
21
|
/** Default `'standard'`. See {@link PrivacyLevel}. */
|
|
22
22
|
privacy?: PrivacyLevel;
|
|
23
|
+
/**
|
|
24
|
+
* Trace grouping identifier. Stamped on `metadata.sessionId` of
|
|
25
|
+
* every event emitted by this wrapper instance, so the dashboard
|
|
26
|
+
* can render related calls (chat turns, multi-step agent runs)
|
|
27
|
+
* as a single trace.
|
|
28
|
+
*
|
|
29
|
+
* When omitted, the wrapper auto-generates a UUID v4 once per
|
|
30
|
+
* `wrapAnthropic()` call and reuses it for the life of the wrapped
|
|
31
|
+
* client. Pass an explicit value to scope a session yourself
|
|
32
|
+
* (per-user, per-conversation, per-request, …).
|
|
33
|
+
*/
|
|
34
|
+
sessionId?: string;
|
|
23
35
|
/** Kill switch. When `false` the wrapper is a no-op pass-through. */
|
|
24
36
|
enabled?: boolean;
|
|
25
37
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,18 @@ interface WrapOptions {
|
|
|
20
20
|
agent?: string;
|
|
21
21
|
/** Default `'standard'`. See {@link PrivacyLevel}. */
|
|
22
22
|
privacy?: PrivacyLevel;
|
|
23
|
+
/**
|
|
24
|
+
* Trace grouping identifier. Stamped on `metadata.sessionId` of
|
|
25
|
+
* every event emitted by this wrapper instance, so the dashboard
|
|
26
|
+
* can render related calls (chat turns, multi-step agent runs)
|
|
27
|
+
* as a single trace.
|
|
28
|
+
*
|
|
29
|
+
* When omitted, the wrapper auto-generates a UUID v4 once per
|
|
30
|
+
* `wrapAnthropic()` call and reuses it for the life of the wrapped
|
|
31
|
+
* client. Pass an explicit value to scope a session yourself
|
|
32
|
+
* (per-user, per-conversation, per-request, …).
|
|
33
|
+
*/
|
|
34
|
+
sessionId?: string;
|
|
23
35
|
/** Kill switch. When `false` the wrapper is a no-op pass-through. */
|
|
24
36
|
enabled?: boolean;
|
|
25
37
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/wrap.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
|
|
1
4
|
// src/identity.ts
|
|
2
5
|
function nonBlank(value) {
|
|
3
6
|
if (typeof value !== "string") return null;
|
|
@@ -209,7 +212,8 @@ function assembleEvent(args) {
|
|
|
209
212
|
const metadata = {
|
|
210
213
|
source: "anthropic-sdk",
|
|
211
214
|
privacyLevel: ctx.privacy,
|
|
212
|
-
streaming
|
|
215
|
+
streaming,
|
|
216
|
+
sessionId: ctx.sessionId
|
|
213
217
|
};
|
|
214
218
|
if (tokens) metadata.tokens = tokens;
|
|
215
219
|
if (args.finishReason !== void 0 && args.finishReason !== null) {
|
|
@@ -425,9 +429,11 @@ function wrapAnthropic(client, options = {}) {
|
|
|
425
429
|
apiKey,
|
|
426
430
|
fetch: opts._fetch
|
|
427
431
|
});
|
|
432
|
+
const sessionId = typeof opts.sessionId === "string" && opts.sessionId.trim().length > 0 ? opts.sessionId.trim() : randomUUID();
|
|
428
433
|
const ctx = {
|
|
429
434
|
agentId,
|
|
430
435
|
privacy: opts.privacy ?? "standard",
|
|
436
|
+
sessionId,
|
|
431
437
|
ingest,
|
|
432
438
|
now: () => Date.now()
|
|
433
439
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/identity.ts","../src/ingest.ts","../src/privacy.ts","../src/instruments/messages.ts","../src/wrap.ts"],"sourcesContent":["/**\n * Identity resolution: API key + agent label.\n *\n * Both helpers are pure — they take the user's options and an `env`\n * record, and return a result. `env` defaults to `process.env` when\n * called bare; passing it explicitly lets tests exercise every\n * fallback path without mutating global state.\n *\n * Resolution priority is fixed and documented at each call site:\n *\n * resolveApiKey: options.voightApiKey → env.VOIGHT_KEY → null\n * resolveAgent: options.agent → env.VOIGHT_AGENT\n * → env.HOSTNAME → 'unknown-agent'\n *\n * Empty and whitespace-only values are treated as missing so a\n * misconfigured env (`VOIGHT_KEY=`) falls through cleanly instead\n * of being mistaken for a real key.\n */\n\nexport interface IdentityOptions {\n voightApiKey?: string | undefined\n agent?: string | undefined\n}\n\ntype Env = Record<string, string | undefined>\n\n/**\n * Trim and return the value, or `null` if it's missing / blank.\n * Centralises the \"empty counts as missing\" rule that both helpers\n * apply at every layer.\n */\nfunction nonBlank(value: string | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length === 0 ? null : trimmed\n}\n\n/**\n * Resolve the Voight API key for outgoing events.\n *\n * Returns `null` when nothing usable is configured. Callers decide\n * how to react — `wrapOpenAI` logs a one-time warning and falls\n * back to a no-op transport, so a missing key never crashes the\n * host app.\n */\nexport function resolveApiKey(\n options: IdentityOptions = {},\n env: Env = process.env,\n): string | null {\n return nonBlank(options.voightApiKey) ?? nonBlank(env.VOIGHT_KEY)\n}\n\n/**\n * Resolve the agent label that groups events in the dashboard.\n *\n * Always returns a non-empty string: when nothing else resolves we\n * emit `'unknown-agent'` so the event is still ingestable and the\n * misconfiguration shows up on screen instead of being silently\n * dropped.\n */\nexport function resolveAgent(\n options: IdentityOptions = {},\n env: Env = process.env,\n): string {\n return (\n nonBlank(options.agent) ??\n nonBlank(env.VOIGHT_AGENT) ??\n nonBlank(env.HOSTNAME) ??\n 'unknown-agent'\n )\n}\n","/**\n * Fire-and-forget HTTP client for the Voight ingest endpoint.\n *\n * Why \"fire and forget\":\n *\n * The wrapper is in the user's app's hot path. A failing or slow\n * Voight backend must NEVER turn into a failing or slow OpenAI\n * call for the user. `send` returns synchronously; the actual\n * POST happens on the microtask queue and any error reaches the\n * optional `onError` hook rather than the caller's stack.\n *\n * We intentionally do not retry, batch, or buffer in beta.1.\n * Those add state and complexity — we'd rather drop the\n * occasional event than ship a half-implemented queue. Retry +\n * buffer arrive in 0.2.0 once we have real-world failure-mode\n * data to design against.\n *\n * Why inject `fetch`:\n *\n * Lets unit tests assert request shape (URL, headers, body)\n * without going to the network and without monkey-patching\n * the global. In production the option is omitted and the\n * runtime's `fetch` (Node 18+) is used.\n */\n\nimport type { EventPayload } from './types.js'\n\nexport interface IngestOptions {\n apiBase: string\n apiKey: string\n /**\n * Optional override for the network call. Tests inject a mock;\n * production callers leave this unset and `globalThis.fetch` is\n * used at dispatch time.\n */\n fetch?: typeof fetch | undefined\n /**\n * Called when a network error or non-2xx response would otherwise\n * be silently dropped. Useful for surfacing misconfiguration\n * (bad key, wrong apiBase) during development. Defaults to a\n * no-op so production stays quiet.\n */\n onError?: ((err: unknown) => void) | undefined\n}\n\nexport interface IngestClient {\n send: (event: EventPayload) => void\n}\n\n/**\n * Build a client bound to a single apiBase + apiKey pair.\n *\n * The returned `send` is synchronous and never throws. Errors are\n * routed to `onError` (a no-op by default).\n */\nexport function createIngestClient(opts: IngestOptions): IngestClient {\n // Normalise the base URL once so callers can pass it with or\n // without a trailing slash and we don't end up with `//v1/events`.\n const base = opts.apiBase.replace(/\\/+$/, '')\n const url = `${base}/v1/events`\n const headers = {\n 'content-type': 'application/json',\n authorization: `Bearer ${opts.apiKey}`,\n }\n const fetchImpl = opts.fetch ?? globalThis.fetch\n const onError = opts.onError ?? (() => {})\n\n return {\n send(event) {\n // Wrap the dispatch in an IIFE so a synchronous throw inside\n // `JSON.stringify` (e.g. circular structure) still ends up\n // at `onError` instead of bubbling to the user's hot path.\n void (async () => {\n try {\n const body = JSON.stringify(event)\n const res = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n })\n if (!res.ok) {\n onError(\n new Error(\n `voight ingest failed: ${res.status} ${res.statusText}`,\n ),\n )\n }\n } catch (err) {\n onError(err)\n }\n })()\n },\n }\n}\n","/**\n * PII scrubbing utilities. Ports the regex catalogue proven in\n * @voightxyz/sdk's privacy.ts (12 patterns + Luhn-validated cards),\n * trimmed to the surface this package needs.\n *\n * `scrubPii` and `scrubAnyValue` are pure: same input produces same\n * output, no I/O, no randomness. Safe to call in a hot path.\n *\n * Why duplicate the catalogue here (rather than depend on the SDK)?\n *\n * v0.1.0-beta.1 ships standalone — no runtime dependency on\n * `@voightxyz/sdk` — so a user installing only `@voightxyz/openai`\n * gets a complete package. If duplication ever becomes painful\n * (a third copy under @voightxyz/anthropic, say), the right move\n * is to extract a private `@voightxyz/core` package; until then,\n * the cost of a 12-pattern table is lower than the cost of a\n * peer-dep + version-skew matrix.\n */\n\n// ─── PII patterns ──────────────────────────────────────────────────\n//\n// Order matters. Multi-line / most-specific patterns run FIRST so\n// that once a span is consumed (e.g. a PEM block), later patterns\n// can't re-match its insides. JWTs run before generic API-key\n// patterns to avoid partial matches.\n//\n// Each pattern has a `name` for unit-test traceability and a `re`\n// that uses the global flag (so `replaceAll` semantics apply).\n\ntype Pattern = {\n name: string\n re: RegExp\n replacement: string\n}\n\nconst RE_PEM_PRIVATE_KEY =\n /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g\n\nconst RE_JWT =\n /\\beyJ[A-Za-z0-9_-]{10,}\\.eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g\n\n// Anthropic must precede the generic OpenAI rule so `sk-ant-...`\n// doesn't get partially consumed as `sk-...`.\nconst RE_ANTHROPIC = /\\bsk-ant-[A-Za-z0-9_-]{40,}\\b/g\n\nconst RE_OPENAI = /\\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\\b/g\n\nconst RE_STRIPE_LIVE = /\\b(?:sk|pk)_live_[A-Za-z0-9]{20,}\\b/g\n\nconst RE_GITHUB_FINE = /\\bgithub_pat_[A-Za-z0-9_]{20,}\\b/g\nconst RE_GITHUB_CLASSIC = /\\bghp_[A-Za-z0-9]{36}\\b/g\n\nconst RE_AWS_ACCESS_KEY = /\\bAKIA[A-Z0-9]{16}\\b/g\n\nconst RE_SLACK = /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g\n\n// Voight's own keys. Defense-in-depth: even if the user accidentally\n// pastes their `vk_…` into a prompt, it never leaves the process in\n// the clear under standard privacy.\nconst RE_VOIGHT = /\\bvk_[A-Za-z0-9_-]{32,}\\b/g\n\n// Strict email: requires a TLD with ≥2 letters. `support@app`\n// (no TLD) and `email_template` (no @) do not match.\nconst RE_EMAIL = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g\n\n// Phone in E.164. Looser variants produce too many false positives\n// over order numbers and identifiers.\nconst RE_PHONE_E164 = /\\+\\d{10,15}\\b/g\n\nconst KEY_PATTERNS: readonly Pattern[] = [\n { name: 'pem-private-key', re: RE_PEM_PRIVATE_KEY, replacement: '[REDACTED-PRIVATE-KEY]' },\n { name: 'jwt', re: RE_JWT, replacement: '[REDACTED-JWT]' },\n { name: 'anthropic-key', re: RE_ANTHROPIC, replacement: '[REDACTED-API-KEY]' },\n { name: 'openai-key', re: RE_OPENAI, replacement: '[REDACTED-API-KEY]' },\n { name: 'stripe-live-key', re: RE_STRIPE_LIVE, replacement: '[REDACTED-API-KEY]' },\n { name: 'github-fine-pat', re: RE_GITHUB_FINE, replacement: '[REDACTED-API-KEY]' },\n { name: 'github-classic-pat', re: RE_GITHUB_CLASSIC, replacement: '[REDACTED-API-KEY]' },\n { name: 'aws-access-key', re: RE_AWS_ACCESS_KEY, replacement: '[REDACTED-API-KEY]' },\n { name: 'slack-token', re: RE_SLACK, replacement: '[REDACTED-API-KEY]' },\n { name: 'voight-key', re: RE_VOIGHT, replacement: '[REDACTED-API-KEY]' },\n { name: 'email', re: RE_EMAIL, replacement: '[REDACTED-EMAIL]' },\n { name: 'phone-e164', re: RE_PHONE_E164, replacement: '[REDACTED-PHONE]' },\n]\n\n// ─── Credit cards ─────────────────────────────────────────────────\n//\n// Credit-card numbers need Luhn validation to avoid false positives\n// over long numeric ID strings, so they don't fit the simple\n// {re, replacement} table.\n\n/**\n * Validate a digit string against the Luhn checksum used by all\n * major card brands. Returns false on empty / non-digit input.\n */\nexport function luhnValid(digits: string): boolean {\n if (digits.length === 0) return false\n let sum = 0\n let alternate = false\n for (let i = digits.length - 1; i >= 0; i--) {\n const ch = digits.charCodeAt(i)\n if (ch < 48 || ch > 57) return false\n let n = ch - 48\n if (alternate) {\n n *= 2\n if (n > 9) n -= 9\n }\n sum += n\n alternate = !alternate\n }\n return sum > 0 && sum % 10 === 0\n}\n\n// 13–19 digits, optionally with single spaces or dashes between\n// groups. Anchored with \\b so we don't match the middle of a longer\n// digit string.\nconst RE_CARD_CANDIDATE = /\\b(?:\\d[ -]?){12,18}\\d\\b/g\n\nfunction scrubCreditCards(input: string): string {\n return input.replace(RE_CARD_CANDIDATE, (match) => {\n const digits = match.replace(/[ -]/g, '')\n if (digits.length < 13 || digits.length > 19) return match\n if (!luhnValid(digits)) return match\n return '[REDACTED-CARD]'\n })\n}\n\n/**\n * Run the credential / PII patterns over a string. Pure, idempotent\n * (already-scrubbed text stays stable), no I/O. Non-string input\n * is returned unchanged so this function is safe to call inside\n * the recursive walker.\n */\nexport function scrubPii(input: string): string {\n // The non-string branch matters: `scrubAnyValue` calls this on\n // unknown values, and we'd rather return the raw value than crash.\n if (typeof input !== 'string' || input.length === 0) return input\n let out = input\n for (const { re, replacement } of KEY_PATTERNS) {\n // Pin a fresh RegExp instance per call: `g`-flagged regexes\n // carry mutable `lastIndex` state, and although `replace` does\n // not depend on it, future Worker-based callers might.\n out = out.replace(new RegExp(re.source, re.flags), replacement)\n }\n out = scrubCreditCards(out)\n return out\n}\n\n/**\n * Recursively scrub every string leaf in a JSON-like value.\n * Non-string primitives, undefined, arrays, and plain objects are\n * walked structurally. Anything else (Date, Map, Set, etc.) is\n * returned unchanged — this package never serialises those.\n *\n * Returns a fresh value; the input is never mutated.\n */\nexport function scrubAnyValue(value: unknown): unknown {\n if (typeof value === 'string') return scrubPii(value)\n if (Array.isArray(value)) return value.map(scrubAnyValue)\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = scrubAnyValue(v)\n }\n return out\n }\n return value\n}\n","/**\n * Instrument for `client.messages.create`.\n *\n * Wraps the Anthropic SDK's primary Messages endpoint. Handles both\n * transports:\n *\n * - Non-streaming: pulls text from `content[].text` blocks, tool\n * calls from `content[].tool_use` blocks (input is unknown JSON,\n * we serialise via JSON.stringify so the wire format matches the\n * openai wrapper's `arguments` string).\n *\n * - Streaming: a state machine over the event sequence\n * `message_start` → `content_block_start` → many\n * `content_block_delta` → `content_block_stop` → `message_delta`\n * → `message_stop`. We maintain a per-block aggregator keyed by\n * `index`. Text deltas concatenate `delta.text`. Tool-use deltas\n * carry `delta.partial_json` fragments that we append to that\n * block's `arguments` string. The final `message_delta` carries\n * the output_tokens; the initial `message_start` carries the\n * input_tokens + the two cache fields.\n *\n * Token surface emitted on `metadata.tokens`:\n * - input, output, total: always\n * - cache_read: only when `cache_read_input_tokens > 0`\n * - cache_creation: only when `cache_creation_input_tokens > 0`\n *\n * Privacy fan-out: identical contract to @voightxyz/openai\n * (`minimal` → tags only, `standard` → scrubbed content,\n * `full` → verbatim). The first tool call's name flows into\n * `toolExecuted` at every level so the audit-log DETAIL column\n * keeps rendering meaningfully even under minimal.\n */\n\nimport type { EventPayload, PrivacyLevel } from '../types.js'\nimport { scrubAnyValue, scrubPii } from '../privacy.js'\n\n// ─── Loose Anthropic types ────────────────────────────────────────\n//\n// We model only the surface this instrument actually reads. Keeping\n// the types loose insulates us from upstream SDK changes that don't\n// affect our wire shape.\n\ninterface MessageCreateParams {\n model: string\n max_tokens: number\n messages: Array<Record<string, unknown>>\n stream?: boolean\n [k: string]: unknown\n}\n\ninterface AnthropicUsage {\n input_tokens?: number\n output_tokens?: number\n cache_creation_input_tokens?: number | null\n cache_read_input_tokens?: number | null\n [k: string]: unknown\n}\n\ninterface TextContentBlock {\n type: 'text'\n text: string\n}\n\ninterface ToolUseContentBlock {\n type: 'tool_use'\n id: string\n name: string\n input: unknown\n}\n\ntype ContentBlock =\n | TextContentBlock\n | ToolUseContentBlock\n | { type: string; [k: string]: unknown }\n\ninterface NonStreamingMessage {\n id?: string\n model?: string\n content?: ContentBlock[]\n stop_reason?: string | null\n usage?: AnthropicUsage\n [k: string]: unknown\n}\n\n// Streaming events\ninterface StreamMessageStart {\n type: 'message_start'\n message: NonStreamingMessage\n}\ninterface StreamContentBlockStart {\n type: 'content_block_start'\n index: number\n content_block: ContentBlock\n}\ninterface StreamContentBlockDelta {\n type: 'content_block_delta'\n index: number\n delta:\n | { type: 'text_delta'; text: string }\n | { type: 'input_json_delta'; partial_json: string }\n | { type: string; [k: string]: unknown }\n}\ninterface StreamContentBlockStop {\n type: 'content_block_stop'\n index: number\n}\ninterface StreamMessageDelta {\n type: 'message_delta'\n delta: { stop_reason?: string | null; [k: string]: unknown }\n usage?: { output_tokens?: number; [k: string]: unknown }\n}\ninterface StreamMessageStop {\n type: 'message_stop'\n}\n\ntype StreamEvent =\n | StreamMessageStart\n | StreamContentBlockStart\n | StreamContentBlockDelta\n | StreamContentBlockStop\n | StreamMessageDelta\n | StreamMessageStop\n | { type: string; [k: string]: unknown }\n\ntype CreateFn = (\n params: MessageCreateParams,\n) => Promise<NonStreamingMessage | AsyncIterable<StreamEvent>>\n\n/**\n * Flat tool-call shape we emit. Mirrors the openai wrapper's\n * `metadata.toolCalls[*]` schema so dashboard rendering doesn't\n * need to know which provider produced the event.\n */\ninterface CapturedToolCall {\n id: string\n name: string\n arguments: string\n}\n\ninterface NormalisedTokens {\n input: number\n output: number\n total: number\n cache_read?: number\n cache_creation?: number\n}\n\nexport interface EventSink {\n send: (event: EventPayload) => void\n}\n\nexport interface InstrumentContext {\n agentId: string\n privacy: PrivacyLevel\n ingest: EventSink\n /** Time source in ms; injected so tests can produce deterministic `durationMs`. */\n now: () => number\n}\n\nexport function instrumentMessages(\n original: CreateFn,\n ctx: InstrumentContext,\n): CreateFn {\n return async function wrappedCreate(params: MessageCreateParams) {\n const startedAt = ctx.now()\n const isStream = params.stream === true\n\n let result: NonStreamingMessage | AsyncIterable<StreamEvent>\n try {\n result = await original(params)\n } catch (err) {\n ctx.ingest.send(\n buildFailureEvent({ ctx, params, startedAt, error: err }),\n )\n throw err\n }\n\n if (!isStream) {\n ctx.ingest.send(\n buildSuccessEvent({\n ctx,\n params,\n startedAt,\n response: result as NonStreamingMessage,\n }),\n )\n return result\n }\n\n return wrapStream(\n result as AsyncIterable<StreamEvent>,\n ctx,\n params,\n startedAt,\n )\n }\n}\n\n// ─── Event builders ──────────────────────────────────────────────\n\nfunction buildSuccessEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n response: NonStreamingMessage\n}): EventPayload {\n const { ctx, params, startedAt, response } = args\n const responseText = extractText(response.content ?? [])\n const toolCalls = extractToolCalls(response.content ?? [])\n const tokens = normaliseTokens(response.usage)\n const durationMs = ctx.now() - startedAt\n\n return assembleEvent({\n ctx,\n params,\n durationMs,\n outcome: 'success',\n responseText: responseText.length > 0 ? responseText : undefined,\n tokens,\n toolCalls,\n streaming: false,\n finishReason: response.stop_reason ?? null,\n modelFromResponse: response.model,\n })\n}\n\nfunction buildFailureEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n error: unknown\n}): EventPayload {\n const { ctx, params, startedAt, error } = args\n const durationMs = ctx.now() - startedAt\n const message = error instanceof Error ? error.message : String(error)\n return assembleEvent({\n ctx,\n params,\n durationMs,\n outcome: 'failed',\n streaming: params.stream === true,\n errorMessage: message,\n })\n}\n\nfunction buildStreamEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n aggregatedText: string\n tokens: NormalisedTokens | null\n toolCalls: CapturedToolCall[] | null\n modelFromResponse: string | undefined\n finishReason: string | null\n}): EventPayload {\n return assembleEvent({\n ctx: args.ctx,\n params: args.params,\n durationMs: args.ctx.now() - args.startedAt,\n outcome: 'success',\n responseText:\n args.aggregatedText.length > 0 ? args.aggregatedText : undefined,\n tokens: args.tokens,\n toolCalls: args.toolCalls,\n streaming: true,\n finishReason: args.finishReason,\n modelFromResponse: args.modelFromResponse,\n })\n}\n\n/**\n * Single assembler — privacy fan-out + payload shape in one place\n * so the three callers above can't drift apart.\n */\nfunction assembleEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n durationMs: number\n outcome: 'success' | 'failed'\n responseText?: string | undefined\n tokens?: NormalisedTokens | null\n toolCalls?: CapturedToolCall[] | null\n streaming: boolean\n finishReason?: string | null\n errorMessage?: string\n modelFromResponse?: string | undefined\n}): EventPayload {\n const { ctx, params, durationMs, outcome, streaming, errorMessage } = args\n const tokens = args.tokens ?? null\n const toolCalls = args.toolCalls ?? null\n const model = args.modelFromResponse ?? params.model\n\n const metadata: Record<string, unknown> = {\n source: 'anthropic-sdk',\n privacyLevel: ctx.privacy,\n streaming,\n }\n if (tokens) metadata.tokens = tokens\n if (args.finishReason !== undefined && args.finishReason !== null) {\n metadata.finishReason = args.finishReason\n }\n\n const firstToolName =\n toolCalls && toolCalls.length > 0 ? toolCalls[0]!.name : undefined\n\n if (ctx.privacy === 'minimal') {\n return {\n agentId: ctx.agentId,\n type: 'reasoning',\n model,\n durationMs,\n outcome,\n ...(firstToolName ? { toolExecuted: firstToolName } : {}),\n metadata,\n ...(errorMessage ? { errorMessage } : {}),\n }\n }\n\n const messages =\n ctx.privacy === 'standard'\n ? (scrubAnyValue(params.messages) as MessageCreateParams['messages'])\n : params.messages\n\n const responseText = args.responseText\n const scrubbedResponse =\n responseText !== undefined\n ? ctx.privacy === 'standard'\n ? scrubPii(responseText)\n : responseText\n : undefined\n if (scrubbedResponse !== undefined) {\n metadata.responseText = scrubbedResponse\n }\n\n if (toolCalls && toolCalls.length > 0) {\n metadata.toolCalls =\n ctx.privacy === 'standard'\n ? toolCalls.map((t) => ({\n id: t.id,\n name: t.name,\n arguments: scrubPii(t.arguments),\n }))\n : toolCalls\n }\n\n return {\n agentId: ctx.agentId,\n type: 'reasoning',\n model,\n durationMs,\n outcome,\n ...(firstToolName ? { toolExecuted: firstToolName } : {}),\n input: { messages },\n metadata,\n ...(errorMessage ? { errorMessage } : {}),\n }\n}\n\n// ─── Non-streaming helpers ──────────────────────────────────────\n\nfunction extractText(content: ContentBlock[]): string {\n let out = ''\n for (const block of content) {\n if (block.type === 'text' && typeof (block as TextContentBlock).text === 'string') {\n out += (block as TextContentBlock).text\n }\n }\n return out\n}\n\nfunction extractToolCalls(content: ContentBlock[]): CapturedToolCall[] | null {\n const out: CapturedToolCall[] = []\n for (const block of content) {\n if (block.type !== 'tool_use') continue\n const t = block as ToolUseContentBlock\n out.push({\n id: typeof t.id === 'string' ? t.id : '',\n name: typeof t.name === 'string' ? t.name : '',\n // Serialise the model's chosen tool input to a JSON string so\n // the wire shape matches openai's `arguments: string` exactly.\n // Anthropic gives us a parsed object; openai gives us the raw\n // string. We normalise here.\n arguments: safeStringify(t.input),\n })\n }\n return out.length > 0 ? out : null\n}\n\nfunction safeStringify(v: unknown): string {\n if (typeof v === 'string') return v\n try {\n return JSON.stringify(v ?? {})\n } catch {\n return ''\n }\n}\n\nfunction normaliseTokens(u: AnthropicUsage | undefined): NormalisedTokens | null {\n if (!u) return null\n const input = numberOrZero(u.input_tokens)\n const output = numberOrZero(u.output_tokens)\n const total = input + output\n const cacheRead = numberOrZero(u.cache_read_input_tokens)\n const cacheCreation = numberOrZero(u.cache_creation_input_tokens)\n const base: NormalisedTokens = { input, output, total }\n if (cacheRead > 0) base.cache_read = cacheRead\n if (cacheCreation > 0) base.cache_creation = cacheCreation\n return base\n}\n\nfunction numberOrZero(v: unknown): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : 0\n}\n\n// ─── Streaming wrapper ──────────────────────────────────────────\n\n/**\n * Mutating accumulator for the streaming state machine. We hold a\n * single source of truth across chunks: text aggregator, per-index\n * tool-call entries, latest usage seen, final stop_reason. The wrap\n * function yields each event to the user untouched and only mutates\n * this struct as bytes pass through.\n */\ninterface StreamState {\n aggregatedText: string\n toolBlocks: Map<number, CapturedToolCall>\n usage: AnthropicUsage | null\n modelFromResponse: string | undefined\n finishReason: string | null\n}\n\nfunction wrapStream(\n source: AsyncIterable<StreamEvent>,\n ctx: InstrumentContext,\n params: MessageCreateParams,\n startedAt: number,\n): AsyncIterable<StreamEvent> {\n const state: StreamState = {\n aggregatedText: '',\n toolBlocks: new Map(),\n usage: null,\n modelFromResponse: undefined,\n finishReason: null,\n }\n let emitted = false\n\n function emit() {\n if (emitted) return\n emitted = true\n ctx.ingest.send(\n buildStreamEvent({\n ctx,\n params,\n startedAt,\n aggregatedText: state.aggregatedText,\n tokens: normaliseTokens(state.usage ?? undefined),\n toolCalls: snapshotTools(state.toolBlocks),\n modelFromResponse: state.modelFromResponse,\n finishReason: state.finishReason,\n }),\n )\n }\n\n return {\n async *[Symbol.asyncIterator]() {\n try {\n for await (const ev of source) {\n applyEvent(state, ev)\n yield ev\n }\n } catch (err) {\n ctx.ingest.send(\n buildFailureEvent({ ctx, params, startedAt, error: err }),\n )\n emitted = true\n throw err\n } finally {\n emit()\n }\n },\n }\n}\n\n/**\n * Step the streaming state machine one event forward. Pure on the\n * `state` argument (mutates it in place). Unknown event types pass\n * through silently — Anthropic adds new event types over time and\n * we don't want a new event class to break capture.\n */\nfunction applyEvent(state: StreamState, ev: StreamEvent): void {\n switch (ev.type) {\n case 'message_start': {\n const e = ev as StreamMessageStart\n if (e.message?.model && !state.modelFromResponse) {\n state.modelFromResponse = e.message.model\n }\n if (e.message?.usage) {\n // message_start carries input_tokens + cache fields. We\n // seed `usage` here so the final tokens object includes\n // cache numbers even if message_delta only overwrites\n // output_tokens.\n state.usage = { ...(state.usage ?? {}), ...e.message.usage }\n }\n return\n }\n case 'content_block_start': {\n const e = ev as StreamContentBlockStart\n if (e.content_block?.type === 'tool_use') {\n const tu = e.content_block as ToolUseContentBlock\n state.toolBlocks.set(e.index, {\n id: typeof tu.id === 'string' ? tu.id : '',\n name: typeof tu.name === 'string' ? tu.name : '',\n arguments: '',\n })\n }\n return\n }\n case 'content_block_delta': {\n const e = ev as StreamContentBlockDelta\n const d = e.delta as { type: string; [k: string]: unknown }\n if (d.type === 'text_delta' && typeof d.text === 'string') {\n state.aggregatedText += d.text\n } else if (\n d.type === 'input_json_delta' &&\n typeof d.partial_json === 'string'\n ) {\n const entry = state.toolBlocks.get(e.index)\n if (entry) entry.arguments += d.partial_json\n }\n return\n }\n case 'message_delta': {\n const e = ev as StreamMessageDelta\n if (e.delta?.stop_reason && state.finishReason === null) {\n state.finishReason = e.delta.stop_reason\n }\n if (e.usage) {\n // message_delta only carries the final output_tokens. Merge,\n // don't replace, so we keep input_tokens + cache fields from\n // message_start.\n state.usage = { ...(state.usage ?? {}), ...e.usage }\n }\n return\n }\n // content_block_stop, message_stop, and unknown future event\n // types pass through without state updates.\n }\n}\n\nfunction snapshotTools(\n acc: Map<number, CapturedToolCall>,\n): CapturedToolCall[] | null {\n if (acc.size === 0) return null\n const entries = [...acc.entries()].sort(([a], [b]) => a - b)\n const out = entries.map(([, v]) => v).filter((t) => t.name.length > 0)\n return out.length > 0 ? out : null\n}\n","/**\n * `wrapAnthropic` — the public entrypoint of @voightxyz/anthropic.\n *\n * Layered `Proxy`: level 0 intercepts the `messages` property,\n * level 1 intercepts the `create` function on it. Everything outside\n * the `client.messages.create` path passes through untouched via\n * `Reflect.get`, so the legacy `completions` namespace, the model\n * list endpoints, batch endpoints, and any future SDK additions\n * keep working with zero special-casing.\n *\n * The proxy is one level shallower than the openai port because\n * Anthropic exposes `messages` at the top level (no intermediate\n * `chat` namespace).\n *\n * Failure modes are intentionally non-fatal — same contract as\n * @voightxyz/openai:\n *\n * - `enabled: false` → return the original client.\n * - no API key resolves → log a one-line warning and return\n * the original client.\n *\n * Internal `_fetch` and `_env` options exist so tests can drive\n * the network + environment surface without touching globals.\n */\n\nimport type { WrapOptions } from './types.js'\nimport { resolveApiKey, resolveAgent } from './identity.js'\nimport { createIngestClient } from './ingest.js'\nimport {\n instrumentMessages,\n type InstrumentContext,\n} from './instruments/messages.js'\n\ninterface InternalOptions extends WrapOptions {\n _fetch?: typeof fetch\n _env?: Record<string, string | undefined>\n}\n\nconst DEFAULT_API_BASE = 'https://api.voight.xyz'\n\nexport function wrapAnthropic<T extends object>(\n client: T,\n options: WrapOptions = {},\n): T {\n const opts = options as InternalOptions\n\n if (opts.enabled === false) return client\n\n const env = opts._env ?? process.env\n const apiKey = resolveApiKey(\n { voightApiKey: opts.voightApiKey, agent: opts.agent },\n env,\n )\n\n if (apiKey === null) {\n console.warn(\n '[voight] no VOIGHT_KEY resolved — wrapper is a pass-through. ' +\n 'Set process.env.VOIGHT_KEY or pass `voightApiKey` to wrapAnthropic() to enable capture.',\n )\n return client\n }\n\n const agentId = resolveAgent(\n { voightApiKey: opts.voightApiKey, agent: opts.agent },\n env,\n )\n\n const ingest = createIngestClient({\n apiBase: opts.apiBase ?? DEFAULT_API_BASE,\n apiKey,\n fetch: opts._fetch,\n })\n\n const ctx: InstrumentContext = {\n agentId,\n privacy: opts.privacy ?? 'standard',\n ingest,\n now: () => Date.now(),\n }\n\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === 'messages') {\n const messages = Reflect.get(target, prop, receiver)\n return wrapMessages(messages as object, ctx)\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n\nfunction wrapMessages<M extends object>(\n messages: M,\n ctx: InstrumentContext,\n): M {\n return new Proxy(messages, {\n get(target, prop, receiver) {\n if (prop === 'create') {\n const original = Reflect.get(target, prop, receiver) as (\n params: never,\n ) => Promise<unknown>\n // .bind so `this` inside the SDK's `create` stays the real\n // messages instance, not the proxy. Without this the\n // Anthropic SDK loses access to its internal http client.\n return instrumentMessages(\n original.bind(target) as never,\n ctx,\n )\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/wrap.ts","../src/identity.ts","../src/ingest.ts","../src/privacy.ts","../src/instruments/messages.ts"],"sourcesContent":["/**\n * `wrapAnthropic` — the public entrypoint of @voightxyz/anthropic.\n *\n * Layered `Proxy`: level 0 intercepts the `messages` property,\n * level 1 intercepts the `create` function on it. Everything outside\n * the `client.messages.create` path passes through untouched via\n * `Reflect.get`, so the legacy `completions` namespace, the model\n * list endpoints, batch endpoints, and any future SDK additions\n * keep working with zero special-casing.\n *\n * The proxy is one level shallower than the openai port because\n * Anthropic exposes `messages` at the top level (no intermediate\n * `chat` namespace).\n *\n * Failure modes are intentionally non-fatal — same contract as\n * @voightxyz/openai:\n *\n * - `enabled: false` → return the original client.\n * - no API key resolves → log a one-line warning and return\n * the original client.\n *\n * Internal `_fetch` and `_env` options exist so tests can drive\n * the network + environment surface without touching globals.\n */\n\nimport { randomUUID } from 'node:crypto'\n\nimport type { WrapOptions } from './types.js'\nimport { resolveApiKey, resolveAgent } from './identity.js'\nimport { createIngestClient } from './ingest.js'\nimport {\n instrumentMessages,\n type InstrumentContext,\n} from './instruments/messages.js'\n\ninterface InternalOptions extends WrapOptions {\n _fetch?: typeof fetch\n _env?: Record<string, string | undefined>\n}\n\nconst DEFAULT_API_BASE = 'https://api.voight.xyz'\n\nexport function wrapAnthropic<T extends object>(\n client: T,\n options: WrapOptions = {},\n): T {\n const opts = options as InternalOptions\n\n if (opts.enabled === false) return client\n\n const env = opts._env ?? process.env\n const apiKey = resolveApiKey(\n { voightApiKey: opts.voightApiKey, agent: opts.agent },\n env,\n )\n\n if (apiKey === null) {\n console.warn(\n '[voight] no VOIGHT_KEY resolved — wrapper is a pass-through. ' +\n 'Set process.env.VOIGHT_KEY or pass `voightApiKey` to wrapAnthropic() to enable capture.',\n )\n return client\n }\n\n const agentId = resolveAgent(\n { voightApiKey: opts.voightApiKey, agent: opts.agent },\n env,\n )\n\n const ingest = createIngestClient({\n apiBase: opts.apiBase ?? DEFAULT_API_BASE,\n apiKey,\n fetch: opts._fetch,\n })\n\n // sessionId is generated once per wrapper instance. Explicit\n // override wins so callers can scope by user / conversation /\n // request without us second-guessing them.\n const sessionId =\n typeof opts.sessionId === 'string' && opts.sessionId.trim().length > 0\n ? opts.sessionId.trim()\n : randomUUID()\n\n const ctx: InstrumentContext = {\n agentId,\n privacy: opts.privacy ?? 'standard',\n sessionId,\n ingest,\n now: () => Date.now(),\n }\n\n return new Proxy(client, {\n get(target, prop, receiver) {\n if (prop === 'messages') {\n const messages = Reflect.get(target, prop, receiver)\n return wrapMessages(messages as object, ctx)\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n\nfunction wrapMessages<M extends object>(\n messages: M,\n ctx: InstrumentContext,\n): M {\n return new Proxy(messages, {\n get(target, prop, receiver) {\n if (prop === 'create') {\n const original = Reflect.get(target, prop, receiver) as (\n params: never,\n ) => Promise<unknown>\n // .bind so `this` inside the SDK's `create` stays the real\n // messages instance, not the proxy. Without this the\n // Anthropic SDK loses access to its internal http client.\n return instrumentMessages(\n original.bind(target) as never,\n ctx,\n )\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n","/**\n * Identity resolution: API key + agent label.\n *\n * Both helpers are pure — they take the user's options and an `env`\n * record, and return a result. `env` defaults to `process.env` when\n * called bare; passing it explicitly lets tests exercise every\n * fallback path without mutating global state.\n *\n * Resolution priority is fixed and documented at each call site:\n *\n * resolveApiKey: options.voightApiKey → env.VOIGHT_KEY → null\n * resolveAgent: options.agent → env.VOIGHT_AGENT\n * → env.HOSTNAME → 'unknown-agent'\n *\n * Empty and whitespace-only values are treated as missing so a\n * misconfigured env (`VOIGHT_KEY=`) falls through cleanly instead\n * of being mistaken for a real key.\n */\n\nexport interface IdentityOptions {\n voightApiKey?: string | undefined\n agent?: string | undefined\n}\n\ntype Env = Record<string, string | undefined>\n\n/**\n * Trim and return the value, or `null` if it's missing / blank.\n * Centralises the \"empty counts as missing\" rule that both helpers\n * apply at every layer.\n */\nfunction nonBlank(value: string | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length === 0 ? null : trimmed\n}\n\n/**\n * Resolve the Voight API key for outgoing events.\n *\n * Returns `null` when nothing usable is configured. Callers decide\n * how to react — `wrapOpenAI` logs a one-time warning and falls\n * back to a no-op transport, so a missing key never crashes the\n * host app.\n */\nexport function resolveApiKey(\n options: IdentityOptions = {},\n env: Env = process.env,\n): string | null {\n return nonBlank(options.voightApiKey) ?? nonBlank(env.VOIGHT_KEY)\n}\n\n/**\n * Resolve the agent label that groups events in the dashboard.\n *\n * Always returns a non-empty string: when nothing else resolves we\n * emit `'unknown-agent'` so the event is still ingestable and the\n * misconfiguration shows up on screen instead of being silently\n * dropped.\n */\nexport function resolveAgent(\n options: IdentityOptions = {},\n env: Env = process.env,\n): string {\n return (\n nonBlank(options.agent) ??\n nonBlank(env.VOIGHT_AGENT) ??\n nonBlank(env.HOSTNAME) ??\n 'unknown-agent'\n )\n}\n","/**\n * Fire-and-forget HTTP client for the Voight ingest endpoint.\n *\n * Why \"fire and forget\":\n *\n * The wrapper is in the user's app's hot path. A failing or slow\n * Voight backend must NEVER turn into a failing or slow OpenAI\n * call for the user. `send` returns synchronously; the actual\n * POST happens on the microtask queue and any error reaches the\n * optional `onError` hook rather than the caller's stack.\n *\n * We intentionally do not retry, batch, or buffer in beta.1.\n * Those add state and complexity — we'd rather drop the\n * occasional event than ship a half-implemented queue. Retry +\n * buffer arrive in 0.2.0 once we have real-world failure-mode\n * data to design against.\n *\n * Why inject `fetch`:\n *\n * Lets unit tests assert request shape (URL, headers, body)\n * without going to the network and without monkey-patching\n * the global. In production the option is omitted and the\n * runtime's `fetch` (Node 18+) is used.\n */\n\nimport type { EventPayload } from './types.js'\n\nexport interface IngestOptions {\n apiBase: string\n apiKey: string\n /**\n * Optional override for the network call. Tests inject a mock;\n * production callers leave this unset and `globalThis.fetch` is\n * used at dispatch time.\n */\n fetch?: typeof fetch | undefined\n /**\n * Called when a network error or non-2xx response would otherwise\n * be silently dropped. Useful for surfacing misconfiguration\n * (bad key, wrong apiBase) during development. Defaults to a\n * no-op so production stays quiet.\n */\n onError?: ((err: unknown) => void) | undefined\n}\n\nexport interface IngestClient {\n send: (event: EventPayload) => void\n}\n\n/**\n * Build a client bound to a single apiBase + apiKey pair.\n *\n * The returned `send` is synchronous and never throws. Errors are\n * routed to `onError` (a no-op by default).\n */\nexport function createIngestClient(opts: IngestOptions): IngestClient {\n // Normalise the base URL once so callers can pass it with or\n // without a trailing slash and we don't end up with `//v1/events`.\n const base = opts.apiBase.replace(/\\/+$/, '')\n const url = `${base}/v1/events`\n const headers = {\n 'content-type': 'application/json',\n authorization: `Bearer ${opts.apiKey}`,\n }\n const fetchImpl = opts.fetch ?? globalThis.fetch\n const onError = opts.onError ?? (() => {})\n\n return {\n send(event) {\n // Wrap the dispatch in an IIFE so a synchronous throw inside\n // `JSON.stringify` (e.g. circular structure) still ends up\n // at `onError` instead of bubbling to the user's hot path.\n void (async () => {\n try {\n const body = JSON.stringify(event)\n const res = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n })\n if (!res.ok) {\n onError(\n new Error(\n `voight ingest failed: ${res.status} ${res.statusText}`,\n ),\n )\n }\n } catch (err) {\n onError(err)\n }\n })()\n },\n }\n}\n","/**\n * PII scrubbing utilities. Ports the regex catalogue proven in\n * @voightxyz/sdk's privacy.ts (12 patterns + Luhn-validated cards),\n * trimmed to the surface this package needs.\n *\n * `scrubPii` and `scrubAnyValue` are pure: same input produces same\n * output, no I/O, no randomness. Safe to call in a hot path.\n *\n * Why duplicate the catalogue here (rather than depend on the SDK)?\n *\n * v0.1.0-beta.1 ships standalone — no runtime dependency on\n * `@voightxyz/sdk` — so a user installing only `@voightxyz/openai`\n * gets a complete package. If duplication ever becomes painful\n * (a third copy under @voightxyz/anthropic, say), the right move\n * is to extract a private `@voightxyz/core` package; until then,\n * the cost of a 12-pattern table is lower than the cost of a\n * peer-dep + version-skew matrix.\n */\n\n// ─── PII patterns ──────────────────────────────────────────────────\n//\n// Order matters. Multi-line / most-specific patterns run FIRST so\n// that once a span is consumed (e.g. a PEM block), later patterns\n// can't re-match its insides. JWTs run before generic API-key\n// patterns to avoid partial matches.\n//\n// Each pattern has a `name` for unit-test traceability and a `re`\n// that uses the global flag (so `replaceAll` semantics apply).\n\ntype Pattern = {\n name: string\n re: RegExp\n replacement: string\n}\n\nconst RE_PEM_PRIVATE_KEY =\n /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g\n\nconst RE_JWT =\n /\\beyJ[A-Za-z0-9_-]{10,}\\.eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g\n\n// Anthropic must precede the generic OpenAI rule so `sk-ant-...`\n// doesn't get partially consumed as `sk-...`.\nconst RE_ANTHROPIC = /\\bsk-ant-[A-Za-z0-9_-]{40,}\\b/g\n\nconst RE_OPENAI = /\\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\\b/g\n\nconst RE_STRIPE_LIVE = /\\b(?:sk|pk)_live_[A-Za-z0-9]{20,}\\b/g\n\nconst RE_GITHUB_FINE = /\\bgithub_pat_[A-Za-z0-9_]{20,}\\b/g\nconst RE_GITHUB_CLASSIC = /\\bghp_[A-Za-z0-9]{36}\\b/g\n\nconst RE_AWS_ACCESS_KEY = /\\bAKIA[A-Z0-9]{16}\\b/g\n\nconst RE_SLACK = /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g\n\n// Voight's own keys. Defense-in-depth: even if the user accidentally\n// pastes their `vk_…` into a prompt, it never leaves the process in\n// the clear under standard privacy.\nconst RE_VOIGHT = /\\bvk_[A-Za-z0-9_-]{32,}\\b/g\n\n// Strict email: requires a TLD with ≥2 letters. `support@app`\n// (no TLD) and `email_template` (no @) do not match.\nconst RE_EMAIL = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g\n\n// Phone in E.164. Looser variants produce too many false positives\n// over order numbers and identifiers.\nconst RE_PHONE_E164 = /\\+\\d{10,15}\\b/g\n\nconst KEY_PATTERNS: readonly Pattern[] = [\n { name: 'pem-private-key', re: RE_PEM_PRIVATE_KEY, replacement: '[REDACTED-PRIVATE-KEY]' },\n { name: 'jwt', re: RE_JWT, replacement: '[REDACTED-JWT]' },\n { name: 'anthropic-key', re: RE_ANTHROPIC, replacement: '[REDACTED-API-KEY]' },\n { name: 'openai-key', re: RE_OPENAI, replacement: '[REDACTED-API-KEY]' },\n { name: 'stripe-live-key', re: RE_STRIPE_LIVE, replacement: '[REDACTED-API-KEY]' },\n { name: 'github-fine-pat', re: RE_GITHUB_FINE, replacement: '[REDACTED-API-KEY]' },\n { name: 'github-classic-pat', re: RE_GITHUB_CLASSIC, replacement: '[REDACTED-API-KEY]' },\n { name: 'aws-access-key', re: RE_AWS_ACCESS_KEY, replacement: '[REDACTED-API-KEY]' },\n { name: 'slack-token', re: RE_SLACK, replacement: '[REDACTED-API-KEY]' },\n { name: 'voight-key', re: RE_VOIGHT, replacement: '[REDACTED-API-KEY]' },\n { name: 'email', re: RE_EMAIL, replacement: '[REDACTED-EMAIL]' },\n { name: 'phone-e164', re: RE_PHONE_E164, replacement: '[REDACTED-PHONE]' },\n]\n\n// ─── Credit cards ─────────────────────────────────────────────────\n//\n// Credit-card numbers need Luhn validation to avoid false positives\n// over long numeric ID strings, so they don't fit the simple\n// {re, replacement} table.\n\n/**\n * Validate a digit string against the Luhn checksum used by all\n * major card brands. Returns false on empty / non-digit input.\n */\nexport function luhnValid(digits: string): boolean {\n if (digits.length === 0) return false\n let sum = 0\n let alternate = false\n for (let i = digits.length - 1; i >= 0; i--) {\n const ch = digits.charCodeAt(i)\n if (ch < 48 || ch > 57) return false\n let n = ch - 48\n if (alternate) {\n n *= 2\n if (n > 9) n -= 9\n }\n sum += n\n alternate = !alternate\n }\n return sum > 0 && sum % 10 === 0\n}\n\n// 13–19 digits, optionally with single spaces or dashes between\n// groups. Anchored with \\b so we don't match the middle of a longer\n// digit string.\nconst RE_CARD_CANDIDATE = /\\b(?:\\d[ -]?){12,18}\\d\\b/g\n\nfunction scrubCreditCards(input: string): string {\n return input.replace(RE_CARD_CANDIDATE, (match) => {\n const digits = match.replace(/[ -]/g, '')\n if (digits.length < 13 || digits.length > 19) return match\n if (!luhnValid(digits)) return match\n return '[REDACTED-CARD]'\n })\n}\n\n/**\n * Run the credential / PII patterns over a string. Pure, idempotent\n * (already-scrubbed text stays stable), no I/O. Non-string input\n * is returned unchanged so this function is safe to call inside\n * the recursive walker.\n */\nexport function scrubPii(input: string): string {\n // The non-string branch matters: `scrubAnyValue` calls this on\n // unknown values, and we'd rather return the raw value than crash.\n if (typeof input !== 'string' || input.length === 0) return input\n let out = input\n for (const { re, replacement } of KEY_PATTERNS) {\n // Pin a fresh RegExp instance per call: `g`-flagged regexes\n // carry mutable `lastIndex` state, and although `replace` does\n // not depend on it, future Worker-based callers might.\n out = out.replace(new RegExp(re.source, re.flags), replacement)\n }\n out = scrubCreditCards(out)\n return out\n}\n\n/**\n * Recursively scrub every string leaf in a JSON-like value.\n * Non-string primitives, undefined, arrays, and plain objects are\n * walked structurally. Anything else (Date, Map, Set, etc.) is\n * returned unchanged — this package never serialises those.\n *\n * Returns a fresh value; the input is never mutated.\n */\nexport function scrubAnyValue(value: unknown): unknown {\n if (typeof value === 'string') return scrubPii(value)\n if (Array.isArray(value)) return value.map(scrubAnyValue)\n if (value !== null && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = scrubAnyValue(v)\n }\n return out\n }\n return value\n}\n","/**\n * Instrument for `client.messages.create`.\n *\n * Wraps the Anthropic SDK's primary Messages endpoint. Handles both\n * transports:\n *\n * - Non-streaming: pulls text from `content[].text` blocks, tool\n * calls from `content[].tool_use` blocks (input is unknown JSON,\n * we serialise via JSON.stringify so the wire format matches the\n * openai wrapper's `arguments` string).\n *\n * - Streaming: a state machine over the event sequence\n * `message_start` → `content_block_start` → many\n * `content_block_delta` → `content_block_stop` → `message_delta`\n * → `message_stop`. We maintain a per-block aggregator keyed by\n * `index`. Text deltas concatenate `delta.text`. Tool-use deltas\n * carry `delta.partial_json` fragments that we append to that\n * block's `arguments` string. The final `message_delta` carries\n * the output_tokens; the initial `message_start` carries the\n * input_tokens + the two cache fields.\n *\n * Token surface emitted on `metadata.tokens`:\n * - input, output, total: always\n * - cache_read: only when `cache_read_input_tokens > 0`\n * - cache_creation: only when `cache_creation_input_tokens > 0`\n *\n * Privacy fan-out: identical contract to @voightxyz/openai\n * (`minimal` → tags only, `standard` → scrubbed content,\n * `full` → verbatim). The first tool call's name flows into\n * `toolExecuted` at every level so the audit-log DETAIL column\n * keeps rendering meaningfully even under minimal.\n */\n\nimport type { EventPayload, PrivacyLevel } from '../types.js'\nimport { scrubAnyValue, scrubPii } from '../privacy.js'\n\n// ─── Loose Anthropic types ────────────────────────────────────────\n//\n// We model only the surface this instrument actually reads. Keeping\n// the types loose insulates us from upstream SDK changes that don't\n// affect our wire shape.\n\ninterface MessageCreateParams {\n model: string\n max_tokens: number\n messages: Array<Record<string, unknown>>\n stream?: boolean\n [k: string]: unknown\n}\n\ninterface AnthropicUsage {\n input_tokens?: number\n output_tokens?: number\n cache_creation_input_tokens?: number | null\n cache_read_input_tokens?: number | null\n [k: string]: unknown\n}\n\ninterface TextContentBlock {\n type: 'text'\n text: string\n}\n\ninterface ToolUseContentBlock {\n type: 'tool_use'\n id: string\n name: string\n input: unknown\n}\n\ntype ContentBlock =\n | TextContentBlock\n | ToolUseContentBlock\n | { type: string; [k: string]: unknown }\n\ninterface NonStreamingMessage {\n id?: string\n model?: string\n content?: ContentBlock[]\n stop_reason?: string | null\n usage?: AnthropicUsage\n [k: string]: unknown\n}\n\n// Streaming events\ninterface StreamMessageStart {\n type: 'message_start'\n message: NonStreamingMessage\n}\ninterface StreamContentBlockStart {\n type: 'content_block_start'\n index: number\n content_block: ContentBlock\n}\ninterface StreamContentBlockDelta {\n type: 'content_block_delta'\n index: number\n delta:\n | { type: 'text_delta'; text: string }\n | { type: 'input_json_delta'; partial_json: string }\n | { type: string; [k: string]: unknown }\n}\ninterface StreamContentBlockStop {\n type: 'content_block_stop'\n index: number\n}\ninterface StreamMessageDelta {\n type: 'message_delta'\n delta: { stop_reason?: string | null; [k: string]: unknown }\n usage?: { output_tokens?: number; [k: string]: unknown }\n}\ninterface StreamMessageStop {\n type: 'message_stop'\n}\n\ntype StreamEvent =\n | StreamMessageStart\n | StreamContentBlockStart\n | StreamContentBlockDelta\n | StreamContentBlockStop\n | StreamMessageDelta\n | StreamMessageStop\n | { type: string; [k: string]: unknown }\n\ntype CreateFn = (\n params: MessageCreateParams,\n) => Promise<NonStreamingMessage | AsyncIterable<StreamEvent>>\n\n/**\n * Flat tool-call shape we emit. Mirrors the openai wrapper's\n * `metadata.toolCalls[*]` schema so dashboard rendering doesn't\n * need to know which provider produced the event.\n */\ninterface CapturedToolCall {\n id: string\n name: string\n arguments: string\n}\n\ninterface NormalisedTokens {\n input: number\n output: number\n total: number\n cache_read?: number\n cache_creation?: number\n}\n\nexport interface EventSink {\n send: (event: EventPayload) => void\n}\n\nexport interface InstrumentContext {\n agentId: string\n privacy: PrivacyLevel\n /**\n * Trace grouping identifier stamped on every emitted event under\n * `metadata.sessionId`. The wrapper resolves it once per instance\n * (explicit option or auto-generated UUID v4).\n */\n sessionId: string\n ingest: EventSink\n /** Time source in ms; injected so tests can produce deterministic `durationMs`. */\n now: () => number\n}\n\nexport function instrumentMessages(\n original: CreateFn,\n ctx: InstrumentContext,\n): CreateFn {\n return async function wrappedCreate(params: MessageCreateParams) {\n const startedAt = ctx.now()\n const isStream = params.stream === true\n\n let result: NonStreamingMessage | AsyncIterable<StreamEvent>\n try {\n result = await original(params)\n } catch (err) {\n ctx.ingest.send(\n buildFailureEvent({ ctx, params, startedAt, error: err }),\n )\n throw err\n }\n\n if (!isStream) {\n ctx.ingest.send(\n buildSuccessEvent({\n ctx,\n params,\n startedAt,\n response: result as NonStreamingMessage,\n }),\n )\n return result\n }\n\n return wrapStream(\n result as AsyncIterable<StreamEvent>,\n ctx,\n params,\n startedAt,\n )\n }\n}\n\n// ─── Event builders ──────────────────────────────────────────────\n\nfunction buildSuccessEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n response: NonStreamingMessage\n}): EventPayload {\n const { ctx, params, startedAt, response } = args\n const responseText = extractText(response.content ?? [])\n const toolCalls = extractToolCalls(response.content ?? [])\n const tokens = normaliseTokens(response.usage)\n const durationMs = ctx.now() - startedAt\n\n return assembleEvent({\n ctx,\n params,\n durationMs,\n outcome: 'success',\n responseText: responseText.length > 0 ? responseText : undefined,\n tokens,\n toolCalls,\n streaming: false,\n finishReason: response.stop_reason ?? null,\n modelFromResponse: response.model,\n })\n}\n\nfunction buildFailureEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n error: unknown\n}): EventPayload {\n const { ctx, params, startedAt, error } = args\n const durationMs = ctx.now() - startedAt\n const message = error instanceof Error ? error.message : String(error)\n return assembleEvent({\n ctx,\n params,\n durationMs,\n outcome: 'failed',\n streaming: params.stream === true,\n errorMessage: message,\n })\n}\n\nfunction buildStreamEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n startedAt: number\n aggregatedText: string\n tokens: NormalisedTokens | null\n toolCalls: CapturedToolCall[] | null\n modelFromResponse: string | undefined\n finishReason: string | null\n}): EventPayload {\n return assembleEvent({\n ctx: args.ctx,\n params: args.params,\n durationMs: args.ctx.now() - args.startedAt,\n outcome: 'success',\n responseText:\n args.aggregatedText.length > 0 ? args.aggregatedText : undefined,\n tokens: args.tokens,\n toolCalls: args.toolCalls,\n streaming: true,\n finishReason: args.finishReason,\n modelFromResponse: args.modelFromResponse,\n })\n}\n\n/**\n * Single assembler — privacy fan-out + payload shape in one place\n * so the three callers above can't drift apart.\n */\nfunction assembleEvent(args: {\n ctx: InstrumentContext\n params: MessageCreateParams\n durationMs: number\n outcome: 'success' | 'failed'\n responseText?: string | undefined\n tokens?: NormalisedTokens | null\n toolCalls?: CapturedToolCall[] | null\n streaming: boolean\n finishReason?: string | null\n errorMessage?: string\n modelFromResponse?: string | undefined\n}): EventPayload {\n const { ctx, params, durationMs, outcome, streaming, errorMessage } = args\n const tokens = args.tokens ?? null\n const toolCalls = args.toolCalls ?? null\n const model = args.modelFromResponse ?? params.model\n\n const metadata: Record<string, unknown> = {\n source: 'anthropic-sdk',\n privacyLevel: ctx.privacy,\n streaming,\n sessionId: ctx.sessionId,\n }\n if (tokens) metadata.tokens = tokens\n if (args.finishReason !== undefined && args.finishReason !== null) {\n metadata.finishReason = args.finishReason\n }\n\n const firstToolName =\n toolCalls && toolCalls.length > 0 ? toolCalls[0]!.name : undefined\n\n if (ctx.privacy === 'minimal') {\n return {\n agentId: ctx.agentId,\n type: 'reasoning',\n model,\n durationMs,\n outcome,\n ...(firstToolName ? { toolExecuted: firstToolName } : {}),\n metadata,\n ...(errorMessage ? { errorMessage } : {}),\n }\n }\n\n const messages =\n ctx.privacy === 'standard'\n ? (scrubAnyValue(params.messages) as MessageCreateParams['messages'])\n : params.messages\n\n const responseText = args.responseText\n const scrubbedResponse =\n responseText !== undefined\n ? ctx.privacy === 'standard'\n ? scrubPii(responseText)\n : responseText\n : undefined\n if (scrubbedResponse !== undefined) {\n metadata.responseText = scrubbedResponse\n }\n\n if (toolCalls && toolCalls.length > 0) {\n metadata.toolCalls =\n ctx.privacy === 'standard'\n ? toolCalls.map((t) => ({\n id: t.id,\n name: t.name,\n arguments: scrubPii(t.arguments),\n }))\n : toolCalls\n }\n\n return {\n agentId: ctx.agentId,\n type: 'reasoning',\n model,\n durationMs,\n outcome,\n ...(firstToolName ? { toolExecuted: firstToolName } : {}),\n input: { messages },\n metadata,\n ...(errorMessage ? { errorMessage } : {}),\n }\n}\n\n// ─── Non-streaming helpers ──────────────────────────────────────\n\nfunction extractText(content: ContentBlock[]): string {\n let out = ''\n for (const block of content) {\n if (block.type === 'text' && typeof (block as TextContentBlock).text === 'string') {\n out += (block as TextContentBlock).text\n }\n }\n return out\n}\n\nfunction extractToolCalls(content: ContentBlock[]): CapturedToolCall[] | null {\n const out: CapturedToolCall[] = []\n for (const block of content) {\n if (block.type !== 'tool_use') continue\n const t = block as ToolUseContentBlock\n out.push({\n id: typeof t.id === 'string' ? t.id : '',\n name: typeof t.name === 'string' ? t.name : '',\n // Serialise the model's chosen tool input to a JSON string so\n // the wire shape matches openai's `arguments: string` exactly.\n // Anthropic gives us a parsed object; openai gives us the raw\n // string. We normalise here.\n arguments: safeStringify(t.input),\n })\n }\n return out.length > 0 ? out : null\n}\n\nfunction safeStringify(v: unknown): string {\n if (typeof v === 'string') return v\n try {\n return JSON.stringify(v ?? {})\n } catch {\n return ''\n }\n}\n\nfunction normaliseTokens(u: AnthropicUsage | undefined): NormalisedTokens | null {\n if (!u) return null\n const input = numberOrZero(u.input_tokens)\n const output = numberOrZero(u.output_tokens)\n const total = input + output\n const cacheRead = numberOrZero(u.cache_read_input_tokens)\n const cacheCreation = numberOrZero(u.cache_creation_input_tokens)\n const base: NormalisedTokens = { input, output, total }\n if (cacheRead > 0) base.cache_read = cacheRead\n if (cacheCreation > 0) base.cache_creation = cacheCreation\n return base\n}\n\nfunction numberOrZero(v: unknown): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : 0\n}\n\n// ─── Streaming wrapper ──────────────────────────────────────────\n\n/**\n * Mutating accumulator for the streaming state machine. We hold a\n * single source of truth across chunks: text aggregator, per-index\n * tool-call entries, latest usage seen, final stop_reason. The wrap\n * function yields each event to the user untouched and only mutates\n * this struct as bytes pass through.\n */\ninterface StreamState {\n aggregatedText: string\n toolBlocks: Map<number, CapturedToolCall>\n usage: AnthropicUsage | null\n modelFromResponse: string | undefined\n finishReason: string | null\n}\n\nfunction wrapStream(\n source: AsyncIterable<StreamEvent>,\n ctx: InstrumentContext,\n params: MessageCreateParams,\n startedAt: number,\n): AsyncIterable<StreamEvent> {\n const state: StreamState = {\n aggregatedText: '',\n toolBlocks: new Map(),\n usage: null,\n modelFromResponse: undefined,\n finishReason: null,\n }\n let emitted = false\n\n function emit() {\n if (emitted) return\n emitted = true\n ctx.ingest.send(\n buildStreamEvent({\n ctx,\n params,\n startedAt,\n aggregatedText: state.aggregatedText,\n tokens: normaliseTokens(state.usage ?? undefined),\n toolCalls: snapshotTools(state.toolBlocks),\n modelFromResponse: state.modelFromResponse,\n finishReason: state.finishReason,\n }),\n )\n }\n\n return {\n async *[Symbol.asyncIterator]() {\n try {\n for await (const ev of source) {\n applyEvent(state, ev)\n yield ev\n }\n } catch (err) {\n ctx.ingest.send(\n buildFailureEvent({ ctx, params, startedAt, error: err }),\n )\n emitted = true\n throw err\n } finally {\n emit()\n }\n },\n }\n}\n\n/**\n * Step the streaming state machine one event forward. Pure on the\n * `state` argument (mutates it in place). Unknown event types pass\n * through silently — Anthropic adds new event types over time and\n * we don't want a new event class to break capture.\n */\nfunction applyEvent(state: StreamState, ev: StreamEvent): void {\n switch (ev.type) {\n case 'message_start': {\n const e = ev as StreamMessageStart\n if (e.message?.model && !state.modelFromResponse) {\n state.modelFromResponse = e.message.model\n }\n if (e.message?.usage) {\n // message_start carries input_tokens + cache fields. We\n // seed `usage` here so the final tokens object includes\n // cache numbers even if message_delta only overwrites\n // output_tokens.\n state.usage = { ...(state.usage ?? {}), ...e.message.usage }\n }\n return\n }\n case 'content_block_start': {\n const e = ev as StreamContentBlockStart\n if (e.content_block?.type === 'tool_use') {\n const tu = e.content_block as ToolUseContentBlock\n state.toolBlocks.set(e.index, {\n id: typeof tu.id === 'string' ? tu.id : '',\n name: typeof tu.name === 'string' ? tu.name : '',\n arguments: '',\n })\n }\n return\n }\n case 'content_block_delta': {\n const e = ev as StreamContentBlockDelta\n const d = e.delta as { type: string; [k: string]: unknown }\n if (d.type === 'text_delta' && typeof d.text === 'string') {\n state.aggregatedText += d.text\n } else if (\n d.type === 'input_json_delta' &&\n typeof d.partial_json === 'string'\n ) {\n const entry = state.toolBlocks.get(e.index)\n if (entry) entry.arguments += d.partial_json\n }\n return\n }\n case 'message_delta': {\n const e = ev as StreamMessageDelta\n if (e.delta?.stop_reason && state.finishReason === null) {\n state.finishReason = e.delta.stop_reason\n }\n if (e.usage) {\n // message_delta only carries the final output_tokens. Merge,\n // don't replace, so we keep input_tokens + cache fields from\n // message_start.\n state.usage = { ...(state.usage ?? {}), ...e.usage }\n }\n return\n }\n // content_block_stop, message_stop, and unknown future event\n // types pass through without state updates.\n }\n}\n\nfunction snapshotTools(\n acc: Map<number, CapturedToolCall>,\n): CapturedToolCall[] | null {\n if (acc.size === 0) return null\n const entries = [...acc.entries()].sort(([a], [b]) => a - b)\n const out = entries.map(([, v]) => v).filter((t) => t.name.length > 0)\n return out.length > 0 ? out : null\n}\n"],"mappings":";AAyBA,SAAS,kBAAkB;;;ACM3B,SAAS,SAAS,OAA0C;AAC1D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,WAAW,IAAI,OAAO;AACvC;AAUO,SAAS,cACd,UAA2B,CAAC,GAC5B,MAAW,QAAQ,KACJ;AACf,SAAO,SAAS,QAAQ,YAAY,KAAK,SAAS,IAAI,UAAU;AAClE;AAUO,SAAS,aACd,UAA2B,CAAC,GAC5B,MAAW,QAAQ,KACX;AACR,SACE,SAAS,QAAQ,KAAK,KACtB,SAAS,IAAI,YAAY,KACzB,SAAS,IAAI,QAAQ,KACrB;AAEJ;;;ACfO,SAAS,mBAAmB,MAAmC;AAGpE,QAAM,OAAO,KAAK,QAAQ,QAAQ,QAAQ,EAAE;AAC5C,QAAM,MAAM,GAAG,IAAI;AACnB,QAAM,UAAU;AAAA,IACd,gBAAgB;AAAA,IAChB,eAAe,UAAU,KAAK,MAAM;AAAA,EACtC;AACA,QAAM,YAAY,KAAK,SAAS,WAAW;AAC3C,QAAM,UAAU,KAAK,YAAY,MAAM;AAAA,EAAC;AAExC,SAAO;AAAA,IACL,KAAK,OAAO;AAIV,YAAM,YAAY;AAChB,YAAI;AACF,gBAAM,OAAO,KAAK,UAAU,KAAK;AACjC,gBAAM,MAAM,MAAM,UAAU,KAAK;AAAA,YAC/B,QAAQ;AAAA,YACR;AAAA,YACA;AAAA,UACF,CAAC;AACD,cAAI,CAAC,IAAI,IAAI;AACX;AAAA,cACE,IAAI;AAAA,gBACF,yBAAyB,IAAI,MAAM,IAAI,IAAI,UAAU;AAAA,cACvD;AAAA,YACF;AAAA,UACF;AAAA,QACF,SAAS,KAAK;AACZ,kBAAQ,GAAG;AAAA,QACb;AAAA,MACF,GAAG;AAAA,IACL;AAAA,EACF;AACF;;;AC1DA,IAAM,qBACJ;AAEF,IAAM,SACJ;AAIF,IAAM,eAAe;AAErB,IAAM,YAAY;AAElB,IAAM,iBAAiB;AAEvB,IAAM,iBAAiB;AACvB,IAAM,oBAAoB;AAE1B,IAAM,oBAAoB;AAE1B,IAAM,WAAW;AAKjB,IAAM,YAAY;AAIlB,IAAM,WAAW;AAIjB,IAAM,gBAAgB;AAEtB,IAAM,eAAmC;AAAA,EACvC,EAAE,MAAM,mBAAmB,IAAI,oBAAoB,aAAa,yBAAyB;AAAA,EACzF,EAAE,MAAM,OAAO,IAAI,QAAQ,aAAa,iBAAiB;AAAA,EACzD,EAAE,MAAM,iBAAiB,IAAI,cAAc,aAAa,qBAAqB;AAAA,EAC7E,EAAE,MAAM,cAAc,IAAI,WAAW,aAAa,qBAAqB;AAAA,EACvE,EAAE,MAAM,mBAAmB,IAAI,gBAAgB,aAAa,qBAAqB;AAAA,EACjF,EAAE,MAAM,mBAAmB,IAAI,gBAAgB,aAAa,qBAAqB;AAAA,EACjF,EAAE,MAAM,sBAAsB,IAAI,mBAAmB,aAAa,qBAAqB;AAAA,EACvF,EAAE,MAAM,kBAAkB,IAAI,mBAAmB,aAAa,qBAAqB;AAAA,EACnF,EAAE,MAAM,eAAe,IAAI,UAAU,aAAa,qBAAqB;AAAA,EACvE,EAAE,MAAM,cAAc,IAAI,WAAW,aAAa,qBAAqB;AAAA,EACvE,EAAE,MAAM,SAAS,IAAI,UAAU,aAAa,mBAAmB;AAAA,EAC/D,EAAE,MAAM,cAAc,IAAI,eAAe,aAAa,mBAAmB;AAC3E;AAYO,SAAS,UAAU,QAAyB;AACjD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,MAAI,MAAM;AACV,MAAI,YAAY;AAChB,WAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,UAAM,KAAK,OAAO,WAAW,CAAC;AAC9B,QAAI,KAAK,MAAM,KAAK,GAAI,QAAO;AAC/B,QAAI,IAAI,KAAK;AACb,QAAI,WAAW;AACb,WAAK;AACL,UAAI,IAAI,EAAG,MAAK;AAAA,IAClB;AACA,WAAO;AACP,gBAAY,CAAC;AAAA,EACf;AACA,SAAO,MAAM,KAAK,MAAM,OAAO;AACjC;AAKA,IAAM,oBAAoB;AAE1B,SAAS,iBAAiB,OAAuB;AAC/C,SAAO,MAAM,QAAQ,mBAAmB,CAAC,UAAU;AACjD,UAAM,SAAS,MAAM,QAAQ,SAAS,EAAE;AACxC,QAAI,OAAO,SAAS,MAAM,OAAO,SAAS,GAAI,QAAO;AACrD,QAAI,CAAC,UAAU,MAAM,EAAG,QAAO;AAC/B,WAAO;AAAA,EACT,CAAC;AACH;AAQO,SAAS,SAAS,OAAuB;AAG9C,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,MAAI,MAAM;AACV,aAAW,EAAE,IAAI,YAAY,KAAK,cAAc;AAI9C,UAAM,IAAI,QAAQ,IAAI,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,WAAW;AAAA,EAChE;AACA,QAAM,iBAAiB,GAAG;AAC1B,SAAO;AACT;AAUO,SAAS,cAAc,OAAyB;AACrD,MAAI,OAAO,UAAU,SAAU,QAAO,SAAS,KAAK;AACpD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,aAAa;AACxD,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACrE,UAAI,CAAC,IAAI,cAAc,CAAC;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;ACDO,SAAS,mBACd,UACA,KACU;AACV,SAAO,eAAe,cAAc,QAA6B;AAC/D,UAAM,YAAY,IAAI,IAAI;AAC1B,UAAM,WAAW,OAAO,WAAW;AAEnC,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,SAAS,MAAM;AAAA,IAChC,SAAS,KAAK;AACZ,UAAI,OAAO;AAAA,QACT,kBAAkB,EAAE,KAAK,QAAQ,WAAW,OAAO,IAAI,CAAC;AAAA,MAC1D;AACA,YAAM;AAAA,IACR;AAEA,QAAI,CAAC,UAAU;AACb,UAAI,OAAO;AAAA,QACT,kBAAkB;AAAA,UAChB;AAAA,UACA;AAAA,UACA;AAAA,UACA,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AACA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAIA,SAAS,kBAAkB,MAKV;AACf,QAAM,EAAE,KAAK,QAAQ,WAAW,SAAS,IAAI;AAC7C,QAAM,eAAe,YAAY,SAAS,WAAW,CAAC,CAAC;AACvD,QAAM,YAAY,iBAAiB,SAAS,WAAW,CAAC,CAAC;AACzD,QAAM,SAAS,gBAAgB,SAAS,KAAK;AAC7C,QAAM,aAAa,IAAI,IAAI,IAAI;AAE/B,SAAO,cAAc;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,cAAc,aAAa,SAAS,IAAI,eAAe;AAAA,IACvD;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,cAAc,SAAS,eAAe;AAAA,IACtC,mBAAmB,SAAS;AAAA,EAC9B,CAAC;AACH;AAEA,SAAS,kBAAkB,MAKV;AACf,QAAM,EAAE,KAAK,QAAQ,WAAW,MAAM,IAAI;AAC1C,QAAM,aAAa,IAAI,IAAI,IAAI;AAC/B,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAO,cAAc;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,WAAW,OAAO,WAAW;AAAA,IAC7B,cAAc;AAAA,EAChB,CAAC;AACH;AAEA,SAAS,iBAAiB,MAST;AACf,SAAO,cAAc;AAAA,IACnB,KAAK,KAAK;AAAA,IACV,QAAQ,KAAK;AAAA,IACb,YAAY,KAAK,IAAI,IAAI,IAAI,KAAK;AAAA,IAClC,SAAS;AAAA,IACT,cACE,KAAK,eAAe,SAAS,IAAI,KAAK,iBAAiB;AAAA,IACzD,QAAQ,KAAK;AAAA,IACb,WAAW,KAAK;AAAA,IAChB,WAAW;AAAA,IACX,cAAc,KAAK;AAAA,IACnB,mBAAmB,KAAK;AAAA,EAC1B,CAAC;AACH;AAMA,SAAS,cAAc,MAYN;AACf,QAAM,EAAE,KAAK,QAAQ,YAAY,SAAS,WAAW,aAAa,IAAI;AACtE,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,QAAQ,KAAK,qBAAqB,OAAO;AAE/C,QAAM,WAAoC;AAAA,IACxC,QAAQ;AAAA,IACR,cAAc,IAAI;AAAA,IAClB;AAAA,IACA,WAAW,IAAI;AAAA,EACjB;AACA,MAAI,OAAQ,UAAS,SAAS;AAC9B,MAAI,KAAK,iBAAiB,UAAa,KAAK,iBAAiB,MAAM;AACjE,aAAS,eAAe,KAAK;AAAA,EAC/B;AAEA,QAAM,gBACJ,aAAa,UAAU,SAAS,IAAI,UAAU,CAAC,EAAG,OAAO;AAE3D,MAAI,IAAI,YAAY,WAAW;AAC7B,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAI,gBAAgB,EAAE,cAAc,cAAc,IAAI,CAAC;AAAA,MACvD;AAAA,MACA,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,IACzC;AAAA,EACF;AAEA,QAAM,WACJ,IAAI,YAAY,aACX,cAAc,OAAO,QAAQ,IAC9B,OAAO;AAEb,QAAM,eAAe,KAAK;AAC1B,QAAM,mBACJ,iBAAiB,SACb,IAAI,YAAY,aACd,SAAS,YAAY,IACrB,eACF;AACN,MAAI,qBAAqB,QAAW;AAClC,aAAS,eAAe;AAAA,EAC1B;AAEA,MAAI,aAAa,UAAU,SAAS,GAAG;AACrC,aAAS,YACP,IAAI,YAAY,aACZ,UAAU,IAAI,CAAC,OAAO;AAAA,MACpB,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,WAAW,SAAS,EAAE,SAAS;AAAA,IACjC,EAAE,IACF;AAAA,EACR;AAEA,SAAO;AAAA,IACL,SAAS,IAAI;AAAA,IACb,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,gBAAgB,EAAE,cAAc,cAAc,IAAI,CAAC;AAAA,IACvD,OAAO,EAAE,SAAS;AAAA,IAClB;AAAA,IACA,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,EACzC;AACF;AAIA,SAAS,YAAY,SAAiC;AACpD,MAAI,MAAM;AACV,aAAW,SAAS,SAAS;AAC3B,QAAI,MAAM,SAAS,UAAU,OAAQ,MAA2B,SAAS,UAAU;AACjF,aAAQ,MAA2B;AAAA,IACrC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,SAAoD;AAC5E,QAAM,MAA0B,CAAC;AACjC,aAAW,SAAS,SAAS;AAC3B,QAAI,MAAM,SAAS,WAAY;AAC/B,UAAM,IAAI;AACV,QAAI,KAAK;AAAA,MACP,IAAI,OAAO,EAAE,OAAO,WAAW,EAAE,KAAK;AAAA,MACtC,MAAM,OAAO,EAAE,SAAS,WAAW,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,MAK5C,WAAW,cAAc,EAAE,KAAK;AAAA,IAClC,CAAC;AAAA,EACH;AACA,SAAO,IAAI,SAAS,IAAI,MAAM;AAChC;AAEA,SAAS,cAAc,GAAoB;AACzC,MAAI,OAAO,MAAM,SAAU,QAAO;AAClC,MAAI;AACF,WAAO,KAAK,UAAU,KAAK,CAAC,CAAC;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,gBAAgB,GAAwD;AAC/E,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,QAAQ,aAAa,EAAE,YAAY;AACzC,QAAM,SAAS,aAAa,EAAE,aAAa;AAC3C,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,aAAa,EAAE,uBAAuB;AACxD,QAAM,gBAAgB,aAAa,EAAE,2BAA2B;AAChE,QAAM,OAAyB,EAAE,OAAO,QAAQ,MAAM;AACtD,MAAI,YAAY,EAAG,MAAK,aAAa;AACrC,MAAI,gBAAgB,EAAG,MAAK,iBAAiB;AAC7C,SAAO;AACT;AAEA,SAAS,aAAa,GAAoB;AACxC,SAAO,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,IAAI,IAAI;AAC3D;AAmBA,SAAS,WACP,QACA,KACA,QACA,WAC4B;AAC5B,QAAM,QAAqB;AAAA,IACzB,gBAAgB;AAAA,IAChB,YAAY,oBAAI,IAAI;AAAA,IACpB,OAAO;AAAA,IACP,mBAAmB;AAAA,IACnB,cAAc;AAAA,EAChB;AACA,MAAI,UAAU;AAEd,WAAS,OAAO;AACd,QAAI,QAAS;AACb,cAAU;AACV,QAAI,OAAO;AAAA,MACT,iBAAiB;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA,gBAAgB,MAAM;AAAA,QACtB,QAAQ,gBAAgB,MAAM,SAAS,MAAS;AAAA,QAChD,WAAW,cAAc,MAAM,UAAU;AAAA,QACzC,mBAAmB,MAAM;AAAA,QACzB,cAAc,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ,OAAO,aAAa,IAAI;AAC9B,UAAI;AACF,yBAAiB,MAAM,QAAQ;AAC7B,qBAAW,OAAO,EAAE;AACpB,gBAAM;AAAA,QACR;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,OAAO;AAAA,UACT,kBAAkB,EAAE,KAAK,QAAQ,WAAW,OAAO,IAAI,CAAC;AAAA,QAC1D;AACA,kBAAU;AACV,cAAM;AAAA,MACR,UAAE;AACA,aAAK;AAAA,MACP;AAAA,IACF;AAAA,EACF;AACF;AAQA,SAAS,WAAW,OAAoB,IAAuB;AAC7D,UAAQ,GAAG,MAAM;AAAA,IACf,KAAK,iBAAiB;AACpB,YAAM,IAAI;AACV,UAAI,EAAE,SAAS,SAAS,CAAC,MAAM,mBAAmB;AAChD,cAAM,oBAAoB,EAAE,QAAQ;AAAA,MACtC;AACA,UAAI,EAAE,SAAS,OAAO;AAKpB,cAAM,QAAQ,EAAE,GAAI,MAAM,SAAS,CAAC,GAAI,GAAG,EAAE,QAAQ,MAAM;AAAA,MAC7D;AACA;AAAA,IACF;AAAA,IACA,KAAK,uBAAuB;AAC1B,YAAM,IAAI;AACV,UAAI,EAAE,eAAe,SAAS,YAAY;AACxC,cAAM,KAAK,EAAE;AACb,cAAM,WAAW,IAAI,EAAE,OAAO;AAAA,UAC5B,IAAI,OAAO,GAAG,OAAO,WAAW,GAAG,KAAK;AAAA,UACxC,MAAM,OAAO,GAAG,SAAS,WAAW,GAAG,OAAO;AAAA,UAC9C,WAAW;AAAA,QACb,CAAC;AAAA,MACH;AACA;AAAA,IACF;AAAA,IACA,KAAK,uBAAuB;AAC1B,YAAM,IAAI;AACV,YAAM,IAAI,EAAE;AACZ,UAAI,EAAE,SAAS,gBAAgB,OAAO,EAAE,SAAS,UAAU;AACzD,cAAM,kBAAkB,EAAE;AAAA,MAC5B,WACE,EAAE,SAAS,sBACX,OAAO,EAAE,iBAAiB,UAC1B;AACA,cAAM,QAAQ,MAAM,WAAW,IAAI,EAAE,KAAK;AAC1C,YAAI,MAAO,OAAM,aAAa,EAAE;AAAA,MAClC;AACA;AAAA,IACF;AAAA,IACA,KAAK,iBAAiB;AACpB,YAAM,IAAI;AACV,UAAI,EAAE,OAAO,eAAe,MAAM,iBAAiB,MAAM;AACvD,cAAM,eAAe,EAAE,MAAM;AAAA,MAC/B;AACA,UAAI,EAAE,OAAO;AAIX,cAAM,QAAQ,EAAE,GAAI,MAAM,SAAS,CAAC,GAAI,GAAG,EAAE,MAAM;AAAA,MACrD;AACA;AAAA,IACF;AAAA,EAGF;AACF;AAEA,SAAS,cACP,KAC2B;AAC3B,MAAI,IAAI,SAAS,EAAG,QAAO;AAC3B,QAAM,UAAU,CAAC,GAAG,IAAI,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC;AAC3D,QAAM,MAAM,QAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,SAAS,CAAC;AACrE,SAAO,IAAI,SAAS,IAAI,MAAM;AAChC;;;AJ3gBA,IAAM,mBAAmB;AAElB,SAAS,cACd,QACA,UAAuB,CAAC,GACrB;AACH,QAAM,OAAO;AAEb,MAAI,KAAK,YAAY,MAAO,QAAO;AAEnC,QAAM,MAAM,KAAK,QAAQ,QAAQ;AACjC,QAAM,SAAS;AAAA,IACb,EAAE,cAAc,KAAK,cAAc,OAAO,KAAK,MAAM;AAAA,IACrD;AAAA,EACF;AAEA,MAAI,WAAW,MAAM;AACnB,YAAQ;AAAA,MACN;AAAA,IAEF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU;AAAA,IACd,EAAE,cAAc,KAAK,cAAc,OAAO,KAAK,MAAM;AAAA,IACrD;AAAA,EACF;AAEA,QAAM,SAAS,mBAAmB;AAAA,IAChC,SAAS,KAAK,WAAW;AAAA,IACzB;AAAA,IACA,OAAO,KAAK;AAAA,EACd,CAAC;AAKD,QAAM,YACJ,OAAO,KAAK,cAAc,YAAY,KAAK,UAAU,KAAK,EAAE,SAAS,IACjE,KAAK,UAAU,KAAK,IACpB,WAAW;AAEjB,QAAM,MAAyB;AAAA,IAC7B;AAAA,IACA,SAAS,KAAK,WAAW;AAAA,IACzB;AAAA,IACA;AAAA,IACA,KAAK,MAAM,KAAK,IAAI;AAAA,EACtB;AAEA,SAAO,IAAI,MAAM,QAAQ;AAAA,IACvB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,YAAY;AACvB,cAAM,WAAW,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AACnD,eAAO,aAAa,UAAoB,GAAG;AAAA,MAC7C;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;AAEA,SAAS,aACP,UACA,KACG;AACH,SAAO,IAAI,MAAM,UAAU;AAAA,IACzB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,UAAU;AACrB,cAAM,WAAW,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAMnD,eAAO;AAAA,UACL,SAAS,KAAK,MAAM;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AACA,aAAO,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAAA,IAC3C;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voightxyz/anthropic",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Voight observability for the Anthropic SDK. Wrap your Anthropic client and capture every Messages call — prompts, tokens, costs, cache reads, tool use, latency, errors — surfaced live in the Voight dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"anthropic",
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
"node": ">=18"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
|
-
"access": "public"
|
|
64
|
-
"tag": "beta"
|
|
63
|
+
"access": "public"
|
|
65
64
|
}
|
|
66
65
|
}
|