@tensorcost/sdk 0.3.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 +36 -0
- package/LICENSE +201 -0
- package/README.md +119 -0
- package/dist/applied.d.ts +98 -0
- package/dist/applied.d.ts.map +1 -0
- package/dist/applied.js +174 -0
- package/dist/applied.js.map +1 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +86 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/anthropic.d.ts +13 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +119 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/detect.d.ts +13 -0
- package/dist/providers/detect.d.ts.map +1 -0
- package/dist/providers/detect.js +39 -0
- package/dist/providers/detect.js.map +1 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai.d.ts +16 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +137 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/transport.d.ts +56 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +150 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/wrap.d.ts +16 -0
- package/dist/wrap.d.ts.map +1 -0
- package/dist/wrap.js +48 -0
- package/dist/wrap.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget HTTP transport for observation events.
|
|
3
|
+
*
|
|
4
|
+
* Design:
|
|
5
|
+
*
|
|
6
|
+
* * `post(observation)` returns synchronously the moment the observation
|
|
7
|
+
* is enqueued. The actual fetch runs on the microtask queue via
|
|
8
|
+
* `queueMicrotask`, so the customer's hot path is never blocked on
|
|
9
|
+
* network IO.
|
|
10
|
+
* * If the in-flight queue saturates (>= MAX_QUEUE_DEPTH), we drop the
|
|
11
|
+
* oldest observation rather than block. Losing one observation is
|
|
12
|
+
* strictly preferable to slowing down the customer's request loop.
|
|
13
|
+
* * A short-lived JWT is fetched once via
|
|
14
|
+
* `POST /api/inference-proxy/sdk-token/exchange` using the long-lived
|
|
15
|
+
* API key.
|
|
16
|
+
* The token is cached in-memory and refreshed when within
|
|
17
|
+
* TOKEN_REFRESH_LEEWAY_MS of expiry.
|
|
18
|
+
* * All errors during posting are swallowed when `failOpen` is true
|
|
19
|
+
* (the default) and surfaced via console.warn. The customer's call
|
|
20
|
+
* site never sees an exception.
|
|
21
|
+
*/
|
|
22
|
+
const SDK_VERSION = "tensorcost-node/0.3.0";
|
|
23
|
+
const TOKEN_REFRESH_LEEWAY_MS = 60_000;
|
|
24
|
+
const DEFAULT_TOKEN_LIFETIME_MS = 15 * 60_000;
|
|
25
|
+
const MAX_QUEUE_DEPTH = 1024;
|
|
26
|
+
const DEFAULT_TIMEOUT_MS = 2_000;
|
|
27
|
+
export class ObservationTransport {
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
this.pending = 0;
|
|
30
|
+
this.tokenCache = null;
|
|
31
|
+
/** In-flight token-exchange promise; coalesces concurrent refreshes. */
|
|
32
|
+
this.tokenInflight = null;
|
|
33
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
34
|
+
this.apiKey = opts.apiKey;
|
|
35
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
36
|
+
this.failOpen = opts.failOpen ?? true;
|
|
37
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Enqueue an observation for fire-and-forget delivery. Returns
|
|
41
|
+
* immediately (synchronously). Errors are logged via console.warn,
|
|
42
|
+
* never thrown, when failOpen is true.
|
|
43
|
+
*/
|
|
44
|
+
post(observation) {
|
|
45
|
+
if (this.pending >= MAX_QUEUE_DEPTH) {
|
|
46
|
+
// Backpressure: drop *this* observation (the "oldest" semantically
|
|
47
|
+
// is whichever is in-flight; in a microtask-dispatched queue we
|
|
48
|
+
// cannot pull in-flight work back, so we drop the newest enqueue
|
|
49
|
+
// attempt — the practical effect of "drop oldest when full" once
|
|
50
|
+
// the queue stays saturated.).
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.warn("tensorcost: observation queue saturated; dropping observation");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.pending += 1;
|
|
56
|
+
queueMicrotask(() => {
|
|
57
|
+
void this.deliver(observation).finally(() => {
|
|
58
|
+
this.pending -= 1;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async deliver(observation) {
|
|
63
|
+
try {
|
|
64
|
+
const token = await this.getToken();
|
|
65
|
+
const url = `${this.baseUrl}/api/inference-proxy/observation`;
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
68
|
+
try {
|
|
69
|
+
const resp = await this.fetchImpl(url, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: `Bearer ${token}`,
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"User-Agent": SDK_VERSION,
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(observation),
|
|
77
|
+
signal: controller.signal,
|
|
78
|
+
});
|
|
79
|
+
if (!resp.ok) {
|
|
80
|
+
// eslint-disable-next-line no-console
|
|
81
|
+
console.warn(`tensorcost: observation POST failed status=${resp.status}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (!this.failOpen) {
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.warn(`tensorcost: observation delivery failed: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async getToken() {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
if (this.tokenCache &&
|
|
99
|
+
now < this.tokenCache.expiresAt - TOKEN_REFRESH_LEEWAY_MS) {
|
|
100
|
+
return this.tokenCache.token;
|
|
101
|
+
}
|
|
102
|
+
if (this.tokenInflight) {
|
|
103
|
+
return this.tokenInflight;
|
|
104
|
+
}
|
|
105
|
+
this.tokenInflight = this.exchangeToken().finally(() => {
|
|
106
|
+
this.tokenInflight = null;
|
|
107
|
+
});
|
|
108
|
+
return this.tokenInflight;
|
|
109
|
+
}
|
|
110
|
+
async exchangeToken() {
|
|
111
|
+
const url = `${this.baseUrl}/api/inference-proxy/sdk-token/exchange`;
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
114
|
+
try {
|
|
115
|
+
const resp = await this.fetchImpl(url, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
"User-Agent": SDK_VERSION,
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ api_key: this.apiKey }),
|
|
122
|
+
signal: controller.signal,
|
|
123
|
+
});
|
|
124
|
+
if (!resp.ok) {
|
|
125
|
+
throw new Error(`token-exchange failed status=${resp.status}`);
|
|
126
|
+
}
|
|
127
|
+
const body = (await resp.json());
|
|
128
|
+
const token = body.token ?? body.access_token;
|
|
129
|
+
if (!token) {
|
|
130
|
+
throw new Error("token-exchange response missing token field");
|
|
131
|
+
}
|
|
132
|
+
const lifetimeMs = typeof body.expires_in === "number"
|
|
133
|
+
? body.expires_in * 1000
|
|
134
|
+
: DEFAULT_TOKEN_LIFETIME_MS;
|
|
135
|
+
this.tokenCache = {
|
|
136
|
+
token,
|
|
137
|
+
expiresAt: Date.now() + lifetimeMs,
|
|
138
|
+
};
|
|
139
|
+
return token;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export const __test__ = {
|
|
147
|
+
SDK_VERSION,
|
|
148
|
+
MAX_QUEUE_DEPTH,
|
|
149
|
+
};
|
|
150
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal type defs for the TensorCost Node SDK.
|
|
3
|
+
*
|
|
4
|
+
* Kept intentionally narrow — the wrap() entry-point is generic over
|
|
5
|
+
* the customer's client type, so most provider shapes are duck-typed
|
|
6
|
+
* at runtime rather than expressed in TypeScript.
|
|
7
|
+
*/
|
|
8
|
+
export type Provider = "openai" | "anthropic";
|
|
9
|
+
export type Operation = "chat.completions" | "completions" | "messages";
|
|
10
|
+
export type ObservationStatus = "success" | "error";
|
|
11
|
+
/**
|
|
12
|
+
* Wire shape posted to `POST /api/proxy/observation`. Mirrors the
|
|
13
|
+
* Python SDK's `Observation` model byte-for-byte so the two SDKs
|
|
14
|
+
* are interchangeable from the backend's point of view.
|
|
15
|
+
*/
|
|
16
|
+
export interface Observation {
|
|
17
|
+
sdk_version: string;
|
|
18
|
+
provider: Provider;
|
|
19
|
+
model: string;
|
|
20
|
+
operation: Operation;
|
|
21
|
+
request_at: string;
|
|
22
|
+
response_at: string;
|
|
23
|
+
input_tokens: number | null;
|
|
24
|
+
output_tokens: number | null;
|
|
25
|
+
cost_usd_cents: number | null;
|
|
26
|
+
status: ObservationStatus;
|
|
27
|
+
error_message: string | null;
|
|
28
|
+
correlation_id: string;
|
|
29
|
+
tenant_id?: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Phase A4 batch 3 — per-environment data scoping. Optional. Stamped
|
|
32
|
+
* onto every observation envelope when the SDK was constructed with
|
|
33
|
+
* `wrap(client, { environment: 'staging' })`. Forwarded verbatim by
|
|
34
|
+
* the inference-proxy ObservationService onto the Kafka envelope;
|
|
35
|
+
* ai-service falls back to 'production' on omit.
|
|
36
|
+
*/
|
|
37
|
+
environment?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Phase A4 batch 4 — provider-connection identifier. Optional.
|
|
40
|
+
* Stamped onto every observation envelope when the SDK was
|
|
41
|
+
* constructed with `wrap(client, { connectionId: '...' })`.
|
|
42
|
+
* Forwarded verbatim by the inference-proxy onto the Kafka
|
|
43
|
+
* envelope; ai-service uses it as the second tier of its
|
|
44
|
+
* env-resolution chain (see ProxyObservationConsumer) when the SDK
|
|
45
|
+
* did not set `environment`.
|
|
46
|
+
*/
|
|
47
|
+
connection_id?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface WrapOptions {
|
|
50
|
+
/** Long-lived TensorCost API key. Falls back to `TENSORCOST_API_KEY`. */
|
|
51
|
+
apiKey?: string;
|
|
52
|
+
/** Backend base URL. Falls back to `TENSORCOST_BASE_URL`, then default. */
|
|
53
|
+
baseUrl?: string;
|
|
54
|
+
/** Optional explicit tenant id, useful for multi-tenant test harnesses. */
|
|
55
|
+
tenantId?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Phase A4 batch 3 — environment tag. When present, the SDK reads
|
|
58
|
+
* the value once at `wrap()` time and stamps every outbound
|
|
59
|
+
* observation envelope with it. Falls back to
|
|
60
|
+
* `TENSORCOST_ENVIRONMENT`. When omitted entirely the field is
|
|
61
|
+
* absent from the envelope; the backend then falls back to the
|
|
62
|
+
* column default ('production'). Free-text per
|
|
63
|
+
* project_environment_scoping.md (1..64 chars).
|
|
64
|
+
*/
|
|
65
|
+
environment?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Phase A4 batch 4 — provider-connection identifier. When present,
|
|
68
|
+
* the SDK stamps it on every outbound observation envelope. Falls
|
|
69
|
+
* back to `TENSORCOST_CONNECTION_ID`. When the SDK is wrapping a
|
|
70
|
+
* client that talks to ONE provider connection, set this so the
|
|
71
|
+
* backend can resolve the connection's `environment` even if the
|
|
72
|
+
* SDK was not given an explicit `environment`.
|
|
73
|
+
*
|
|
74
|
+
* Precedence (most-specific wins):
|
|
75
|
+
* 1. SDK envelope `environment` (when set on the SDK or env var).
|
|
76
|
+
* 2. SDK envelope `connectionId` → backend looks up
|
|
77
|
+
* `integration.cloud_account.environment` (60s LRU cache).
|
|
78
|
+
* 3. Backend column default `'production'`.
|
|
79
|
+
*
|
|
80
|
+
* Customers using ONE SDK instance across multiple connections
|
|
81
|
+
* should leave this unset and rely on `environment` per-init, OR
|
|
82
|
+
* construct a separate `wrap(...)` per connection.
|
|
83
|
+
*/
|
|
84
|
+
connectionId?: string;
|
|
85
|
+
/**
|
|
86
|
+
* When true (the default), any internal observation-pipeline error is
|
|
87
|
+
* caught and logged via console.warn — the customer's call ALWAYS
|
|
88
|
+
* returns normally. Set to false only in SDK tests / CI smoke runs.
|
|
89
|
+
*/
|
|
90
|
+
failOpen?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Layer 2 applied mode. When true, the SDK routes every inference
|
|
93
|
+
* request through the TensorCost inference-proxy rather than sending
|
|
94
|
+
* it directly to the upstream AI provider. The proxy decides
|
|
95
|
+
* per-request whether to actually re-route or pass through unchanged.
|
|
96
|
+
*
|
|
97
|
+
* Defaults to false. Observe-only telemetry continues to fire
|
|
98
|
+
* regardless of this setting.
|
|
99
|
+
*
|
|
100
|
+
* Requires `proxyUrl` (or `TENSORCOST_PROXY_URL` env var) to be set;
|
|
101
|
+
* throws `TensorCostConfigError` at `wrap()` time if neither is present.
|
|
102
|
+
*
|
|
103
|
+
* Not yet supported for AWS Bedrock clients — `wrap()` will throw
|
|
104
|
+
* immediately if you enable applied mode with a Bedrock client.
|
|
105
|
+
*/
|
|
106
|
+
appliedMode?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* TensorCost inference-proxy base URL used in applied mode.
|
|
109
|
+
* Falls back to `TENSORCOST_PROXY_URL`. When `appliedMode=true` and
|
|
110
|
+
* neither is set, `wrap()` throws `TensorCostConfigError`.
|
|
111
|
+
*
|
|
112
|
+
* Example: `https://proxy.tensorcost.com`
|
|
113
|
+
*/
|
|
114
|
+
proxyUrl?: string;
|
|
115
|
+
}
|
|
116
|
+
export interface ResolvedConfig {
|
|
117
|
+
apiKey: string;
|
|
118
|
+
baseUrl: string;
|
|
119
|
+
tenantId: string | null;
|
|
120
|
+
/**
|
|
121
|
+
* Phase A4 batch 3 — resolved environment tag. `null` when the
|
|
122
|
+
* customer did not configure one; the SDK omits the `environment`
|
|
123
|
+
* field from the envelope in that case so the backend's default
|
|
124
|
+
* ('production') applies.
|
|
125
|
+
*/
|
|
126
|
+
environment: string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Phase A4 batch 4 — resolved provider-connection identifier.
|
|
129
|
+
* `null` when the customer did not configure one; the SDK omits the
|
|
130
|
+
* `connection_id` field from the envelope in that case.
|
|
131
|
+
*/
|
|
132
|
+
connectionId: string | null;
|
|
133
|
+
failOpen: boolean;
|
|
134
|
+
/** Resolved applied-mode flag. */
|
|
135
|
+
appliedMode: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Resolved proxy base URL. Non-null only when `appliedMode=true`.
|
|
138
|
+
* Guaranteed by config resolution — `wrap()` throws before returning
|
|
139
|
+
* a config with `appliedMode=true` and `proxyUrl=null`.
|
|
140
|
+
*/
|
|
141
|
+
proxyUrl: string | null;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG,kBAAkB,GAAG,aAAa,GAAG,UAAU,CAAC;AAExE,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;;OAKG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,kCAAkC;IAClC,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal type defs for the TensorCost Node SDK.
|
|
3
|
+
*
|
|
4
|
+
* Kept intentionally narrow — the wrap() entry-point is generic over
|
|
5
|
+
* the customer's client type, so most provider shapes are duck-typed
|
|
6
|
+
* at runtime rather than expressed in TypeScript.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
package/dist/wrap.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single public entry point: `wrap(client, options?)`.
|
|
3
|
+
*
|
|
4
|
+
* Returns the SAME client reference (mutates in place), per the SDK
|
|
5
|
+
* ergonomics non-negotiable: customer changes one line, no other code
|
|
6
|
+
* paths shift.
|
|
7
|
+
*
|
|
8
|
+
* Applied mode (Layer 2): when `options.appliedMode=true`, the client's
|
|
9
|
+
* baseURL is permanently redirected to the TensorCost inference-proxy and
|
|
10
|
+
* two custom headers (`x-tc-provider-url`, `x-tc-provider-auth`) are
|
|
11
|
+
* injected into `client.defaultHeaders`. The proxy handles per-request
|
|
12
|
+
* routing decisions. Observe-only telemetry still fires post-call.
|
|
13
|
+
*/
|
|
14
|
+
import type { WrapOptions } from "./types.js";
|
|
15
|
+
export declare function wrap<T>(client: T, options?: WrapOptions): T;
|
|
16
|
+
//# sourceMappingURL=wrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrap.d.ts","sourceRoot":"","sources":["../src/wrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,GAAE,WAAgB,GAAG,CAAC,CAyC/D"}
|
package/dist/wrap.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single public entry point: `wrap(client, options?)`.
|
|
3
|
+
*
|
|
4
|
+
* Returns the SAME client reference (mutates in place), per the SDK
|
|
5
|
+
* ergonomics non-negotiable: customer changes one line, no other code
|
|
6
|
+
* paths shift.
|
|
7
|
+
*
|
|
8
|
+
* Applied mode (Layer 2): when `options.appliedMode=true`, the client's
|
|
9
|
+
* baseURL is permanently redirected to the TensorCost inference-proxy and
|
|
10
|
+
* two custom headers (`x-tc-provider-url`, `x-tc-provider-auth`) are
|
|
11
|
+
* injected into `client.defaultHeaders`. The proxy handles per-request
|
|
12
|
+
* routing decisions. Observe-only telemetry still fires post-call.
|
|
13
|
+
*/
|
|
14
|
+
import { resolveConfig } from "./config.js";
|
|
15
|
+
import { ObservationTransport } from "./transport.js";
|
|
16
|
+
import { detectProvider } from "./providers/detect.js";
|
|
17
|
+
import { installAppliedMode } from "./applied.js";
|
|
18
|
+
import * as openaiProvider from "./providers/openai.js";
|
|
19
|
+
import * as anthropicProvider from "./providers/anthropic.js";
|
|
20
|
+
export function wrap(client, options = {}) {
|
|
21
|
+
// resolveConfig throws MissingConfigError when apiKey is absent, and
|
|
22
|
+
// TensorCostConfigError when appliedMode=true with no proxyUrl.
|
|
23
|
+
// Both are intentional non-fail-open cases — see config.ts header.
|
|
24
|
+
const config = resolveConfig(options);
|
|
25
|
+
const transport = new ObservationTransport({
|
|
26
|
+
baseUrl: config.baseUrl,
|
|
27
|
+
apiKey: config.apiKey,
|
|
28
|
+
failOpen: config.failOpen,
|
|
29
|
+
});
|
|
30
|
+
const provider = detectProvider(client);
|
|
31
|
+
// Applied mode: redirect the client to the proxy before installing
|
|
32
|
+
// the observe-only wrappers. Order matters — installAppliedMode mutates
|
|
33
|
+
// baseURL and defaultHeaders, and the provider wrappers capture the
|
|
34
|
+
// (now-modified) bound methods, so post-call observations correctly
|
|
35
|
+
// attribute the response to the original provider model even though
|
|
36
|
+
// the network call went through the proxy.
|
|
37
|
+
if (config.appliedMode && config.proxyUrl !== null) {
|
|
38
|
+
installAppliedMode(client, config.proxyUrl, provider);
|
|
39
|
+
}
|
|
40
|
+
if (provider === "openai") {
|
|
41
|
+
openaiProvider.install(client, transport, config.tenantId, config.environment, config.connectionId);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
anthropicProvider.install(client, transport, config.tenantId, config.environment, config.connectionId);
|
|
45
|
+
}
|
|
46
|
+
return client;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=wrap.js.map
|
package/dist/wrap.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrap.js","sourceRoot":"","sources":["../src/wrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,cAAc,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,iBAAiB,MAAM,0BAA0B,CAAC;AAG9D,MAAM,UAAU,IAAI,CAAI,MAAS,EAAE,UAAuB,EAAE;IAC1D,qEAAqE;IACrE,gEAAgE;IAChE,mEAAmE;IACnE,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,IAAI,oBAAoB,CAAC;QACzC,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;KAC1B,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IAExC,mEAAmE;IACnE,wEAAwE;IACxE,oEAAoE;IACpE,oEAAoE;IACpE,oEAAoE;IACpE,2CAA2C;IAC3C,IAAI,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QACnD,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACxD,CAAC;IAED,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,cAAc,CAAC,OAAO,CACpB,MAAM,EACN,SAAS,EACT,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,YAAY,CACpB,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,iBAAiB,CAAC,OAAO,CACvB,MAAM,EACN,SAAS,EACT,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,WAAW,EAClB,MAAM,CAAC,YAAY,CACpB,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tensorcost/sdk",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "One-line wrapper SDK for TensorCost — fire-and-forget LLM observability for OpenAI and Anthropic clients.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"llm",
|
|
27
|
+
"openai",
|
|
28
|
+
"anthropic",
|
|
29
|
+
"observability",
|
|
30
|
+
"cost",
|
|
31
|
+
"tensorcost"
|
|
32
|
+
],
|
|
33
|
+
"author": "Vaadh Labs / TensorCost <engineering@tensorcost.com>",
|
|
34
|
+
"homepage": "https://tensorcost.com",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -b",
|
|
37
|
+
"dev": "tsc -b --watch",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run --coverage",
|
|
40
|
+
"lint": "echo 'no lint configured'"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"@vitest/coverage-v8": "^2.1",
|
|
45
|
+
"typescript": "^5.6.0",
|
|
46
|
+
"vitest": "^2.1.0"
|
|
47
|
+
}
|
|
48
|
+
}
|