@to11ai/sdk 0.0.2 → 0.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.
- package/LICENSE +373 -0
- package/README.md +91 -1
- package/dist/client.cjs +20 -5
- package/dist/client.d.ts +10 -1
- package/dist/client.js +26 -2
- package/dist/errors.cjs +89 -1
- package/dist/errors.d.ts +96 -0
- package/dist/errors.js +89 -1
- package/dist/gateway.cjs +8 -1
- package/dist/gateway.d.ts +19 -0
- package/dist/gateway.js +8 -1
- package/dist/projectEnvironments/resource.cjs +110 -0
- package/dist/projectEnvironments/resource.d.ts +67 -0
- package/dist/projectEnvironments/resource.js +70 -0
- package/dist/projectEnvironments/types.d.ts +121 -0
- package/dist/prompts/resource.cjs +81 -0
- package/dist/prompts/resource.d.ts +61 -2
- package/dist/prompts/resource.js +81 -0
- package/dist/prompts/types.d.ts +217 -3
- package/dist/routingRules/resource.cjs +131 -0
- package/dist/routingRules/resource.d.ts +22 -0
- package/dist/routingRules/resource.js +91 -0
- package/dist/routingRules/types.d.ts +169 -0
- package/dist/traceparent.d.ts +61 -0
- package/dist/transport.d.ts +6 -1
- package/package.json +25 -5
- package/src/client.ts +60 -1
- package/src/errors.ts +172 -0
- package/src/gateway.ts +29 -1
- package/src/projectEnvironments/resource.ts +220 -0
- package/src/projectEnvironments/types.ts +151 -0
- package/src/prompts/resource.ts +232 -0
- package/src/prompts/types.ts +270 -3
- package/src/routingRules/resource.ts +180 -0
- package/src/routingRules/types.ts +222 -0
- package/src/traceparent.ts +124 -0
- package/src/transport.ts +191 -5
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { ApiEnvelope, Transport } from "../transport";
|
|
2
|
+
import type {
|
|
3
|
+
CreateRuleVersionInput,
|
|
4
|
+
CreateRuleVersionResult,
|
|
5
|
+
GetRuleVersionInput,
|
|
6
|
+
GetRuleVersionResult,
|
|
7
|
+
ListRuleReleasesInput,
|
|
8
|
+
ListRuleReleasesResult,
|
|
9
|
+
ListRuleVersionsInput,
|
|
10
|
+
ListRuleVersionsResult,
|
|
11
|
+
RollbackRuleReleaseInput,
|
|
12
|
+
RollbackRuleReleaseResult,
|
|
13
|
+
SetRuleReleaseInput,
|
|
14
|
+
SetRuleReleaseResult,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
CreateRuleVersionInput,
|
|
19
|
+
CreateRuleVersionResult,
|
|
20
|
+
GetRuleVersionInput,
|
|
21
|
+
GetRuleVersionResult,
|
|
22
|
+
ListRuleReleasesInput,
|
|
23
|
+
ListRuleReleasesResult,
|
|
24
|
+
ListRuleVersionsInput,
|
|
25
|
+
ListRuleVersionsResult,
|
|
26
|
+
RollbackRuleReleaseInput,
|
|
27
|
+
RollbackRuleReleaseResult,
|
|
28
|
+
RuleModelBinding,
|
|
29
|
+
RuleProviderRef,
|
|
30
|
+
RuleRelease,
|
|
31
|
+
RuleReleaseActorType,
|
|
32
|
+
RuleReleaseApprovalStatus,
|
|
33
|
+
RuleReleaseChangeKind,
|
|
34
|
+
RuleReleaseStrategy,
|
|
35
|
+
RuleReleaseWeight,
|
|
36
|
+
RuleSnapshot,
|
|
37
|
+
RuleStrategy,
|
|
38
|
+
RuleVersion,
|
|
39
|
+
SetRuleReleaseInput,
|
|
40
|
+
SetRuleReleaseResult,
|
|
41
|
+
} from "./types";
|
|
42
|
+
|
|
43
|
+
function unwrap<T>(envelope: ApiEnvelope<T>): T {
|
|
44
|
+
if (envelope.data === null || envelope.data === undefined) {
|
|
45
|
+
throw new Error("API returned success but data is null");
|
|
46
|
+
}
|
|
47
|
+
return envelope.data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rulePath(projectId: string, ruleId: string): string {
|
|
51
|
+
return `/v1/projects/${encodeURIComponent(projectId)}/routing/rules/${encodeURIComponent(ruleId)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function versionsPath(projectId: string, ruleId: string): string {
|
|
55
|
+
return `${rulePath(projectId, ruleId)}/versions`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function versionPath(
|
|
59
|
+
projectId: string,
|
|
60
|
+
ruleId: string,
|
|
61
|
+
versionId: string,
|
|
62
|
+
): string {
|
|
63
|
+
return `${versionsPath(projectId, ruleId)}/${encodeURIComponent(versionId)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function releasesPath(projectId: string, ruleId: string): string {
|
|
67
|
+
return `${rulePath(projectId, ruleId)}/releases`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function releasePath(
|
|
71
|
+
projectId: string,
|
|
72
|
+
ruleId: string,
|
|
73
|
+
envSlug: string,
|
|
74
|
+
): string {
|
|
75
|
+
return `${releasesPath(projectId, ruleId)}/${encodeURIComponent(envSlug)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function releaseRollbackPath(
|
|
79
|
+
projectId: string,
|
|
80
|
+
ruleId: string,
|
|
81
|
+
envSlug: string,
|
|
82
|
+
): string {
|
|
83
|
+
return `${releasePath(projectId, ruleId, envSlug)}/rollback`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class RoutingRuleVersionResource {
|
|
87
|
+
private readonly transport: Transport;
|
|
88
|
+
|
|
89
|
+
constructor(transport: Transport) {
|
|
90
|
+
this.transport = transport;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async create(
|
|
94
|
+
input: CreateRuleVersionInput,
|
|
95
|
+
): Promise<CreateRuleVersionResult> {
|
|
96
|
+
const { projectId, ruleId, ...body } = input;
|
|
97
|
+
const envelope = await this.transport.request<CreateRuleVersionResult>(
|
|
98
|
+
"POST",
|
|
99
|
+
versionsPath(projectId, ruleId),
|
|
100
|
+
{ body },
|
|
101
|
+
);
|
|
102
|
+
return unwrap(envelope);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async list(input: ListRuleVersionsInput): Promise<ListRuleVersionsResult> {
|
|
106
|
+
const query: Record<string, string | number | undefined> = {};
|
|
107
|
+
if (input.cursor !== undefined) query.cursor = input.cursor;
|
|
108
|
+
if (input.limit !== undefined) query.limit = input.limit;
|
|
109
|
+
const envelope = await this.transport.request<ListRuleVersionsResult>(
|
|
110
|
+
"GET",
|
|
111
|
+
versionsPath(input.projectId, input.ruleId),
|
|
112
|
+
{ query },
|
|
113
|
+
);
|
|
114
|
+
return unwrap(envelope);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async get(input: GetRuleVersionInput): Promise<GetRuleVersionResult> {
|
|
118
|
+
const envelope = await this.transport.request<GetRuleVersionResult>(
|
|
119
|
+
"GET",
|
|
120
|
+
versionPath(input.projectId, input.ruleId, input.versionId),
|
|
121
|
+
);
|
|
122
|
+
return unwrap(envelope);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class RoutingRuleReleaseResource {
|
|
127
|
+
private readonly transport: Transport;
|
|
128
|
+
|
|
129
|
+
constructor(transport: Transport) {
|
|
130
|
+
this.transport = transport;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async set(input: SetRuleReleaseInput): Promise<SetRuleReleaseResult> {
|
|
134
|
+
const { projectId, ruleId, envSlug, ...body } = input;
|
|
135
|
+
const envelope = await this.transport.request<SetRuleReleaseResult>(
|
|
136
|
+
"PUT",
|
|
137
|
+
releasePath(projectId, ruleId, envSlug),
|
|
138
|
+
{ body },
|
|
139
|
+
);
|
|
140
|
+
return unwrap(envelope);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async rollback(
|
|
144
|
+
input: RollbackRuleReleaseInput,
|
|
145
|
+
): Promise<RollbackRuleReleaseResult> {
|
|
146
|
+
const { projectId, ruleId, envSlug, ...body } = input;
|
|
147
|
+
const envelope = await this.transport.request<RollbackRuleReleaseResult>(
|
|
148
|
+
"POST",
|
|
149
|
+
releaseRollbackPath(projectId, ruleId, envSlug),
|
|
150
|
+
{ body },
|
|
151
|
+
);
|
|
152
|
+
return unwrap(envelope);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async list(input: ListRuleReleasesInput): Promise<ListRuleReleasesResult> {
|
|
156
|
+
const query: Record<string, string | number | undefined> = {};
|
|
157
|
+
if (input.cursor !== undefined) query.cursor = input.cursor;
|
|
158
|
+
if (input.limit !== undefined) query.limit = input.limit;
|
|
159
|
+
const path =
|
|
160
|
+
input.envSlug !== undefined
|
|
161
|
+
? releasePath(input.projectId, input.ruleId, input.envSlug)
|
|
162
|
+
: releasesPath(input.projectId, input.ruleId);
|
|
163
|
+
const envelope = await this.transport.request<ListRuleReleasesResult>(
|
|
164
|
+
"GET",
|
|
165
|
+
path,
|
|
166
|
+
{ query },
|
|
167
|
+
);
|
|
168
|
+
return unwrap(envelope);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export class RoutingRulesResource {
|
|
173
|
+
readonly versions: RoutingRuleVersionResource;
|
|
174
|
+
readonly releases: RoutingRuleReleaseResource;
|
|
175
|
+
|
|
176
|
+
constructor(transport: Transport) {
|
|
177
|
+
this.versions = new RoutingRuleVersionResource(transport);
|
|
178
|
+
this.releases = new RoutingRuleReleaseResource(transport);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Routing Rule Versions + Env Pointers wire types (TO11-795 / llm-routing §S17).
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Spec: docs/product-specs/2026-05-14-llm-routing-env-pointers.md
|
|
5
|
+
// Plan: docs/exec-plans/active/2026-05-14-llm-routing-env-pointers-plan.md §S17
|
|
6
|
+
//
|
|
7
|
+
// Hand-rolled TS mirrors of the Effect Schema types defined in
|
|
8
|
+
// `apps/api/src/domains/llm-routing/model/rule-version.ts` and the wire
|
|
9
|
+
// schemas exported from `apps/api/src/infrastructure/http/routes/rule-versions.ts`.
|
|
10
|
+
// The published `@to11ai/sdk` package intentionally does NOT depend on the
|
|
11
|
+
// internal `@to11/api` workspace package — keep these in lockstep with the
|
|
12
|
+
// API model when adding fields.
|
|
13
|
+
//
|
|
14
|
+
// Credential invariant (per spec §"Data model"): `snapshotJson` MUST NOT
|
|
15
|
+
// carry plaintext credentials. The wire shape declares it as `unknown` to
|
|
16
|
+
// mirror the domain — provider references live inside `modelBindings[]` and
|
|
17
|
+
// the gateway resolves credentials lazily at request time.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
// ─── Snapshot shape ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reference to a provider config bound to a specific environment. Mirrors
|
|
24
|
+
* `RuleProviderRef` in the API domain model. `envSlug` is optional — when
|
|
25
|
+
* omitted, the gateway falls back to the project's `is_gateway_fallback`
|
|
26
|
+
* environment.
|
|
27
|
+
*/
|
|
28
|
+
export interface RuleProviderRef {
|
|
29
|
+
providerId: string;
|
|
30
|
+
envSlug?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Per-variant model binding inside a rule snapshot. Identifies which
|
|
35
|
+
* `(provider, model)` pair serves a strategy variant; the gateway resolves
|
|
36
|
+
* the actual credential at request time. `weight` is intentionally
|
|
37
|
+
* permissive (0..100 in v1) so weighted/experimental strategies can evolve
|
|
38
|
+
* without a SDK churn.
|
|
39
|
+
*/
|
|
40
|
+
export interface RuleModelBinding {
|
|
41
|
+
modelId: string;
|
|
42
|
+
provider: RuleProviderRef;
|
|
43
|
+
weight?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type RuleStrategy = "single" | "weighted" | "fallback" | "experiment";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Frozen rule snapshot — the JSONB payload stored in
|
|
50
|
+
* `rule_versions.snapshot_json`. Mirrors the API domain `RuleSnapshot`:
|
|
51
|
+
*
|
|
52
|
+
* * `alias` — optional `function::<alias>` indirection (S1).
|
|
53
|
+
* * `strategy` — `single`, `weighted`, `fallback`, `experiment`.
|
|
54
|
+
* * `endpoint` — `chat`, `embeddings`, …
|
|
55
|
+
* * `variants[]` | `steps[]` | `chain[]` — strategy-shape arrays.
|
|
56
|
+
* * `when` — optional condition AST that gates the rule.
|
|
57
|
+
* * `retry` — optional `{ maxRetries?, backoffBaseMs?, retryableStatusCodes? }`.
|
|
58
|
+
* * `modelBindings[]` — which `(providerId, modelId, envSlug)` serves each variant.
|
|
59
|
+
* * `providerRefs[]` — flat denormalized list of referenced providers.
|
|
60
|
+
*
|
|
61
|
+
* No credential field exists; the gateway resolves credentials lazily from
|
|
62
|
+
* the per-env `routing_snapshots` ciphertext.
|
|
63
|
+
*/
|
|
64
|
+
export interface RuleSnapshot {
|
|
65
|
+
alias?: string;
|
|
66
|
+
strategy: RuleStrategy | string;
|
|
67
|
+
endpoint: string;
|
|
68
|
+
variants?: unknown[];
|
|
69
|
+
steps?: unknown[];
|
|
70
|
+
chain?: unknown[];
|
|
71
|
+
when?: unknown;
|
|
72
|
+
retry?: {
|
|
73
|
+
maxRetries?: number;
|
|
74
|
+
backoffBaseMs?: number;
|
|
75
|
+
retryableStatusCodes?: number[];
|
|
76
|
+
};
|
|
77
|
+
modelBindings?: RuleModelBinding[];
|
|
78
|
+
providerRefs?: RuleProviderRef[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Persisted shapes ──────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The persisted form of a rule version row as returned by
|
|
85
|
+
* `GET /v1/projects/:projectId/routing/rules/:ruleId/versions/:versionId`.
|
|
86
|
+
*
|
|
87
|
+
* `version` is monotonic per `ruleId` — the server allocates it inside a
|
|
88
|
+
* `SELECT MAX … FOR UPDATE` transaction. `snapshotJson` is typed as
|
|
89
|
+
* `unknown` to mirror the API contract (the storage path persists JSONB
|
|
90
|
+
* verbatim); callers that need the typed shape should cast through
|
|
91
|
+
* `RuleSnapshot`.
|
|
92
|
+
*/
|
|
93
|
+
export interface RuleVersion {
|
|
94
|
+
id: string;
|
|
95
|
+
ruleId: string;
|
|
96
|
+
version: number;
|
|
97
|
+
snapshotJson: unknown;
|
|
98
|
+
description: string | null;
|
|
99
|
+
createdBy: string;
|
|
100
|
+
/** ISO-8601 timestamp. */
|
|
101
|
+
createdAt: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Inputs ─────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wire input accepted by
|
|
108
|
+
* `POST /v1/projects/:p/routing/rules/:ruleId/versions`. Requires the
|
|
109
|
+
* `routing:write` capability.
|
|
110
|
+
*
|
|
111
|
+
* `snapshot` is the cleartext snapshot JSON the gateway uses to resolve
|
|
112
|
+
* runtime traffic — stored verbatim in `rule_versions.snapshot_json`.
|
|
113
|
+
* `description` is an optional commit message; the API surfaces it on the
|
|
114
|
+
* audit timeline.
|
|
115
|
+
*/
|
|
116
|
+
export interface CreateRuleVersionInput {
|
|
117
|
+
projectId: string;
|
|
118
|
+
ruleId: string;
|
|
119
|
+
snapshot: RuleSnapshot | unknown;
|
|
120
|
+
description?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type CreateRuleVersionResult = RuleVersion;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Cursor-paginated list input. `cursor` is the `version` number BELOW which
|
|
127
|
+
* to fetch (keyset pagination — pass the last item's `version` to continue).
|
|
128
|
+
* `limit` is clamped server-side to `MAX_LIST_LIMIT` (200 at the time of
|
|
129
|
+
* writing); default is 50.
|
|
130
|
+
*/
|
|
131
|
+
export interface ListRuleVersionsInput {
|
|
132
|
+
projectId: string;
|
|
133
|
+
ruleId: string;
|
|
134
|
+
/**
|
|
135
|
+
* The `version` number BELOW which to fetch (the route uses a keyset
|
|
136
|
+
* `WHERE version < cursor` clause and returns versions in `version DESC`
|
|
137
|
+
* order). Omit for the first page.
|
|
138
|
+
*/
|
|
139
|
+
cursor?: number;
|
|
140
|
+
/** Page size. Server clamps to `[1, MAX_LIST_LIMIT]`. */
|
|
141
|
+
limit?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type ListRuleVersionsResult = RuleVersion[];
|
|
145
|
+
|
|
146
|
+
export interface GetRuleVersionInput {
|
|
147
|
+
projectId: string;
|
|
148
|
+
ruleId: string;
|
|
149
|
+
versionId: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type GetRuleVersionResult = RuleVersion;
|
|
153
|
+
|
|
154
|
+
// ─── Releases (S2 / TO11-1009) ──────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export type RuleReleaseStrategy = "single" | "weighted";
|
|
157
|
+
|
|
158
|
+
export type RuleReleaseChangeKind =
|
|
159
|
+
| "release_initial"
|
|
160
|
+
| "release_promoted"
|
|
161
|
+
| "release_rolled_back"
|
|
162
|
+
| "release_weight_changed";
|
|
163
|
+
|
|
164
|
+
export type RuleReleaseApprovalStatus = "committed" | "pending" | "rejected";
|
|
165
|
+
|
|
166
|
+
export type RuleReleaseActorType = "user" | "api_key" | "worker";
|
|
167
|
+
|
|
168
|
+
export interface RuleReleaseWeight {
|
|
169
|
+
versionId: string;
|
|
170
|
+
weight: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface RuleRelease {
|
|
174
|
+
id: string;
|
|
175
|
+
ruleId: string;
|
|
176
|
+
environmentId: string;
|
|
177
|
+
envSlug: string;
|
|
178
|
+
strategy: RuleReleaseStrategy;
|
|
179
|
+
versionId: string | null;
|
|
180
|
+
weights: RuleReleaseWeight[] | null;
|
|
181
|
+
changeKind: RuleReleaseChangeKind;
|
|
182
|
+
priorReleaseId: string | null;
|
|
183
|
+
actorId: string;
|
|
184
|
+
actorType: RuleReleaseActorType;
|
|
185
|
+
reason: string | null;
|
|
186
|
+
approvalStatus: RuleReleaseApprovalStatus;
|
|
187
|
+
activeFrom: string | null;
|
|
188
|
+
activeTo: string | null;
|
|
189
|
+
createdAt: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface SetRuleReleaseInput {
|
|
193
|
+
projectId: string;
|
|
194
|
+
ruleId: string;
|
|
195
|
+
envSlug: string;
|
|
196
|
+
strategy: RuleReleaseStrategy;
|
|
197
|
+
versionId?: string;
|
|
198
|
+
weights?: RuleReleaseWeight[];
|
|
199
|
+
reason?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type SetRuleReleaseResult = RuleRelease;
|
|
203
|
+
|
|
204
|
+
export interface RollbackRuleReleaseInput {
|
|
205
|
+
projectId: string;
|
|
206
|
+
ruleId: string;
|
|
207
|
+
envSlug: string;
|
|
208
|
+
reason: string;
|
|
209
|
+
targetReleaseId?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export type RollbackRuleReleaseResult = RuleRelease;
|
|
213
|
+
|
|
214
|
+
export interface ListRuleReleasesInput {
|
|
215
|
+
projectId: string;
|
|
216
|
+
ruleId: string;
|
|
217
|
+
envSlug?: string;
|
|
218
|
+
cursor?: string;
|
|
219
|
+
limit?: number;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export type ListRuleReleasesResult = RuleRelease[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3C Trace Context propagation for outbound SDK calls.
|
|
3
|
+
*
|
|
4
|
+
* Reads the host process's active OpenTelemetry span — if any — and formats a
|
|
5
|
+
* W3C `traceparent` (and optional `tracestate`) string for injection as
|
|
6
|
+
* outbound HTTP headers. Lets customers correlate their tracer's spans with
|
|
7
|
+
* the gateway-emitted span we record.
|
|
8
|
+
*
|
|
9
|
+
* Design constraints (from TO11-1016 / S8c spec):
|
|
10
|
+
* 1. NO runtime dependency on `@opentelemetry/api`. The package is detected
|
|
11
|
+
* at runtime via a try/require shim; if it is not loadable, this module
|
|
12
|
+
* is a strict no-op and `activeTraceContext()` returns `null`.
|
|
13
|
+
* 2. NO SDK config surface change. There is no constructor option or env
|
|
14
|
+
* var; behavior is invisible if the host has no OTel installed.
|
|
15
|
+
* 3. Header omitted, not zero-filled — W3C §3.2.2.5 forbids all-zero
|
|
16
|
+
* trace/span ids in `traceparent`. We return `null` (caller omits).
|
|
17
|
+
*
|
|
18
|
+
* @see https://www.w3.org/TR/trace-context/ for the wire format.
|
|
19
|
+
* @see docs/product-specs/2026-05-20-to11-1016-sdk-traceparent-propagation.md
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Minimal structural typing of the OTel API surface we actually use. Keeping
|
|
23
|
+
// this local — rather than importing types from `@opentelemetry/api` — is
|
|
24
|
+
// what keeps the SDK zero-dep at the TypeScript layer too. Source types:
|
|
25
|
+
// https://github.com/open-telemetry/opentelemetry-js-api/
|
|
26
|
+
interface OtelTraceState {
|
|
27
|
+
serialize(): string;
|
|
28
|
+
}
|
|
29
|
+
interface OtelSpanContext {
|
|
30
|
+
traceId: string;
|
|
31
|
+
spanId: string;
|
|
32
|
+
traceFlags: number;
|
|
33
|
+
traceState?: OtelTraceState;
|
|
34
|
+
}
|
|
35
|
+
interface OtelSpan {
|
|
36
|
+
spanContext(): OtelSpanContext;
|
|
37
|
+
}
|
|
38
|
+
interface OtelTrace {
|
|
39
|
+
getActiveSpan(): OtelSpan | undefined;
|
|
40
|
+
}
|
|
41
|
+
interface OtelApi {
|
|
42
|
+
trace: OtelTrace;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type OtelLoader = () => OtelApi | null;
|
|
46
|
+
|
|
47
|
+
const ZERO_TRACE_ID = "0".repeat(32);
|
|
48
|
+
const ZERO_SPAN_ID = "0".repeat(16);
|
|
49
|
+
|
|
50
|
+
let cached: OtelApi | null | undefined; // undefined = not yet probed
|
|
51
|
+
let loader: OtelLoader = defaultLoader;
|
|
52
|
+
|
|
53
|
+
function defaultLoader(): OtelApi | null {
|
|
54
|
+
try {
|
|
55
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic require shim — see header comment
|
|
56
|
+
const g = globalThis as any;
|
|
57
|
+
// biome-ignore lint/security/noGlobalEval: runtime-detected optional peer dep; bundlers cannot statically resolve
|
|
58
|
+
const r = (g.require ?? eval("require")) as (id: string) => unknown;
|
|
59
|
+
return r("@opentelemetry/api") as OtelApi;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the W3C `traceparent` (and optional `tracestate`) string pair for
|
|
67
|
+
* the currently active OpenTelemetry span in the host process, or `null` if
|
|
68
|
+
* no active span exists or `@opentelemetry/api` is not installed.
|
|
69
|
+
*
|
|
70
|
+
* Idempotent and side-effect-free past the one-time module probe. Callers
|
|
71
|
+
* inject the returned strings as outbound HTTP headers when non-null.
|
|
72
|
+
*/
|
|
73
|
+
export function activeTraceContext(): {
|
|
74
|
+
traceparent: string;
|
|
75
|
+
tracestate?: string;
|
|
76
|
+
} | null {
|
|
77
|
+
if (cached === undefined) cached = loader();
|
|
78
|
+
if (cached === null) return null;
|
|
79
|
+
|
|
80
|
+
let span: OtelSpan | undefined;
|
|
81
|
+
let ctx: OtelSpanContext;
|
|
82
|
+
try {
|
|
83
|
+
span = cached.trace.getActiveSpan();
|
|
84
|
+
if (!span) return null;
|
|
85
|
+
ctx = span.spanContext();
|
|
86
|
+
} catch {
|
|
87
|
+
// A broken / partially-installed OTel SDK must not break customer
|
|
88
|
+
// outbound calls. Degrade to no-op.
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { traceId, spanId } = ctx;
|
|
93
|
+
if (!traceId || !spanId) return null;
|
|
94
|
+
if (traceId === ZERO_TRACE_ID || spanId === ZERO_SPAN_ID) return null;
|
|
95
|
+
|
|
96
|
+
const flags = ((ctx.traceFlags ?? 0) & 0xff).toString(16).padStart(2, "0");
|
|
97
|
+
const traceparent = `00-${traceId}-${spanId}-${flags}`;
|
|
98
|
+
|
|
99
|
+
let tracestate: string | undefined;
|
|
100
|
+
try {
|
|
101
|
+
const raw = ctx.traceState?.serialize();
|
|
102
|
+
if (typeof raw === "string" && raw.length > 0) tracestate = raw;
|
|
103
|
+
} catch {
|
|
104
|
+
// Same defensive posture — never let a vendor tracestate bug
|
|
105
|
+
// surface as a request failure.
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return tracestate === undefined
|
|
109
|
+
? { traceparent }
|
|
110
|
+
: { traceparent, tracestate };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Test-only seam: install a deterministic OTel loader and reset the
|
|
115
|
+
* module-level cache. Underscore-prefixed and intentionally NOT re-exported
|
|
116
|
+
* from the package entry-point.
|
|
117
|
+
*
|
|
118
|
+
* Production code never calls this. Tests call it to simulate
|
|
119
|
+
* "OTel installed" / "OTel absent" / "broken OTel install" cells.
|
|
120
|
+
*/
|
|
121
|
+
export function _loadOtelForTests(injected: OtelLoader | undefined): void {
|
|
122
|
+
loader = injected ?? defaultLoader;
|
|
123
|
+
cached = undefined;
|
|
124
|
+
}
|