@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.
- package/CHANGELOG.md +79 -8
- package/README.md +245 -129
- package/dist/adapters/crewai.d.ts +4 -0
- package/dist/adapters/crewai.d.ts.map +1 -0
- package/dist/adapters/crewai.js +6 -0
- package/dist/adapters/crewai.js.map +1 -0
- package/dist/adapters/langchain.d.ts +4 -0
- package/dist/adapters/langchain.d.ts.map +1 -0
- package/dist/adapters/langchain.js +6 -0
- package/dist/adapters/langchain.js.map +1 -0
- package/dist/adapters/llamaindex.d.ts +4 -0
- package/dist/adapters/llamaindex.d.ts.map +1 -0
- package/dist/adapters/llamaindex.js +6 -0
- package/dist/adapters/llamaindex.js.map +1 -0
- package/dist/adapters/vercel-ai.d.ts +4 -0
- package/dist/adapters/vercel-ai.d.ts.map +1 -0
- package/dist/adapters/vercel-ai.js +6 -0
- package/dist/adapters/vercel-ai.js.map +1 -0
- package/dist/admit-client.d.ts +39 -0
- package/dist/admit-client.d.ts.map +1 -0
- package/dist/admit-client.js +93 -0
- package/dist/admit-client.js.map +1 -0
- package/dist/applied.d.ts +6 -1
- package/dist/applied.d.ts.map +1 -1
- package/dist/applied.js +13 -1
- package/dist/applied.js.map +1 -1
- package/dist/circuit-observations.d.ts +12 -0
- package/dist/circuit-observations.d.ts.map +1 -0
- package/dist/circuit-observations.js +37 -0
- package/dist/circuit-observations.js.map +1 -0
- package/dist/circuit.d.ts +4 -1
- package/dist/circuit.d.ts.map +1 -1
- package/dist/circuit.js +13 -7
- package/dist/circuit.js.map +1 -1
- package/dist/config.d.ts +2 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +54 -6
- package/dist/config.js.map +1 -1
- package/dist/decision.d.ts +11 -0
- package/dist/decision.d.ts.map +1 -0
- package/dist/decision.js +40 -0
- package/dist/decision.js.map +1 -0
- package/dist/errors.d.ts +23 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/layer.d.ts +33 -0
- package/dist/layer.d.ts.map +1 -0
- package/dist/layer.js +83 -0
- package/dist/layer.js.map +1 -0
- package/dist/prewarm.d.ts +12 -0
- package/dist/prewarm.d.ts.map +1 -0
- package/dist/prewarm.js +22 -0
- package/dist/prewarm.js.map +1 -0
- package/dist/providers/anthropic.d.ts +10 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +134 -57
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts +1 -1
- package/dist/providers/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +16 -34
- package/dist/providers/bedrock.js.map +1 -1
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +143 -61
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/vertex.d.ts +1 -1
- package/dist/providers/vertex.d.ts.map +1 -1
- package/dist/providers/vertex.js +18 -27
- package/dist/providers/vertex.js.map +1 -1
- package/dist/proxy-client.d.ts +30 -7
- package/dist/proxy-client.d.ts.map +1 -1
- package/dist/proxy-client.js +123 -19
- package/dist/proxy-client.js.map +1 -1
- package/dist/runtime.d.ts +23 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +60 -0
- package/dist/runtime.js.map +1 -0
- package/dist/sdk-layer.d.ts +33 -0
- package/dist/sdk-layer.d.ts.map +1 -0
- package/dist/sdk-layer.js +107 -0
- package/dist/sdk-layer.js.map +1 -0
- package/dist/transport.d.ts +3 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +6 -2
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +82 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/dist/wrap-overhead.d.ts +32 -0
- package/dist/wrap-overhead.d.ts.map +1 -0
- package/dist/wrap-overhead.js +60 -0
- package/dist/wrap-overhead.js.map +1 -0
- package/dist/wrap.d.ts +13 -17
- package/dist/wrap.d.ts.map +1 -1
- package/dist/wrap.js +59 -74
- package/dist/wrap.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,18 +2,89 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 1.1.0 — 2026-09-19
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
`
|
|
15
|
-
`
|
|
16
|
-
|
|
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
|
|
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+ (
|
|
15
|
+
Requires Node.js 18+ (native `fetch`).
|
|
16
|
+
|
|
17
|
+
## Quick start (observe)
|
|
14
18
|
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
## Control layers (`maxLayer`)
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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:
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
##
|
|
185
|
+
## Proxy hardening
|
|
92
186
|
|
|
93
|
-
|
|
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
|
-
|
|
195
|
+
maxLayer: "route",
|
|
102
196
|
proxyUrl: "...",
|
|
103
197
|
retry: { maxAttempts: 5, baseDelayMs: 500, maxDelayMs: 30_000 },
|
|
104
198
|
});
|
|
105
199
|
```
|
|
106
200
|
|
|
107
|
-
5xx
|
|
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
|
-
|
|
209
|
+
maxLayer: "route",
|
|
114
210
|
proxyUrl: "...",
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
+
maxLayer: "route",
|
|
128
232
|
proxyUrl: "...",
|
|
129
233
|
onLifecycleEvent(event: LifecycleEvent) {
|
|
130
|
-
//
|
|
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
|
-
|
|
240
|
+
Payloads contain request metadata only — never prompt content, response bodies, or credentials.
|
|
139
241
|
|
|
140
|
-
###
|
|
242
|
+
### Circuit breaker
|
|
141
243
|
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
+
## Data grants (`maxGrants`)
|
|
168
267
|
|
|
169
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
+
## Trace metadata & attribution
|
|
182
282
|
|
|
183
|
-
|
|
283
|
+
### Deployment & prompt templates
|
|
184
284
|
|
|
185
|
-
|
|
285
|
+
Group traces in the dashboard by deploy or template:
|
|
186
286
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
|
|
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
|
-
|
|
298
|
+
### Agent runs
|
|
299
|
+
|
|
300
|
+
Tag spend for **Router · Agent runs**:
|
|
195
301
|
|
|
196
302
|
```ts
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
import { wrap } from "@tensorcost/sdk";
|
|
303
|
+
const client = wrap(new OpenAI(...), {
|
|
304
|
+
apiKey: "...",
|
|
305
|
+
agentId: "support-bot",
|
|
306
|
+
});
|
|
202
307
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
+
## Fail-open guarantee
|
|
226
315
|
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
+
Set `failOpen: false` only in tests to surface observation-pipeline errors.
|
|
237
323
|
|
|
238
|
-
|
|
324
|
+
## What is captured
|
|
239
325
|
|
|
240
|
-
|
|
326
|
+
Metadata only — never prompt or completion content:
|
|
241
327
|
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
373
|
+
Also exported: `wrapLlamaIndex`, `wrapCrewAI`, `wrapVercelAi`.
|
|
258
374
|
|
|
259
|
-
|
|
260
|
-
- Streaming token accounting for Bedrock (`InvokeModelWithResponseStreamCommand`, `ConverseStreamCommand`)
|
|
261
|
-
- Tool / function calling (multi-call trace stitching)
|
|
375
|
+
## Further reading
|
|
262
376
|
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
384
|
+
Apache-2.0
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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"}
|