@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.
@@ -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
+ }