@tensorcost/sdk 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +268 -77
  3. package/dist/admit-client.js +2 -2
  4. package/dist/admit-client.js.map +1 -1
  5. package/dist/circuit.d.ts +11 -3
  6. package/dist/circuit.d.ts.map +1 -1
  7. package/dist/circuit.js +28 -5
  8. package/dist/circuit.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +3 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/errors.d.ts +3 -3
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.js.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/layer.d.ts +6 -0
  20. package/dist/layer.d.ts.map +1 -1
  21. package/dist/layer.js +6 -0
  22. package/dist/layer.js.map +1 -1
  23. package/dist/prewarm.d.ts +14 -0
  24. package/dist/prewarm.d.ts.map +1 -0
  25. package/dist/prewarm.js +29 -0
  26. package/dist/prewarm.js.map +1 -0
  27. package/dist/providers/anthropic.d.ts +1 -0
  28. package/dist/providers/anthropic.d.ts.map +1 -1
  29. package/dist/providers/anthropic.js +98 -96
  30. package/dist/providers/anthropic.js.map +1 -1
  31. package/dist/providers/bedrock.d.ts.map +1 -1
  32. package/dist/providers/bedrock.js +12 -32
  33. package/dist/providers/bedrock.js.map +1 -1
  34. package/dist/providers/openai.d.ts +1 -0
  35. package/dist/providers/openai.d.ts.map +1 -1
  36. package/dist/providers/openai.js +104 -99
  37. package/dist/providers/openai.js.map +1 -1
  38. package/dist/providers/vertex.d.ts.map +1 -1
  39. package/dist/providers/vertex.js +9 -25
  40. package/dist/providers/vertex.js.map +1 -1
  41. package/dist/proxy-client.d.ts +14 -2
  42. package/dist/proxy-client.d.ts.map +1 -1
  43. package/dist/proxy-client.js +72 -17
  44. package/dist/proxy-client.js.map +1 -1
  45. package/dist/runtime.d.ts +24 -0
  46. package/dist/runtime.d.ts.map +1 -0
  47. package/dist/runtime.js +65 -0
  48. package/dist/runtime.js.map +1 -0
  49. package/dist/sdk-layer.d.ts +7 -0
  50. package/dist/sdk-layer.d.ts.map +1 -1
  51. package/dist/sdk-layer.js +57 -23
  52. package/dist/sdk-layer.js.map +1 -1
  53. package/dist/transport.d.ts +7 -0
  54. package/dist/transport.d.ts.map +1 -1
  55. package/dist/transport.js +14 -0
  56. package/dist/transport.js.map +1 -1
  57. package/dist/types.d.ts +12 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/dist/wrap-overhead.d.ts +32 -0
  62. package/dist/wrap-overhead.d.ts.map +1 -0
  63. package/dist/wrap-overhead.js +60 -0
  64. package/dist/wrap-overhead.js.map +1 -0
  65. package/dist/wrap.d.ts.map +1 -1
  66. package/dist/wrap.js +4 -12
  67. package/dist/wrap.js.map +1 -1
  68. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.1.1 — 2026-09-19
6
+
7
+ Enterprise architect-hour fixes: fail-open correctness, honest streaming stance,
8
+ observation flush for Lambda.
9
+
10
+ ### Fixed
11
+
12
+ **Circuit HALF_OPEN probe** — successful probe no longer triggers a second direct
13
+ provider call (`proxyFailed || circuitWasOpen` → `proxyFailed` only).
14
+
15
+ **Buffered JSON headers timeout** — non-streaming steer/route waits up to
16
+ `timeoutMs` for proxy headers (avoids fail-open double-bill while proxy buffers
17
+ upstream).
18
+
19
+ **Single-flight HALF_OPEN probe** — concurrent OPEN callers bypass until the
20
+ probe completes.
21
+
22
+ ### Added
23
+
24
+ **`flushObservations()`** — best-effort drain of fire-and-forget observation POSTs
25
+ before Lambda freeze.
26
+
27
+ **`stream: true` through steer proxy** — refused (direct provider when fail-open
28
+ enabled; `TensorCostConfigError` when fail-open disabled).
29
+
30
+ ## 1.1.0 — 2026-09-19
31
+
32
+ Agent hot-path hardening: fail-open faster on wedged proxy connections, optional
33
+ cold-start warmup, and non-blocking sdk-layer refresh.
34
+
35
+ ### Added
36
+
37
+ **`prewarm()`** — prefetch JWT + console published layer at Lambda/container
38
+ init. Shares the same process-wide runtime cache as `wrap()`.
39
+
40
+ **`headersTimeoutMs`** (default 2s) — separate deadline for proxy response
41
+ headers vs body read (`timeoutMs`, default 60s). Missing headers within the
42
+ budget fail-open to the direct provider without retry.
43
+
44
+ ### Changed
45
+
46
+ **Sdk-layer fetch** — 250ms cap on cold `GET /sdk-layer`; stale-while-revalidate
47
+ after the 30s TTL so refresh never blocks a model call.
48
+
49
+ **Admit** — HTTP timeout tightened to 500ms (still fail-open on errors).
50
+
51
+ **`wrap()`** — background-warms the shared runtime on first use.
52
+
53
+ ### Fixed
54
+
55
+ Body JSON parse no longer leaves an unhandled rejection when the body-read
56
+ deadline fires before `response.json()` settles.
57
+
5
58
  ## 1.0.0 — 2026-09-13
6
59
 
7
60
  Major release: control-layer ceilings replace the `appliedMode` boolean.
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 and optional inference control for OpenAI, Anthropic, AWS Bedrock, and Google Vertex AI 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.1 — fail-open correctness (no probe double-bill), buffered-json headers contract, `stream: true` refusal through steer proxy, `flushObservations()`. See [CHANGELOG](./CHANGELOG.md), [Architect hour Q&A](https://github.com/vaadhlabs/tensorcost/blob/main/docs/demo/architect-hour.md), and [Migrating from 0.x](#migrating-from-0x).
4
6
 
5
7
  ## Install
6
8
 
@@ -10,18 +12,20 @@ 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" }],
@@ -34,7 +38,9 @@ Anthropic works the same way:
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,61 +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` | Inference-proxy base URL | Required when `maxLayer` is `steer` or `route` |
56
- | `TENSORCOST_ENVIRONMENT` | Environment tag stamped on every observation | No |
57
- | `TENSORCOST_CONNECTION_ID` | Provider-connection identifier | No |
58
- | `TENSORCOST_AGENT_ID` / `TENSORCOST_WORKFLOW_ID` | Agent-run attribution defaults | No |
59
- | `TENSORCOST_CUSTOMER` / `TENSORCOST_FEATURE` | Chargeback tag defaults | 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:
61
+
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`.
60
67
 
61
- Or pass explicitly:
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 |
62
75
 
63
76
  ```ts
64
- const client = wrap(new OpenAI(...), {
65
- apiKey: "tc_live_xxx",
66
- 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
67
81
  });
68
82
  ```
69
83
 
70
- ## Control layers (`maxLayer`)
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`.
71
85
 
72
- v1.0 replaces the old `appliedMode` boolean with a **control-layer ceiling**. A layer is a ceiling, not a mode — the effective layer is always `min(your maxLayer, console published layer)`.
86
+ ### Console-published ceiling
73
87
 
74
- | Layer | Behavior |
75
- |---|---|
76
- | `off` | No SDK wiring; client returned unchanged |
77
- | `observe` | Telemetry only (**default** when `maxLayer` is omitted) |
78
- | `govern` | Metadata-only `POST /admit` before the provider call; may refuse unsafe calls |
79
- | `steer` | Proxy on the path; proxy may steer without full live routing |
80
- | `route` | Full applied-mode routing through the inference-proxy |
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.)
103
+
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).
81
117
 
82
118
  ```ts
83
- const client = wrap(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), {
84
- apiKey: "tc_live_xxx",
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,
85
124
  maxLayer: "route",
86
- proxyUrl: "https://api.my-instance.tensorcost.com", // or TENSORCOST_PROXY_URL
125
+ });
126
+
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,
87
131
  });
88
132
  ```
89
133
 
90
- `wrap()` throws `TensorCostConfigError` when `maxLayer` is `steer` or `route` and no `proxyUrl` / `TENSORCOST_PROXY_URL` is set.
134
+ `prewarm()` is best-effort: failures are swallowed and the first real call retries the fetches.
91
135
 
92
- The SDK fetches the console-published ceiling from `GET /api/inference-proxy/v1/sdk-layer` (30s cache, decays to `govern` after 5 minutes unreachable). Govern refusals and proxy decisions surface in the `x-tc-decision` response header.
136
+ ## Migrating from 0.x
93
137
 
94
- `appliedMode: true` still works but is **deprecated** — it maps to `maxLayer: "route"` with a console warning. Prefer `maxLayer` in new code.
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 |
95
143
 
96
- See [ADR-0026](https://github.com/vaadhlabs/tensorcost/blob/main/docs/adr/0026-sdk-control-layer-ceilings.md) and [`docs/features/inference-proxy-applied-mode.md`](../../docs/features/inference-proxy-applied-mode.md).
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 |
172
+
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
+ ```
97
184
 
98
185
  ## Proxy hardening
99
186
 
100
- When `maxLayer` is `govern`, `steer`, or `route`, you can configure retries, timeouts, lifecycle hooks, and the fail-open circuit breaker.
187
+ Applies when `maxLayer` is `govern`, `steer`, or `route` (admit + proxy paths).
101
188
 
102
189
  ### Retries
103
190
 
@@ -111,21 +198,33 @@ const client = wrap(new OpenAI(...), {
111
198
  });
112
199
  ```
113
200
 
114
- 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.
115
202
 
116
203
  ### Timeouts
117
204
 
205
+ Proxy timeouts are split so a wedged TCP connection fails open quickly while slow model bodies still get a full budget:
206
+
118
207
  ```ts
119
208
  const client = wrap(new OpenAI(...), {
120
209
  maxLayer: "route",
121
210
  proxyUrl: "...",
122
- timeoutMs: 30_000, // 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)
123
214
  });
124
215
  ```
125
216
 
126
- Surfaces as `TensorCostTimeoutError` (with `timeoutKind: "total"`) when exceeded.
217
+ | Option | Default | `TensorCostTimeoutError.kind` | On timeout (steer/route) |
218
+ |---|---|---|---|
219
+ | `headersTimeoutMs` | 2s | `"headers"` | Streaming only (when SSE client ships); **not used for buffered JSON** |
220
+ | `timeoutMs` | 60s | `"total"` | TTFB + body for non-streaming proxy calls; fail-open when `failOpenEnabled: true` |
221
+ | `idleTimeoutMs` | 30s | `"idle"` | Reserved for future steer SSE client |
222
+
223
+ For **non-streaming** steer/route, the SDK waits up to **`timeoutMs` for response headers** because the proxy may buffer the full upstream response before sending headers — using a 2s headers cap would fail-open and **double-bill** while the proxy still completes.
224
+
225
+ **Streaming (`stream: true`):** not implemented through the steer proxy client (1.1.1). With `failOpenEnabled: true` (default), calls go **direct** to the provider. With `failOpenEnabled: false`, `TensorCostConfigError` is thrown.
127
226
 
128
- ### Lifecycle telemetry hooks
227
+ ### Lifecycle hooks
129
228
 
130
229
  ```ts
131
230
  import { wrap, LifecycleEvent } from "@tensorcost/sdk";
@@ -140,11 +239,17 @@ const client = wrap(new OpenAI(...), {
140
239
  });
141
240
  ```
142
241
 
143
- Events contain only request metadata — never prompt content, response bodies, or credentials.
242
+ Payloads contain request metadata only — never prompt content, response bodies, or credentials.
144
243
 
145
- ### Typed errors
244
+ ### Circuit breaker
245
+
246
+ 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.
247
+
248
+ 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.
146
249
 
147
- Every failure from the proxy path is a subclass of `TensorCostError`:
250
+ HALF_OPEN recovery uses a **single-flight probe** (1.1.1) — a successful probe does **not** invoke the provider twice.
251
+
252
+ ### Typed errors
148
253
 
149
254
  ```ts
150
255
  import {
@@ -154,20 +259,53 @@ import {
154
259
  TensorCostProxyError,
155
260
  TensorCostQuotaError,
156
261
  TensorCostProviderError,
262
+ TensorCostRunBudgetExceededError,
263
+ TensorCostModelGovernanceDeniedError,
264
+ TensorCostGuardrailHardStopError,
157
265
  } from "@tensorcost/sdk";
158
266
  ```
159
267
 
160
- ### Circuit breaker
268
+ 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.
161
269
 
162
- After consecutive proxy failures the circuit opens and requests route directly to the provider (fail-open). Set `failOpenEnabled: false` when direct provider access is unavailable — proxy failures then surface as `TensorCostProxyError` instead of falling back.
270
+ ## Data grants (`maxGrants`)
163
271
 
164
- ## Agent-run attribution
272
+ Code can cap which console data grants apply:
165
273
 
166
- Tag spend so **Router · Agent runs** can roll up cost per workflow:
274
+ ```ts
275
+ import { wrap, intersectGrants } from "@tensorcost/sdk";
276
+
277
+ const client = wrap(new OpenAI(...), {
278
+ apiKey: "...",
279
+ maxGrants: ["telemetry", "retain_completions"],
280
+ });
281
+ ```
282
+
283
+ 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`.
284
+
285
+ ## Trace metadata & attribution
286
+
287
+ ### Deployment & prompt templates
288
+
289
+ Group traces in the dashboard by deploy or template:
167
290
 
168
291
  ```ts
169
292
  const client = wrap(new OpenAI(...), {
170
- apiKey: "tc_live_xxx",
293
+ apiKey: "...",
294
+ deployment: process.env.GIT_SHA ?? "local",
295
+ promptTemplateId: "onboarding-email-v2",
296
+ });
297
+
298
+ // Per-call override (OpenAI + Anthropic)
299
+ await client.withMeta({ promptTemplateId: "research-agent-v5" }).chat.completions.create({ ... });
300
+ ```
301
+
302
+ ### Agent runs
303
+
304
+ Tag spend for **Router · Agent runs**:
305
+
306
+ ```ts
307
+ const client = wrap(new OpenAI(...), {
308
+ apiKey: "...",
171
309
  agentId: "support-bot",
172
310
  });
173
311
 
@@ -175,57 +313,110 @@ const scoped = client.withMeta({ workflowId: crypto.randomUUID() });
175
313
  await scoped.chat.completions.create({ model: "gpt-4o-mini", messages: [...] });
176
314
  ```
177
315
 
178
- `customer` / `feature` are separate chargeback tags — not substitutes for `agentId` / `workflowId`.
316
+ `customer` / `feature` are separate chargeback dimensions — not substitutes for `agentId` / `workflowId`. Bedrock and Vertex accept wrap-time defaults only (no `withMeta()`).
179
317
 
180
318
  ## Fail-open guarantee
181
319
 
182
- 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.
320
+ 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). Exceptions:
321
+
322
+ - Missing `TENSORCOST_API_KEY` → `MissingConfigError` at `wrap()` time
323
+ - Missing `proxyUrl` when `maxLayer` is `steer` or `route` → `TensorCostConfigError` at `wrap()` time
324
+ - Govern **refusals** (403 from `/admit`) → call blocked by design
325
+ - Steer **run-budget** refusals → not fail-opened
326
+
327
+ ### Duplicate spend (honest)
328
+
329
+ Fail-open may call the provider **after** a failed proxy attempt. If the proxy already forwarded to the provider before failing, you can be billed twice. Mitigations:
330
+
331
+ - **Sidecar / in-VPC steer:** set `failOpenEnabled: false` — no silent direct fallback
332
+ - **Buffered JSON:** SDK 1.1.1+ waits `timeoutMs` for headers (not 2s) to avoid aborting a healthy slow proxy call
333
+
334
+ Set `failOpen: false` only in tests to surface observation-pipeline errors.
335
+
336
+ ### Lambda: flush observations
337
+
338
+ ```ts
339
+ import { flushObservations } from "@tensorcost/sdk";
340
+
341
+ export const handler = async (event) => {
342
+ // ... model call via wrapped client ...
343
+ await flushObservations({ apiKey: process.env.TENSORCOST_API_KEY }, 3_000);
344
+ };
345
+ ```
346
+
347
+ ## SDK version matrix (1.0.0 vs 1.1.x)
348
+
349
+ | Feature | 1.0.0 | 1.1.0+ | 1.1.1+ |
350
+ |---------|-------|--------|--------|
351
+ | `prewarm()` | No | Yes | Yes |
352
+ | `headersTimeoutMs` / split timeouts | No | Yes | Yes |
353
+ | Admit 500ms cap | Partial | Yes | Yes |
354
+ | Buffered JSON headers = `timeoutMs` | No | No | **Yes** |
355
+ | Probe success no double-call | No | No | **Yes** |
356
+ | `stream: true` steer refusal | No | No | **Yes** |
357
+ | `flushObservations()` | No | No | **Yes** |
183
358
 
184
359
  ## What is captured
185
360
 
186
- Only metadata — never prompt or completion content:
361
+ Metadata only — never prompt or completion content:
187
362
 
188
- - Provider + model + operation + modality
363
+ - SDK version, provider, model, operation, modality
189
364
  - Request / response timestamps and token counts
190
365
  - Status (`success` / `error`) and error class/message on failure
191
366
  - Correlation UUID, environment, agent/workflow ids, chargeback tags
367
+ - `decision_action` / `decision_reason` when `x-tc-decision` is present
192
368
 
193
- ## AWS Bedrock (observe-only)
369
+ Proxied requests include `x-tc-sdk-version`, `x-tc-sdk-capabilities`, and `x-tc-max-layer`.
194
370
 
195
- ```ts
196
- import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
197
- import { wrap } from "@tensorcost/sdk";
371
+ ## Supported providers & operations
198
372
 
199
- const client = wrap(
200
- new BedrockRuntimeClient({ region: process.env.AWS_REGION ?? "us-east-1" }),
201
- );
202
- ```
373
+ | Provider | Package | Layers | Operations |
374
+ |---|---|---|---|
375
+ | OpenAI | `openai` ≥ 4.x | observe → route | chat, completions, embeddings, responses |
376
+ | Anthropic | `@anthropic-ai/sdk` | observe → route | messages |
377
+ | Azure OpenAI | `openai` (`AzureOpenAI`) | observe → route | same as OpenAI (`provider: "openai"`) |
378
+ | AWS Bedrock | `@aws-sdk/client-bedrock-runtime` | observe, govern only | InvokeModel, Converse (token placeholders) |
379
+ | Google Vertex | `@google/genai` (`vertexai: true`) | observe, govern only | generateContent (token placeholders) |
380
+
381
+ **Steer/route streaming:** SDK routes `stream: true` **direct** to the provider (or errors if fail-open off). Proxy server supports SSE; SDK SSE client not shipped yet.
203
382
 
204
- Supported commands: `InvokeModelCommand`, `ConverseCommand`, and their streaming counterparts (streaming records latency/status; per-chunk token accounting is a follow-up).
383
+ Full matrix: [`docs/demo/data-path-one-pager.md`](https://github.com/vaadhlabs/tensorcost/blob/main/docs/demo/data-path-one-pager.md).
205
384
 
206
- **Steer/route are not supported for Bedrock** — SigV4 signing happens inside the AWS SDK before the proxy can intercept. Use `maxLayer: "govern"` or lower for Bedrock telemetry.
385
+ **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.
207
386
 
208
- `@aws-sdk/client-bedrock-runtime` is an optional peer dependency.
387
+ Bedrock peer dependency is optional:
209
388
 
210
- ## Google Vertex AI (observe-only)
389
+ ```ts
390
+ import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
391
+ import { wrap } from "@tensorcost/sdk";
211
392
 
212
- Wrap `@google/genai` clients constructed with `vertexai: true`. `models.generateContent` and streaming variants emit observations with `provider: "vertex"`. Steer/route are not supported (same signing constraint as Bedrock).
393
+ const client = wrap(new BedrockRuntimeClient({ region: "us-east-1" }), {
394
+ apiKey: process.env.TENSORCOST_API_KEY,
395
+ });
396
+ ```
213
397
 
214
398
  ## Framework adapters
215
399
 
216
- Optional helpers for common LLM frameworks (observe + proxy paths where supported):
400
+ Thin passthroughs that call `wrap()` on the underlying LLM client:
217
401
 
218
402
  ```ts
219
- import { wrapLangChain, wrapLlamaIndex, wrapCrewAI, wrapVercelAi } from "@tensorcost/sdk";
403
+ import { ChatOpenAI } from "@langchain/openai";
404
+ import { wrapLangChain } from "@tensorcost/sdk";
405
+
406
+ const llm = wrapLangChain(new ChatOpenAI({ model: "gpt-4o-mini" }), {
407
+ apiKey: process.env.TENSORCOST_API_KEY,
408
+ maxLayer: "observe",
409
+ });
220
410
  ```
221
411
 
222
- ## Currently supported
412
+ Also exported: `wrapLlamaIndex`, `wrapCrewAI`, `wrapVercelAi`.
413
+
414
+ ## Further reading
223
415
 
224
- - OpenAI (`openai` >= 4.x) — chat, completions, embeddings, responses
225
- - Anthropic (`@anthropic-ai/sdk`) — messages
226
- - AWS Bedrock (`@aws-sdk/client-bedrock-runtime`) — invoke/converse (+ streaming placeholders)
227
- - Google Vertex AI (`@google/genai` with `vertexai: true`) — generateContent
228
- - Azure OpenAI — wrap the standard `AzureOpenAI` client (observations report `provider: "openai"`)
416
+ - [ADR-0026 — SDK control-layer ceilings](https://github.com/vaadhlabs/tensorcost/blob/main/docs/adr/0026-sdk-control-layer-ceilings.md)
417
+ - [Inference proxy SDK feature doc](https://github.com/vaadhlabs/tensorcost/blob/main/docs/features/inference-proxy-sdk.md)
418
+ - [Applied mode / live routing](https://github.com/vaadhlabs/tensorcost/blob/main/docs/features/inference-proxy-applied-mode.md)
419
+ - [CHANGELOG](./CHANGELOG.md)
229
420
 
230
421
  ## License
231
422
 
@@ -4,7 +4,7 @@
4
4
  * Fail-open on transport failure: the customer's call proceeds to the
5
5
  * provider and an error observation is emitted when transport is available.
6
6
  */
7
- import { ADMIT_PATH } from "./layer.js";
7
+ import { ADMIT_PATH, ADMIT_TIMEOUT_MS } from "./layer.js";
8
8
  import { SDK_VERSION, sdkCapabilityHeader } from "./version.js";
9
9
  export async function admitRequest(opts) {
10
10
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -33,7 +33,7 @@ export async function admitRequest(opts) {
33
33
  : {}),
34
34
  };
35
35
  const controller = new AbortController();
36
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5_000);
36
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? ADMIT_TIMEOUT_MS);
37
37
  try {
38
38
  const resp = await fetchImpl(url, {
39
39
  method: "POST",
@@ -1 +1 @@
1
- {"version":3,"file":"admit-client.js","sourceRoot":"","sources":["../src/admit-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"admit-client.js","sourceRoot":"","sources":["../src/admit-client.ts"],"names":[],"mappings":"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"}
package/dist/circuit.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * State machine:
11
11
  *
12
12
  * CLOSED ──(failures >= openThreshold)──> OPEN
13
- * OPEN ──(next call, always probes)───> HALF_OPEN
13
+ * OPEN ──(single-flight probe)───────> HALF_OPEN
14
14
  * HALF_OPEN ──(proxy 2xx)──────────────> probeSuccesses++
15
15
  * ──(proxy 5xx/err)──────────> back to OPEN (resets probes)
16
16
  * HALF_OPEN ──(probeSuccesses >= closeThreshold)──> CLOSED
@@ -31,6 +31,8 @@ export declare class CircuitBreaker {
31
31
  private state;
32
32
  private consecutiveFailures;
33
33
  private probeSuccesses;
34
+ /** True while a HALF_OPEN probe is in flight (single-flight). */
35
+ private probeInFlight;
34
36
  readonly config: CircuitBreakerConfig;
35
37
  private readonly onTransition?;
36
38
  constructor(config?: CircuitBreakerConfig, onTransition?: CircuitTransitionListener);
@@ -46,12 +48,18 @@ export declare class CircuitBreaker {
46
48
  */
47
49
  shouldBypass(): boolean;
48
50
  /**
49
- * Returns true when the next call should be a probe (the circuit is
50
- * OPEN and we are transitioning to HALF_OPEN to test the proxy).
51
+ * Returns true when this caller should run the single-flight HALF_OPEN
52
+ * probe (OPEN → HALF_OPEN). Concurrent OPEN callers get false and bypass.
51
53
  */
52
54
  shouldProbe(): boolean;
53
55
  /** Call after a proxy request succeeds. */
54
56
  recordSuccess(): void;
57
+ /**
58
+ * Clear a HALF_OPEN probe that never reached recordSuccess/recordFailure
59
+ * (e.g. token exchange threw before the HTTP call). Idempotent once the
60
+ * probe has settled.
61
+ */
62
+ releaseStuckProbe(): void;
55
63
  /** Call after a proxy request fails (5xx or network error). */
56
64
  recordFailure(): void;
57
65
  private reset;
@@ -1 +1 @@
1
- {"version":3,"file":"circuit.d.ts","sourceRoot":"","sources":["../src/circuit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAE3D,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,sBAAsB,EAAE,oBAGpC,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,CACtC,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,YAAY,KACb,IAAI,CAAC;AAEV,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,cAAc,CAAK;IAC3B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAA4B;gBAGxD,MAAM,GAAE,oBAA6C,EACrD,YAAY,CAAC,EAAE,yBAAyB;IAM1C,OAAO,CAAC,UAAU;IAOlB,6BAA6B;IAC7B,IAAI,YAAY,IAAI,YAAY,CAE/B;IAED,wDAAwD;IACxD,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;;;OAIG;IACH,YAAY,IAAI,OAAO;IAIvB;;;OAGG;IACH,WAAW,IAAI,OAAO;IAQtB,2CAA2C;IAC3C,aAAa,IAAI,IAAI;IAWrB,+DAA+D;IAC/D,aAAa,IAAI,IAAI;IAarB,OAAO,CAAC,KAAK;CAKd"}
1
+ {"version":3,"file":"circuit.d.ts","sourceRoot":"","sources":["../src/circuit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAE3D,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,qFAAqF;IACrF,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,sBAAsB,EAAE,oBAGpC,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,CACtC,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,YAAY,KACb,IAAI,CAAC;AAEV,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,cAAc,CAAK;IAC3B,iEAAiE;IACjE,OAAO,CAAC,aAAa,CAAS;IAC9B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAA4B;gBAGxD,MAAM,GAAE,oBAA6C,EACrD,YAAY,CAAC,EAAE,yBAAyB;IAM1C,OAAO,CAAC,UAAU;IAOlB,6BAA6B;IAC7B,IAAI,YAAY,IAAI,YAAY,CAE/B;IAED,wDAAwD;IACxD,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;;;OAIG;IACH,YAAY,IAAI,OAAO;IAOvB;;;OAGG;IACH,WAAW,IAAI,OAAO;IAStB,2CAA2C;IAC3C,aAAa,IAAI,IAAI;IAYrB;;;;OAIG;IACH,iBAAiB,IAAI,IAAI;IAQzB,+DAA+D;IAC/D,aAAa,IAAI,IAAI;IAcrB,OAAO,CAAC,KAAK;CAMd"}