@tensorcost/sdk 0.6.0 → 1.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +79 -8
  2. package/README.md +245 -129
  3. package/dist/adapters/crewai.d.ts +4 -0
  4. package/dist/adapters/crewai.d.ts.map +1 -0
  5. package/dist/adapters/crewai.js +6 -0
  6. package/dist/adapters/crewai.js.map +1 -0
  7. package/dist/adapters/langchain.d.ts +4 -0
  8. package/dist/adapters/langchain.d.ts.map +1 -0
  9. package/dist/adapters/langchain.js +6 -0
  10. package/dist/adapters/langchain.js.map +1 -0
  11. package/dist/adapters/llamaindex.d.ts +4 -0
  12. package/dist/adapters/llamaindex.d.ts.map +1 -0
  13. package/dist/adapters/llamaindex.js +6 -0
  14. package/dist/adapters/llamaindex.js.map +1 -0
  15. package/dist/adapters/vercel-ai.d.ts +4 -0
  16. package/dist/adapters/vercel-ai.d.ts.map +1 -0
  17. package/dist/adapters/vercel-ai.js +6 -0
  18. package/dist/adapters/vercel-ai.js.map +1 -0
  19. package/dist/admit-client.d.ts +39 -0
  20. package/dist/admit-client.d.ts.map +1 -0
  21. package/dist/admit-client.js +93 -0
  22. package/dist/admit-client.js.map +1 -0
  23. package/dist/applied.d.ts +6 -1
  24. package/dist/applied.d.ts.map +1 -1
  25. package/dist/applied.js +13 -1
  26. package/dist/applied.js.map +1 -1
  27. package/dist/circuit-observations.d.ts +12 -0
  28. package/dist/circuit-observations.d.ts.map +1 -0
  29. package/dist/circuit-observations.js +37 -0
  30. package/dist/circuit-observations.js.map +1 -0
  31. package/dist/circuit.d.ts +4 -1
  32. package/dist/circuit.d.ts.map +1 -1
  33. package/dist/circuit.js +13 -7
  34. package/dist/circuit.js.map +1 -1
  35. package/dist/config.d.ts +2 -3
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +54 -6
  38. package/dist/config.js.map +1 -1
  39. package/dist/decision.d.ts +11 -0
  40. package/dist/decision.d.ts.map +1 -0
  41. package/dist/decision.js +40 -0
  42. package/dist/decision.js.map +1 -0
  43. package/dist/errors.d.ts +23 -3
  44. package/dist/errors.d.ts.map +1 -1
  45. package/dist/errors.js +24 -0
  46. package/dist/errors.js.map +1 -1
  47. package/dist/index.d.ts +9 -3
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +8 -2
  50. package/dist/index.js.map +1 -1
  51. package/dist/layer.d.ts +33 -0
  52. package/dist/layer.d.ts.map +1 -0
  53. package/dist/layer.js +83 -0
  54. package/dist/layer.js.map +1 -0
  55. package/dist/prewarm.d.ts +12 -0
  56. package/dist/prewarm.d.ts.map +1 -0
  57. package/dist/prewarm.js +22 -0
  58. package/dist/prewarm.js.map +1 -0
  59. package/dist/providers/anthropic.d.ts +10 -0
  60. package/dist/providers/anthropic.d.ts.map +1 -1
  61. package/dist/providers/anthropic.js +134 -57
  62. package/dist/providers/anthropic.js.map +1 -1
  63. package/dist/providers/bedrock.d.ts +1 -1
  64. package/dist/providers/bedrock.d.ts.map +1 -1
  65. package/dist/providers/bedrock.js +16 -34
  66. package/dist/providers/bedrock.js.map +1 -1
  67. package/dist/providers/openai.d.ts +10 -0
  68. package/dist/providers/openai.d.ts.map +1 -1
  69. package/dist/providers/openai.js +143 -61
  70. package/dist/providers/openai.js.map +1 -1
  71. package/dist/providers/vertex.d.ts +1 -1
  72. package/dist/providers/vertex.d.ts.map +1 -1
  73. package/dist/providers/vertex.js +18 -27
  74. package/dist/providers/vertex.js.map +1 -1
  75. package/dist/proxy-client.d.ts +30 -7
  76. package/dist/proxy-client.d.ts.map +1 -1
  77. package/dist/proxy-client.js +123 -19
  78. package/dist/proxy-client.js.map +1 -1
  79. package/dist/runtime.d.ts +23 -0
  80. package/dist/runtime.d.ts.map +1 -0
  81. package/dist/runtime.js +60 -0
  82. package/dist/runtime.js.map +1 -0
  83. package/dist/sdk-layer.d.ts +33 -0
  84. package/dist/sdk-layer.d.ts.map +1 -0
  85. package/dist/sdk-layer.js +107 -0
  86. package/dist/sdk-layer.js.map +1 -0
  87. package/dist/transport.d.ts +3 -1
  88. package/dist/transport.d.ts.map +1 -1
  89. package/dist/transport.js +6 -2
  90. package/dist/transport.js.map +1 -1
  91. package/dist/types.d.ts +82 -15
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/version.d.ts +9 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/dist/version.js +10 -0
  96. package/dist/version.js.map +1 -0
  97. package/dist/wrap-overhead.d.ts +32 -0
  98. package/dist/wrap-overhead.d.ts.map +1 -0
  99. package/dist/wrap-overhead.js +60 -0
  100. package/dist/wrap-overhead.js.map +1 -0
  101. package/dist/wrap.d.ts +13 -17
  102. package/dist/wrap.d.ts.map +1 -1
  103. package/dist/wrap.js +59 -74
  104. package/dist/wrap.js.map +1 -1
  105. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,18 +2,89 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- ## 0.6.0 — 2026-08-04
5
+ ## 1.1.0 — 2026-09-19
6
6
 
7
- SDK metadata story — lets you group traces by prompt template or deployment in
8
- the TensorCost dashboard — plus observe-only Google Vertex AI wrap.
7
+ Agent hot-path hardening: fail-open faster on wedged proxy connections, optional
8
+ cold-start warmup, and non-blocking sdk-layer refresh.
9
+
10
+ ### Added
11
+
12
+ **`prewarm()`** — prefetch JWT + console published layer at Lambda/container
13
+ init. Shares the same process-wide runtime cache as `wrap()`.
14
+
15
+ **`headersTimeoutMs`** (default 2s) — separate deadline for proxy response
16
+ headers vs body read (`timeoutMs`, default 60s). Missing headers within the
17
+ budget fail-open to the direct provider without retry.
18
+
19
+ ### Changed
20
+
21
+ **Sdk-layer fetch** — 250ms cap on cold `GET /sdk-layer`; stale-while-revalidate
22
+ after the 30s TTL so refresh never blocks a model call.
23
+
24
+ **Admit** — HTTP timeout tightened to 500ms (still fail-open on errors).
25
+
26
+ **`wrap()`** — background-warms the shared runtime on first use.
27
+
28
+ ### Fixed
29
+
30
+ Body JSON parse no longer leaves an unhandled rejection when the body-read
31
+ deadline fires before `response.json()` settles.
32
+
33
+ ## 1.0.0 — 2026-09-13
34
+
35
+ Major release: control-layer ceilings replace the `appliedMode` boolean.
36
+ Effective layer is always `min(code.maxLayer, console published layer)`.
37
+
38
+ ### Added
39
+
40
+ **Control layers (`maxLayer`)** — `off`, `observe`, `govern`, `steer`, `route`.
41
+ At `govern`, metadata-only `POST /admit` may refuse before the provider is
42
+ called. SDK fetches console ceiling via `GET /sdk-layer` (30s cache).
43
+
44
+ **Wire headers** — `x-tc-sdk-version`, `x-tc-sdk-capabilities`, `x-tc-max-layer`
45
+ on proxied requests; parses `x-tc-decision` on responses.
46
+
47
+ **Framework adapters** — `wrapLangChain`, `wrapLlamaIndex`, `wrapCrewAI`,
48
+ `wrapVercelAi`.
49
+
50
+ **Google Vertex AI (observe-only)** — `@google/genai` clients with
51
+ `vertexai: true`; `models.generateContent` / streaming variants.
52
+
53
+ **Grant intersection** — `maxGrants` caps console data grants from code;
54
+ prompt cache writes gated on `retain_completions`.
55
+
56
+ ### Changed
57
+
58
+ **Breaking:** `appliedMode` is deprecated — use `maxLayer: "route"`. Default
59
+ proxy header `x-tc-max-layer` is now `observe` (was `route`).
60
+
61
+ ### Fixed
62
+
63
+ Circuit breaker re-probes proxy after fail-open; cold-start layer cache no
64
+ longer skips decay window.
65
+
66
+ ## 0.7.0 — 2026-08-22
67
+
68
+ A-04 chargeback attribution — stamp end-customer cohort and product feature on
69
+ every observation and forward them through applied-mode proxy headers.
9
70
 
10
71
  ### Added
11
72
 
12
- **Google Vertex AI support (observe-only)** — `wrap()` detects `@google/genai`
13
- `GoogleGenAI` clients with `vertexai: true` and patches
14
- `models.generateContent` / `models.generateContentStream`. Observations use
15
- `provider: "vertex"`. Applied mode raises `TensorCostConfigError`.
16
- Gemini Developer API clients (`vertexai: false`) are not detected.
73
+ **`customer` / `feature` options** — optional chargeback dimensions (max 128
74
+ chars each). Set at wrap time via `wrap({ customer, feature })` or
75
+ `TENSORCOST_CUSTOMER` / `TENSORCOST_FEATURE` env vars. Per-call overrides via
76
+ `.withMeta({ customer, feature })` on OpenAI and Anthropic clients. Values land
77
+ as top-level `customer` / `feature` fields on observations (not inside
78
+ `metadata`) and as `x-tc-customer` / `x-tc-feature` in applied mode.
79
+
80
+ Bedrock and Vertex clients support wrap-time defaults only (no `withMeta()`).
81
+
82
+ ## 0.6.0 — 2026-05-28
83
+
84
+ SDK metadata story — lets you group traces by prompt template or deployment in
85
+ the TensorCost dashboard.
86
+
87
+ ### Added
17
88
 
18
89
  **`deployment` option** — stamp a git SHA, CI run id, or release label on every
19
90
  observation emitted by a wrapped client. The backend groups by this value when
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @tensorcost/sdk
2
2
 
3
- One-line wrapper SDK for TensorCost — fire-and-forget LLM observability for OpenAI, Anthropic, and AWS Bedrock clients in Node.js.
3
+ One-line wrapper for TensorCost — fire-and-forget LLM observability and optional inference control for OpenAI, Anthropic, AWS Bedrock, and Google Vertex AI clients in Node.js.
4
+
5
+ **Current version:** 1.1.0 — adds `prewarm()`, separate proxy headers timeout, and non-blocking sdk-layer refresh. See [CHANGELOG](./CHANGELOG.md) and [Migrating from 0.x](#migrating-from-0x).
4
6
 
5
7
  ## Install
6
8
 
@@ -10,31 +12,35 @@ npm install @tensorcost/sdk
10
12
  pnpm add @tensorcost/sdk
11
13
  ```
12
14
 
13
- Requires Node.js 18+ (uses native `fetch`).
15
+ Requires Node.js 18+ (native `fetch`).
16
+
17
+ ## Quick start (observe)
14
18
 
15
- ## Use
19
+ The default layer is **`observe`**: telemetry only, provider call unchanged.
16
20
 
17
21
  ```ts
18
22
  import OpenAI from "openai";
19
23
  import { wrap } from "@tensorcost/sdk";
20
24
 
21
- const client = wrap(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
25
+ const client = wrap(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), {
26
+ apiKey: process.env.TENSORCOST_API_KEY,
27
+ });
22
28
 
23
- // Use the client exactly as before — TensorCost observes every call
24
- // and ships fire-and-forget observations to the platform.
25
29
  const resp = await client.chat.completions.create({
26
30
  model: "gpt-4o-mini",
27
31
  messages: [{ role: "user", content: "hi" }],
28
32
  });
29
33
  ```
30
34
 
31
- Anthropic is just as simple:
35
+ Anthropic works the same way:
32
36
 
33
37
  ```ts
34
38
  import Anthropic from "@anthropic-ai/sdk";
35
39
  import { wrap } from "@tensorcost/sdk";
36
40
 
37
- const client = wrap(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }));
41
+ const client = wrap(new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }), {
42
+ apiKey: process.env.TENSORCOST_API_KEY,
43
+ });
38
44
 
39
45
  await client.messages.create({
40
46
  model: "claude-3-5-sonnet-20241022",
@@ -43,54 +49,142 @@ await client.messages.create({
43
49
  });
44
50
  ```
45
51
 
46
- ## Configuration
52
+ Create a long-lived SDK key in the TensorCost console (**Setup → Keys**). The SDK exchanges it for a short-lived JWT via `POST /api/inference-proxy/sdk-token/exchange` and refreshes ~60s before expiry.
47
53
 
48
- `wrap()` reads from the environment by default:
54
+ ## Control layers (`maxLayer`)
49
55
 
50
- | Variable | Purpose | Required |
51
- |---|---|---|
52
- | `TENSORCOST_API_KEY` | Long-lived API key from the TensorCost console | Yes |
53
- | `TENSORCOST_BASE_URL` | Backend base URL | No (default `https://api.tensorcost.com`) |
54
- | `TENSORCOST_TENANT_ID` | Optional explicit tenant id | No |
55
- | `TENSORCOST_PROXY_URL` | Proxy base URL for applied mode | Required when `appliedMode: true` |
56
- | `TENSORCOST_ENVIRONMENT` | Environment tag stamped on every observation | No |
57
- | `TENSORCOST_CONNECTION_ID` | Provider-connection identifier | No |
56
+ v1.0 uses a single ladder for how much TensorCost may intervene on a call:
57
+
58
+ `off → observe → govern → steer → route`
59
+
60
+ A layer is a **ceiling**, not a mode switch. The effective layer is always:
58
61
 
59
- Or pass explicitly:
62
+ ```text
63
+ min(your maxLayer, console published layer)
64
+ ```
65
+
66
+ When routing is paused in the console, the published ceiling is capped at `govern`.
67
+
68
+ | Layer | Provider path | Behavior |
69
+ |---|---|---|
70
+ | `off` | Direct | No SDK wiring; client returned unchanged |
71
+ | `observe` | Direct | Telemetry only (**default** when `maxLayer` is omitted) |
72
+ | `govern` | Direct | Metadata-only `POST /v1/admit` before the provider call; may refuse unsafe calls |
73
+ | `steer` | Via proxy | Proxy on the path; may steer without full live routing |
74
+ | `route` | Via proxy | Full applied-mode routing through inference-proxy |
60
75
 
61
76
  ```ts
62
- const client = wrap(new OpenAI(...), {
63
- apiKey: "tc_live_xxx",
64
- baseUrl: "https://api.tensorcost.com",
77
+ const client = wrap(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), {
78
+ apiKey: process.env.TENSORCOST_API_KEY,
79
+ maxLayer: "route",
80
+ proxyUrl: process.env.TENSORCOST_PROXY_URL, // required for steer | route
65
81
  });
66
82
  ```
67
83
 
68
- ## Applied mode (Layer 2)
84
+ `wrap()` throws `TensorCostConfigError` when `maxLayer` is `steer` or `route` and no `proxyUrl` / `TENSORCOST_PROXY_URL` is set. **`govern` does not require `proxyUrl`** — admit uses `baseUrl`.
85
+
86
+ ### Console-published ceiling
87
+
88
+ Operators set the tenant ceiling in the console (**Policy → Routing**) or via `PATCH /api/ai/router/layer` (`published_layer`, default `observe`). The SDK fetches the effective value from `GET /api/inference-proxy/v1/sdk-layer`:
89
+
90
+ - **30s cache** while the endpoint is reachable
91
+ - **250ms cap** on a cold fetch — a hung TensorCost cannot block the model call
92
+ - **Stale-while-revalidate** after TTL: serve the last snapshot immediately and refresh in the background
93
+ - **Decays to `govern`** after 5 minutes unreachable (fail-safe: still run admit, never silently upgrade to route)
94
+
95
+ Publishing `steer` or `route` in the console requires a **content-retention data grant** (`retain_completions`, etc.). Steer/route without a grant returns **409**.
96
+
97
+ ### Govern admission
98
+
99
+ At `govern` and above, the SDK calls `POST /api/inference-proxy/v1/admit` with metadata only — **no prompt or completion bodies**:
100
+
101
+ - **204** + `x-tc-decision: layer=govern; action=allow` → proceed to the provider
102
+ - **403** + `x-tc-decision: layer=govern; action=refuse; reason=…` → call blocked (budget, model governance, etc.)
69
103
 
70
- When you enable applied mode, the SDK routes inference requests through the TensorCost inference-proxy instead of directly to the AI provider. The proxy decides per-request whether to re-route to an alternate provider or pass through unchanged — the SDK doesn't make that call.
104
+ Admit is **fail-open** on transport errors: the provider call proceeds and an `admit_unavailable` observation is emitted when transport recovers. The admit HTTP call is capped at **500ms**.
105
+
106
+ ### Route layer prerequisites
107
+
108
+ `maxLayer: "route"` is necessary but not sufficient for live provider swaps. ADR-0016 gates still apply on the proxy side: LaunchDarkly flag, `AI_ROUTING_LIVE_EXECUTION_PERMITTED`, non-`dry_run` policy with rollout, and routing not paused. When gated off, traffic pass-throughs to the original provider and the decision is recorded.
109
+
110
+ See [ADR-0026](https://github.com/vaadhlabs/tensorcost/blob/main/docs/adr/0026-sdk-control-layer-ceilings.md) and [inference-proxy applied mode](https://github.com/vaadhlabs/tensorcost/blob/main/docs/features/inference-proxy-applied-mode.md).
111
+
112
+ ## Cold start (`prewarm`)
113
+
114
+ At Lambda/container init, the first steer/route call can otherwise pay for JWT exchange and sdk-layer fetch on the hot path. Call `prewarm()` once at module scope — it shares the same process-wide runtime that `wrap()` uses, so a later `wrap()` hits a warm cache.
115
+
116
+ `wrap()` also background-warms that shared runtime on first use; explicit `prewarm()` is recommended when you know traffic is about to arrive (e.g. before the handler is invoked).
71
117
 
72
118
  ```ts
73
- const client = wrap(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), {
74
- apiKey: "tc_live_xxx",
75
- appliedMode: true,
76
- proxyUrl: "https://proxy.tensorcost.com", // or set TENSORCOST_PROXY_URL
119
+ import { prewarm, wrap } from "@tensorcost/sdk";
120
+
121
+ await prewarm({
122
+ apiKey: process.env.TENSORCOST_API_KEY,
123
+ baseUrl: process.env.TENSORCOST_BASE_URL,
124
+ maxLayer: "route",
77
125
  });
78
126
 
79
- // Use normally — the SDK redirects the client to the proxy transparently.
80
- // Observe-only telemetry still fires after every call.
81
- const resp = await client.chat.completions.create({
82
- model: "gpt-4o-mini",
83
- messages: [{ role: "user", content: "hi" }],
127
+ export const client = wrap(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), {
128
+ apiKey: process.env.TENSORCOST_API_KEY,
129
+ maxLayer: "route",
130
+ proxyUrl: process.env.TENSORCOST_PROXY_URL,
84
131
  });
85
132
  ```
86
133
 
87
- `wrap()` throws `TensorCostConfigError` at `wrap()` time if `appliedMode: true` and neither `proxyUrl` nor `TENSORCOST_PROXY_URL` is set. AWS Bedrock is not supported in applied mode — SigV4 signing happens inside the AWS SDK before the proxy layer can intercept it; passing a Bedrock client with `appliedMode: true` throws `TensorCostConfigError` immediately. Observe-only mode works fine for Bedrock.
134
+ `prewarm()` is best-effort: failures are swallowed and the first real call retries the fetches.
135
+
136
+ ## Migrating from 0.x
137
+
138
+ | 0.x | 1.0 |
139
+ |---|---|
140
+ | `appliedMode: true` | `maxLayer: "route"` (deprecated alias still works with a console warning) |
141
+ | Default `x-tc-max-layer: route` on proxy calls | Default is now `observe` |
142
+ | Boolean on/off | Full layer ladder + console ceiling |
143
+
144
+ ```ts
145
+ // Before (0.6.x)
146
+ wrap(client, { appliedMode: true, proxyUrl: "..." });
147
+
148
+ // After (1.0)
149
+ wrap(client, { maxLayer: "route", proxyUrl: "..." });
150
+ ```
151
+
152
+ Recommended rollout: **`observe` → `govern` → `route`**, raising the console `published_layer` only after each stage is validated.
153
+
154
+ ## Configuration
155
+
156
+ `wrap()` resolves options in order: explicit kwargs → environment variables → defaults.
157
+
158
+ | Variable | Purpose | Required |
159
+ |---|---|---|
160
+ | `TENSORCOST_API_KEY` | Long-lived SDK key from **Setup → Keys** | Yes |
161
+ | `TENSORCOST_BASE_URL` | Backend base URL (admit, sdk-layer, token exchange) | No (default `https://api.tensorcost.com`) |
162
+ | `TENSORCOST_PROXY_URL` | Inference-proxy base URL | Required when `maxLayer` is `steer` or `route` |
163
+ | `TENSORCOST_TENANT_ID` | Explicit tenant id (multi-tenant tests) | No |
164
+ | `TENSORCOST_ENVIRONMENT` | Environment tag on every observation | No |
165
+ | `TENSORCOST_CONNECTION_ID` | Provider-connection id for environment resolution | No |
166
+ | `TENSORCOST_AGENT_ID` / `TENSORCOST_WORKFLOW_ID` | Agent-run attribution defaults | No |
167
+ | `TENSORCOST_CUSTOMER` / `TENSORCOST_FEATURE` | Chargeback tag defaults | No |
168
+ | `TENSORCOST_DEPLOYMENT` | Git SHA / release label (`metadata.deployment`) | No |
169
+ | `TENSORCOST_PROMPT_TEMPLATE_ID` | Default prompt-template id | No |
170
+ | `TC_APPLICATION` / `TENSORCOST_APPLICATION` | Routing-policy application scope (proxy) | No |
171
+ | `TC_TAGS` / `TENSORCOST_TAGS` | Comma-separated routing tags (proxy) | No |
88
172
 
89
- See [`docs/features/inference-proxy-applied-mode.md`](../../docs/features/inference-proxy-applied-mode.md) for the proxy-side design.
173
+ ```ts
174
+ const client = wrap(new OpenAI(...), {
175
+ apiKey: "tc_live_xxx",
176
+ baseUrl: "https://api.tensorcost.com",
177
+ maxLayer: "govern",
178
+ deployment: process.env.GIT_SHA,
179
+ promptTemplateId: "support-chat-v2",
180
+ application: "customer-support",
181
+ tags: ["tier:enterprise"],
182
+ });
183
+ ```
90
184
 
91
- ## Applied-mode hardening (v0.4.0)
185
+ ## Proxy hardening
92
186
 
93
- When `appliedMode: true`, you can configure retry policy, timeouts, lifecycle hooks, and the fail-open circuit breaker.
187
+ Applies when `maxLayer` is `govern`, `steer`, or `route` (admit + proxy paths).
94
188
 
95
189
  ### Retries
96
190
 
@@ -98,48 +192,60 @@ When `appliedMode: true`, you can configure retry policy, timeouts, lifecycle ho
98
192
  import { wrap, RetryConfig } from "@tensorcost/sdk";
99
193
 
100
194
  const client = wrap(new OpenAI(...), {
101
- appliedMode: true,
195
+ maxLayer: "route",
102
196
  proxyUrl: "...",
103
197
  retry: { maxAttempts: 5, baseDelayMs: 500, maxDelayMs: 30_000 },
104
198
  });
105
199
  ```
106
200
 
107
- 5xx responses, network errors, and 429 are retried with jittered exponential backoff. A `Retry-After` header on 429 is honoured. 4xx errors (except 429) are never retried.
201
+ 5xx, network errors, and 429 are retried with jittered exponential backoff. `Retry-After` on 429 is honoured. 4xx (except 429) are never retried.
108
202
 
109
203
  ### Timeouts
110
204
 
205
+ Proxy timeouts are split so a wedged TCP connection fails open quickly while slow model bodies still get a full budget:
206
+
111
207
  ```ts
112
208
  const client = wrap(new OpenAI(...), {
113
- appliedMode: true,
209
+ maxLayer: "route",
114
210
  proxyUrl: "...",
115
- timeoutMs: 30_000, // 30 seconds; default 60_000
211
+ headersTimeoutMs: 2_000, // wait for response headers (default 2_000)
212
+ timeoutMs: 60_000, // non-streaming body read (default 60_000)
213
+ idleTimeoutMs: 30_000, // streaming idle (default 30_000)
116
214
  });
117
215
  ```
118
216
 
119
- Surfaces as `TensorCostTimeoutError` (with `timeoutKind: "total"`) when exceeded.
217
+ | Option | Default | `TensorCostTimeoutError.kind` | On timeout (steer/route) |
218
+ |---|---|---|---|
219
+ | `headersTimeoutMs` | 2s | `"headers"` | Fail-open to direct provider (no retry) |
220
+ | `timeoutMs` | 60s | `"total"` | Fail-open when `failOpenEnabled: true` |
221
+ | `idleTimeoutMs` | 30s | `"idle"` | Fail-open when `failOpenEnabled: true` |
120
222
 
121
- ### Lifecycle telemetry hooks
223
+ The headers deadline targets wedged proxy connections, not slow upstream models — body read uses `timeoutMs` independently once headers arrive.
224
+
225
+ ### Lifecycle hooks
122
226
 
123
227
  ```ts
124
228
  import { wrap, LifecycleEvent } from "@tensorcost/sdk";
125
229
 
126
230
  const client = wrap(new OpenAI(...), {
127
- appliedMode: true,
231
+ maxLayer: "route",
128
232
  proxyUrl: "...",
129
233
  onLifecycleEvent(event: LifecycleEvent) {
130
- // event.kind is one of:
131
- // "before_request" | "after_response" | "on_retry"
132
- // | "on_error" | "on_fallback"
234
+ // "before_request" | "after_response" | "on_retry" | "on_error" | "on_fallback"
133
235
  console.log(event.kind, event.model, event.attemptNumber);
134
236
  },
135
237
  });
136
238
  ```
137
239
 
138
- Events contain only request metadata — never prompt content, response bodies, or credentials. Errors thrown by the callback are swallowed.
240
+ Payloads contain request metadata only — never prompt content, response bodies, or credentials.
139
241
 
140
- ### Typed errors
242
+ ### Circuit breaker
141
243
 
142
- Every failure from the proxy path is a subclass of `TensorCostError`. Import the classes from `@tensorcost/sdk`:
244
+ After consecutive proxy 5xx failures (default 3), the circuit opens and requests route directly to the provider (fail-open). After consecutive successful probe calls (default 5), the circuit closes.
245
+
246
+ Set `failOpenEnabled: false` when direct provider access is unavailable (e.g. in-VPC sidecar-only). Proxy failures then surface as `TensorCostProxyError` instead of falling back.
247
+
248
+ ### Typed errors
143
249
 
144
250
  ```ts
145
251
  import {
@@ -149,120 +255,130 @@ import {
149
255
  TensorCostProxyError,
150
256
  TensorCostQuotaError,
151
257
  TensorCostProviderError,
258
+ TensorCostRunBudgetExceededError,
259
+ TensorCostModelGovernanceDeniedError,
260
+ TensorCostGuardrailHardStopError,
152
261
  } from "@tensorcost/sdk";
153
-
154
- try {
155
- await client.chat.completions.create({ model: "gpt-4o-mini", messages: [] });
156
- } catch (err) {
157
- if (err instanceof TensorCostQuotaError) {
158
- console.log("rate limited; retry after", err.retryAfterMs, "ms");
159
- } else if (err instanceof TensorCostError) {
160
- console.log("tensorcost error", err.status, err.attempt);
161
- }
162
- }
163
262
  ```
164
263
 
165
- ### Circuit breaker
264
+ Govern refusals and proxy decisions are also reflected in the `x-tc-decision` header and attached to observations as `decision_action` / `decision_reason` metadata when present.
166
265
 
167
- After 3 consecutive proxy 5xx responses the circuit opens and requests route directly to the provider (fail-open). The circuit closes after 5 consecutive probe successes.
266
+ ## Data grants (`maxGrants`)
168
267
 
169
- Set `failOpenEnabled: false` if direct provider access is unavailable (e.g. in-VPC deployments) — proxy failures then surface as `TensorCostProxyError` instead of falling back.
268
+ Code can cap which console data grants apply:
170
269
 
171
270
  ```ts
271
+ import { wrap, intersectGrants } from "@tensorcost/sdk";
272
+
172
273
  const client = wrap(new OpenAI(...), {
173
- appliedMode: true,
174
- proxyUrl: "...",
175
- failOpenEnabled: false, // raise on proxy error instead of falling through
274
+ apiKey: "...",
275
+ maxGrants: ["telemetry", "retain_completions"],
176
276
  });
177
277
  ```
178
278
 
179
- ## Fail-open guarantee
279
+ Effective grants = `intersect(maxGrants, console.data_grants)`. Omit `maxGrants` to allow all console grants. An empty array allows none. Grant vocabulary: `telemetry`, `retain_completions`, `retain_prompts`, `retain_tools`.
180
280
 
181
- If TensorCost is slow, down, or misconfigured, your customer call STILL succeeds. The SDK swallows internal errors and surfaces them via `console.warn`. The one exception: a missing API key throws `MissingConfigError` at `wrap()` time — silently dropping every observation would be worse than a loud import-time failure.
281
+ ## Trace metadata & attribution
182
282
 
183
- ## What is captured
283
+ ### Deployment & prompt templates
184
284
 
185
- Only metadata — never prompt or completion content:
285
+ Group traces in the dashboard by deploy or template:
186
286
 
187
- - Provider + model
188
- - Operation (`chat.completions`, `completions`, `messages`)
189
- - Request / response timestamps
190
- - Input / output token counts (from the provider's `usage` field)
191
- - Status (`success` / `error`) and error class/message on failure
192
- - A correlation UUID
287
+ ```ts
288
+ const client = wrap(new OpenAI(...), {
289
+ apiKey: "...",
290
+ deployment: process.env.GIT_SHA ?? "local",
291
+ promptTemplateId: "onboarding-email-v2",
292
+ });
293
+
294
+ // Per-call override (OpenAI + Anthropic)
295
+ await client.withMeta({ promptTemplateId: "research-agent-v5" }).chat.completions.create({ ... });
296
+ ```
193
297
 
194
- ## AWS Bedrock (observe-only)
298
+ ### Agent runs
299
+
300
+ Tag spend for **Router · Agent runs**:
195
301
 
196
302
  ```ts
197
- import {
198
- BedrockRuntimeClient,
199
- InvokeModelCommand,
200
- } from "@aws-sdk/client-bedrock-runtime";
201
- import { wrap } from "@tensorcost/sdk";
303
+ const client = wrap(new OpenAI(...), {
304
+ apiKey: "...",
305
+ agentId: "support-bot",
306
+ });
202
307
 
203
- // The only change: wrap() before passing the client to the rest of your code.
204
- const client = wrap(
205
- new BedrockRuntimeClient({ region: process.env.AWS_REGION ?? "us-east-1" }),
206
- );
207
-
208
- // Everything else is unchanged.
209
- const response = await client.send(
210
- new InvokeModelCommand({
211
- modelId: "anthropic.claude-3-haiku-20240307-v1:0",
212
- body: JSON.stringify({
213
- anthropic_version: "bedrock-2023-05-31",
214
- max_tokens: 64,
215
- messages: [{ role: "user", content: "What is 2 + 2?" }],
216
- }),
217
- contentType: "application/json",
218
- accept: "application/json",
219
- }),
220
- );
308
+ const scoped = client.withMeta({ workflowId: crypto.randomUUID() });
309
+ await scoped.chat.completions.create({ model: "gpt-4o-mini", messages: [...] });
221
310
  ```
222
311
 
223
- The SDK registers a Smithy middleware on `client.middlewareStack` that intercepts every `client.send()` call and emits a fire-and-forget observation with the model id, token counts, latency, and status. Your code is otherwise unchanged.
312
+ `customer` / `feature` are separate chargeback dimensions — not substitutes for `agentId` / `workflowId`. Bedrock and Vertex accept wrap-time defaults only (no `withMeta()`).
224
313
 
225
- ### Supported commands
314
+ ## Fail-open guarantee
226
315
 
227
- | Command | Operation recorded | Token counts |
228
- |---|---|---|
229
- | `InvokeModelCommand` | `bedrock.invoke_model` | Parsed from response body (Anthropic-on-Bedrock and Nova/Titan shapes) |
230
- | `ConverseCommand` | `bedrock.converse` | Read from `response.usage.inputTokens / outputTokens` |
231
- | `InvokeModelWithResponseStreamCommand` | `bedrock.invoke_model_stream` | Not yet — placeholder observation with `null` tokens |
232
- | `ConverseStreamCommand` | `bedrock.converse_stream` | Not yet — placeholder observation with `null` tokens |
316
+ If TensorCost is slow, down, or misconfigured, your customer call still succeeds. Internal errors are swallowed and logged via `console.warn` (or your lifecycle hook). On steer/route, missing proxy headers within **2s** also fail-open to the direct provider so agents are not blocked behind a wedged sidecar. Exceptions:
233
317
 
234
- Per-chunk token accounting for streaming commands is a follow-up workstream. The placeholder observations record latency and success/error status, which is enough for alerting and anomaly detection. Token cost attribution will arrive once per-model pricing is wired in on the backend.
318
+ - Missing `TENSORCOST_API_KEY` → `MissingConfigError` at `wrap()` time
319
+ - Missing `proxyUrl` when `maxLayer` is `steer` or `route` → `TensorCostConfigError` at `wrap()` time
320
+ - Govern **refusals** (403 from `/admit`) → call blocked by design
235
321
 
236
- ### Applied mode
322
+ Set `failOpen: false` only in tests to surface observation-pipeline errors.
237
323
 
238
- Applied mode (`{ appliedMode: true }`) is not supported for Bedrock. AWS Bedrock uses SigV4 request signing inside the AWS SDK before our middleware sees the request, so we cannot intercept and re-route it the way we do for OpenAI/Anthropic HTTP-Bearer clients. Calling `wrap(bedrockClient, { appliedMode: true })` throws `TensorCostConfigError` immediately.
324
+ ## What is captured
239
325
 
240
- ### Installation
326
+ Metadata only — never prompt or completion content:
241
327
 
242
- `@aws-sdk/client-bedrock-runtime` is an optional peer dependency — you only need it if you're using Bedrock. The SDK will not pull it in for OpenAI/Anthropic-only users.
328
+ - SDK version, provider, model, operation, modality
329
+ - Request / response timestamps and token counts
330
+ - Status (`success` / `error`) and error class/message on failure
331
+ - Correlation UUID, environment, agent/workflow ids, chargeback tags
332
+ - `decision_action` / `decision_reason` when `x-tc-decision` is present
243
333
 
244
- ```bash
245
- npm install @aws-sdk/client-bedrock-runtime
246
- # or
247
- pnpm add @aws-sdk/client-bedrock-runtime
334
+ Proxied requests include `x-tc-sdk-version`, `x-tc-sdk-capabilities`, and `x-tc-max-layer`.
335
+
336
+ ## Supported providers & operations
337
+
338
+ | Provider | Package | Layers | Operations |
339
+ |---|---|---|---|
340
+ | OpenAI | `openai` ≥ 4.x | observe → route | chat, completions, embeddings, responses (+ streaming) |
341
+ | Anthropic | `@anthropic-ai/sdk` | observe → route | messages (+ streaming) |
342
+ | Azure OpenAI | `openai` (`AzureOpenAI`) | observe → route | same as OpenAI (`provider: "openai"`) |
343
+ | AWS Bedrock | `@aws-sdk/client-bedrock-runtime` | observe, govern only | InvokeModel, Converse (+ streaming placeholders) |
344
+ | Google Vertex | `@google/genai` (`vertexai: true`) | observe, govern only | generateContent (+ streaming) |
345
+
346
+ **Bedrock and Vertex:** SigV4 / Vertex signing happens inside the AWS / Google SDK before the proxy can intercept — **`steer` and `route` are not supported**. Use `maxLayer: "govern"` or lower.
347
+
348
+ Bedrock peer dependency is optional:
349
+
350
+ ```ts
351
+ import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
352
+ import { wrap } from "@tensorcost/sdk";
353
+
354
+ const client = wrap(new BedrockRuntimeClient({ region: "us-east-1" }), {
355
+ apiKey: process.env.TENSORCOST_API_KEY,
356
+ });
248
357
  ```
249
358
 
250
- ## Currently supported
359
+ ## Framework adapters
360
+
361
+ Thin passthroughs that call `wrap()` on the underlying LLM client:
362
+
363
+ ```ts
364
+ import { ChatOpenAI } from "@langchain/openai";
365
+ import { wrapLangChain } from "@tensorcost/sdk";
251
366
 
252
- - OpenAI (`openai` >= 4.x) — `chat.completions.create`, `completions.create`
253
- - Anthropic (`@anthropic-ai/sdk` >= 0.x) — `messages.create`
254
- - AWS Bedrock (`@aws-sdk/client-bedrock-runtime` >= 3.x) — `InvokeModelCommand`, `ConverseCommand` (observe-only)
255
- - Google Vertex AI (`@google/genai` with `vertexai: true`) — `models.generateContent` (observe-only). Optional peer: `@google/genai`.
367
+ const llm = wrapLangChain(new ChatOpenAI({ model: "gpt-4o-mini" }), {
368
+ apiKey: process.env.TENSORCOST_API_KEY,
369
+ maxLayer: "observe",
370
+ });
371
+ ```
256
372
 
257
- ## Coming soon
373
+ Also exported: `wrapLlamaIndex`, `wrapCrewAI`, `wrapVercelAi`.
258
374
 
259
- - Gemini Developer API wrap (`vertexai: false`)
260
- - Streaming token accounting for Bedrock (`InvokeModelWithResponseStreamCommand`, `ConverseStreamCommand`)
261
- - Tool / function calling (multi-call trace stitching)
375
+ ## Further reading
262
376
 
263
- Note: **Azure OpenAI** is supported by wrapping the standard `AzureOpenAI`
264
- client (observations report `provider: "openai"`).
377
+ - [ADR-0026 — SDK control-layer ceilings](https://github.com/vaadhlabs/tensorcost/blob/main/docs/adr/0026-sdk-control-layer-ceilings.md)
378
+ - [Inference proxy SDK feature doc](https://github.com/vaadhlabs/tensorcost/blob/main/docs/features/inference-proxy-sdk.md)
379
+ - [Applied mode / live routing](https://github.com/vaadhlabs/tensorcost/blob/main/docs/features/inference-proxy-applied-mode.md)
380
+ - [CHANGELOG](./CHANGELOG.md)
265
381
 
266
382
  ## License
267
383
 
268
- Proprietary — TensorCost.
384
+ Apache-2.0
@@ -0,0 +1,4 @@
1
+ import type { WrapOptions } from "../types.js";
2
+ /** Thin passthrough — wraps the LLM used by CrewAI agents. */
3
+ export declare function wrapCrewAI<T>(client: T, options?: WrapOptions): T;
4
+ //# sourceMappingURL=crewai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crewai.d.ts","sourceRoot":"","sources":["../../src/adapters/crewai.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,CAEjE"}
@@ -0,0 +1,6 @@
1
+ import { wrap } from "../wrap.js";
2
+ /** Thin passthrough — wraps the LLM used by CrewAI agents. */
3
+ export function wrapCrewAI(client, options) {
4
+ return wrap(client, options);
5
+ }
6
+ //# sourceMappingURL=crewai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crewai.js","sourceRoot":"","sources":["../../src/adapters/crewai.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,8DAA8D;AAC9D,MAAM,UAAU,UAAU,CAAI,MAAS,EAAE,OAAqB;IAC5D,OAAO,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { WrapOptions } from "../types.js";
2
+ /** Thin passthrough — wraps the underlying OpenAI/Anthropic client. */
3
+ export declare function wrapLangChain<T>(client: T, options?: WrapOptions): T;
4
+ //# sourceMappingURL=langchain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"langchain.d.ts","sourceRoot":"","sources":["../../src/adapters/langchain.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,CAEpE"}
@@ -0,0 +1,6 @@
1
+ import { wrap } from "../wrap.js";
2
+ /** Thin passthrough — wraps the underlying OpenAI/Anthropic client. */
3
+ export function wrapLangChain(client, options) {
4
+ return wrap(client, options);
5
+ }
6
+ //# sourceMappingURL=langchain.js.map