@to11ai/sdk 2.0.0-rc.3 → 2.0.0-rc.5

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/src/client.ts CHANGED
@@ -8,46 +8,30 @@ import {
8
8
  } from "./context";
9
9
  import { gatewayAuthHeaders } from "./gateway";
10
10
  import { ProjectEnvironmentResource } from "./projectEnvironments/resource";
11
- import type { CacheConfig } from "./prompts/cache";
12
11
  import { PromptResource } from "./prompts/resource";
13
12
  import {
14
13
  ENV_SLUG_REGEX,
15
14
  type RenderFormat,
16
15
  type UnknownRoleMode,
17
16
  } from "./prompts/types";
18
- import { RoutingRulesResource } from "./routingRules/resource";
19
17
  import { Transport } from "./transport";
20
18
 
21
- // SDK-behavior delivery (TO11-2100): the resolution store + helpers.
22
- export {
23
- BehaviorStore,
24
- type BehaviorStoreOptions,
25
- envVarNameForKey,
26
- type SdkBehaviorEnvelope,
27
- } from "./behaviors/store";
28
19
  export type { TurnAuthContext } from "./context";
29
20
  export { Conversation, Session, Turn } from "./context";
30
21
  // Re-export all error types for convenience
31
22
  export type {
32
- AmbiguousEnvLabelsReason,
33
- EnvNotFoundReason,
34
23
  NoActiveReleaseReason,
35
- NoApplicableRouteReason,
36
24
  PromptNotFoundReason,
37
25
  PromptPolicyViolationReason,
38
26
  } from "./errors";
39
27
  export {
40
28
  // TO11-980 (S9) prompt-resolution typed errors.
41
- AmbiguousEnvLabelsError,
42
- EnvNotEnabledForProviderError,
43
- EnvNotFoundError,
29
+ LabelNotFoundError,
30
+ MissingLabelError,
44
31
  NoActiveReleaseError,
45
- NoApplicableRouteError,
46
32
  PromptLabelNotRegisteredError,
47
33
  PromptNotFoundError,
48
34
  PromptPolicyViolationError,
49
- RuleNotFoundError,
50
- RuleVersionInvalidSnapshotError,
51
35
  To11aiApiError,
52
36
  To11aiAuthError,
53
37
  To11aiError,
@@ -75,7 +59,6 @@ export type {
75
59
  UpdateProjectEnvironmentResult,
76
60
  } from "./projectEnvironments/resource";
77
61
  export { ProjectEnvironmentResource } from "./projectEnvironments/resource";
78
- export type { CacheConfig } from "./prompts/cache";
79
62
  export type { PromptResourceConfig } from "./prompts/resource";
80
63
  export { PromptResource } from "./prompts/resource";
81
64
  export type {
@@ -92,42 +75,9 @@ export type {
92
75
  RenderedPrompt,
93
76
  RenderFormat,
94
77
  RenderOptions,
95
- ResolvedPrompt,
96
- ResolvedPromptEnvelope,
97
78
  UnknownRoleMode,
98
79
  } from "./prompts/types";
99
80
  export { ENV_SLUG_REGEX } from "./prompts/types";
100
- export type {
101
- CreateRuleVersionInput,
102
- CreateRuleVersionResult,
103
- GetRuleVersionInput,
104
- GetRuleVersionResult,
105
- ListRuleReleasesInput,
106
- ListRuleReleasesResult,
107
- ListRuleVersionsInput,
108
- ListRuleVersionsResult,
109
- RollbackRuleReleaseInput,
110
- RollbackRuleReleaseResult,
111
- RuleModelBinding,
112
- RuleProviderRef,
113
- RuleRelease,
114
- RuleReleaseActorType,
115
- RuleReleaseApprovalStatus,
116
- RuleReleaseChangeKind,
117
- RuleReleaseStrategy,
118
- RuleReleaseWeight,
119
- RuleSnapshot,
120
- RuleStrategy,
121
- RuleVersion,
122
- SetRuleReleaseInput,
123
- SetRuleReleaseResult,
124
- } from "./routingRules/resource";
125
- export {
126
- RoutingRuleReleaseResource,
127
- RoutingRulesResource,
128
- RoutingRuleVersionResource,
129
- } from "./routingRules/resource";
130
- export type { Transport } from "./transport";
131
81
 
132
82
  // ─── Client options ────────────────────────────────────────────────────────
133
83
 
@@ -143,12 +93,11 @@ export interface ClientOptions {
143
93
  */
144
94
  gatewayUrl?: string;
145
95
  /**
146
- * API key for server-side use. Precedence: this option → `TO11_API_KEY` env.
147
- * Rejected in browser contexts — use `getAccessToken` there.
96
+ * API key for server-side use (server, CLI, backend job). Precedence: this
97
+ * option → `TO11_API_KEY` env. Required. Rejected in browser contexts — a
98
+ * to11 API key carries gateway access and must never ship in client code.
148
99
  */
149
100
  apiKey?: string;
150
- /** Token callback for browser / edge contexts — avoids static secrets. */
151
- getAccessToken?: () => Promise<string>;
152
101
  /**
153
102
  * Provider API key (BYOK) carried by `openaiOptions()` / `anthropicOptions()`
154
103
  * as the provider client's `apiKey`, so a BYOK call is a single
@@ -183,17 +132,10 @@ export interface ClientOptions {
183
132
  * via `render(slug, { onUnknownRole })`.
184
133
  */
185
134
  onUnknownRole?: UnknownRoleMode;
186
- /** LRU cache configuration for render(). Defaults: ttlSeconds=300, maxEntries=100 */
187
- cache?: CacheConfig;
188
135
  /** Request timeout in milliseconds. Default: 30_000 */
189
136
  timeoutMs?: number;
190
137
  /** Max retry attempts for network errors / 5xx. Default: 3 */
191
138
  maxRetries?: number;
192
- /**
193
- * Purely local overrides for server-evaluated SDK-behavior flags (TO11-2100).
194
- * An init option here wins over a `TO11_SDK_BEHAVIOR_*` env var. No LaunchDarkly.
195
- */
196
- behaviorOverrides?: Record<string, unknown>;
197
139
  }
198
140
 
199
141
  // ─── Provider-client options (openaiOptions / anthropicOptions) ─────────────
@@ -230,19 +172,10 @@ export type ProviderClientOptions<
230
172
  export interface To11aiClient<F extends RenderFormat | undefined = undefined> {
231
173
  readonly prompts: PromptResource<F>;
232
174
  readonly projectEnvironments: ProjectEnvironmentResource;
233
- readonly routingRules: RoutingRulesResource;
234
- /**
235
- * Resolve a server-evaluated SDK-behavior flag value (TO11-2100) under the
236
- * fixed precedence: local override (init option → `TO11_SDK_BEHAVIOR_*` env
237
- * var) → server-delivered value (captured from responses the client has
238
- * already made) → the `codedDefault` passed here. A value whose runtime type
239
- * disagrees with `codedDefault` falls through to the next tier. The SDK
240
- * carries no LaunchDarkly dependency; this only reads what the server
241
- * delivered on the envelope.
242
- */
243
- readonly resolveBehavior: <T>(key: string, codedDefault: T) => T;
244
- /** Resolved API key (server mode); `undefined` in browser (`getAccessToken`) mode. */
245
- readonly apiKey: string | undefined;
175
+ /** Resolved API key (option → `TO11_API_KEY`). */
176
+ readonly apiKey: string;
177
+ /** Resolved control-plane base URL (option → `TO11_API_URL` → default). */
178
+ readonly baseUrl: string;
246
179
  /** Resolved data-plane gateway URL (option → `TO11_GATEWAY_URL` → default). */
247
180
  readonly gatewayUrl: string;
248
181
  /**
@@ -251,6 +184,11 @@ export interface To11aiClient<F extends RenderFormat | undefined = undefined> {
251
184
  * already default to it, so you don't need to pass it back into calls.
252
185
  */
253
186
  readonly projectId: string | undefined;
187
+ /**
188
+ * Resolved default serving environment (option → `TO11_ENV`); `undefined`
189
+ * when none is configured. `prompts.render()` already defaults to it.
190
+ */
191
+ readonly env: string | undefined;
254
192
  /** A new session (unit of work). Pass an id to group telemetry by your own id. */
255
193
  session(id?: string): Session;
256
194
  /**
@@ -299,9 +237,8 @@ export interface To11aiClient<F extends RenderFormat | undefined = undefined> {
299
237
  * - URLs (`baseUrl` / `gatewayUrl`) — option → env var → built-in default.
300
238
  * - `apiKey` / `projectId` — option → env var (no default).
301
239
  * - `env` / `format` — option only.
302
- * Auth mode:
303
- * - `apiKey` (or `TO11_API_KEY`) → server-side; rejected in browser contexts.
304
- * - `getAccessToken` → browser-safe; token fetched per request.
240
+ * Authenticate with `apiKey` (or `TO11_API_KEY`) — required, and server-side
241
+ * only (server, CLI, backend job); constructing with `apiKey` in a browser throws.
305
242
  *
306
243
  * @example Minimal setup (single call)
307
244
  * import { createClient } from "@to11ai/sdk";
@@ -371,15 +308,7 @@ export function createClient<
371
308
  )
372
309
  .replace(/\/+$/, "")
373
310
  .replace(/\/v1$/, "");
374
- const getAccessToken = options.getAccessToken;
375
- // Explicit token auth suppresses an ambient `TO11_API_KEY` so a shared env
376
- // (SSR / CI / edge) can't defeat an explicit `getAccessToken`. Only an
377
- // *explicit* `options.apiKey` alongside `getAccessToken` stays ambiguous and
378
- // is rejected by the exactly-one check below.
379
- const apiKey =
380
- getAccessToken !== undefined
381
- ? options.apiKey
382
- : pick(options.apiKey, "TO11_API_KEY");
311
+ const apiKey = pick(options.apiKey, "TO11_API_KEY");
383
312
  const projectId = pick(options.projectId, "TO11_PROJECT_ID");
384
313
  // Default serving env: explicit option → `TO11_ENV` env var — the same
385
314
  // `pick(option, "TO11_*")` resolution `apiKey`/`projectId` use (empty option
@@ -395,7 +324,7 @@ export function createClient<
395
324
  // undefined in the browser, so only the explicit option can reach here.
396
325
  if (isBrowser && options.apiKey !== undefined) {
397
326
  throw new Error(
398
- "[to11ai/sdk] Do not use `apiKey` in browser contexts — use `getAccessToken` instead.",
327
+ "[to11ai/sdk] Do not use `apiKey` in a browser — a to11 API key carries gateway access and must stay server-side (server, CLI, or backend job).",
399
328
  );
400
329
  }
401
330
 
@@ -422,23 +351,19 @@ export function createClient<
422
351
  );
423
352
  }
424
353
 
425
- // Exactly one auth source after resolution.
426
- const hasApiKey = apiKey !== undefined;
427
- const hasGetAccessToken = getAccessToken !== undefined;
428
- if (hasApiKey === hasGetAccessToken) {
354
+ // `apiKey` is the only auth source and is required.
355
+ if (apiKey === undefined) {
429
356
  throw new Error(
430
- "[to11ai/sdk] Provide exactly one of `apiKey` (or `TO11_API_KEY`) or `getAccessToken`.",
357
+ "[to11ai/sdk] An `apiKey` (or `TO11_API_KEY`) is required.",
431
358
  );
432
359
  }
433
360
 
434
- const auth = hasApiKey
435
- ? ({ apiKey: apiKey as string } as const)
436
- : ({ getAccessToken: getAccessToken as () => Promise<string> } as const);
361
+ const auth = { apiKey } as const;
437
362
 
438
- const behaviorStore = new BehaviorStore({
439
- initOverrides: options.behaviorOverrides,
440
- env: processEnv,
441
- });
363
+ // The SDK-behavior store (TO11-2100) still captures any server-delivered
364
+ // flags off responses, but the public `resolveBehavior` accessor is hidden
365
+ // for the 2.0 surface — kept internal so it can be re-exposed step by step.
366
+ const behaviorStore = new BehaviorStore({ env: processEnv });
442
367
 
443
368
  const transport = new Transport({
444
369
  baseUrl,
@@ -448,24 +373,18 @@ export function createClient<
448
373
  behaviorStore,
449
374
  });
450
375
 
451
- const prompts = new PromptResource<F>(transport, options.cache, {
376
+ const prompts = new PromptResource<F>(transport, {
452
377
  projectId,
453
378
  env,
454
379
  format: options.format,
455
380
  onUnknownRole: options.onUnknownRole,
456
381
  });
457
382
  const projectEnvironments = new ProjectEnvironmentResource(transport);
458
- const routingRules = new RoutingRulesResource(transport);
459
383
 
460
384
  // ── Gateway observability context (session → conversation → turn) ──
461
- // These build gateway auth headers, so they require a server-side apiKey +
462
- // projectId; fail loudly rather than emit a broken header bag.
385
+ // These build gateway auth headers, so they require a projectId; fail loudly
386
+ // rather than emit a broken header bag.
463
387
  const gatewayAuthContext = (): TurnAuthContext => {
464
- if (apiKey === undefined) {
465
- throw new Error(
466
- "[to11ai/sdk] session()/turn()/openaiOptions() need an `apiKey` (or `TO11_API_KEY`) — gateway calls require a server-side key. In the browser use `getAccessToken` for control-plane calls only.",
467
- );
468
- }
469
388
  if (projectId === undefined) {
470
389
  throw new Error(
471
390
  "[to11ai/sdk] session()/turn() need a `projectId` (or `TO11_PROJECT_ID`).",
@@ -496,11 +415,6 @@ export function createClient<
496
415
  defaultBaseURL: string,
497
416
  overrides?: T,
498
417
  ): ProviderClientOptions<T> => {
499
- if (apiKey === undefined) {
500
- throw new Error(
501
- "[to11ai/sdk] openaiOptions()/anthropicOptions() need an `apiKey` (or `TO11_API_KEY`) — gateway calls require a server-side key. In the browser use `getAccessToken` for control-plane calls only.",
502
- );
503
- }
504
418
  if (projectId === undefined) {
505
419
  throw new Error(
506
420
  "[to11ai/sdk] openaiOptions()/anthropicOptions() need a `projectId` (or `TO11_PROJECT_ID`) — it authenticates the gateway call via `x-to11-project-id`.",
@@ -530,12 +444,11 @@ export function createClient<
530
444
  return {
531
445
  prompts,
532
446
  projectEnvironments,
533
- routingRules,
534
- resolveBehavior: (key, codedDefault) =>
535
- behaviorStore.resolve(key, codedDefault),
536
447
  apiKey,
448
+ baseUrl,
537
449
  gatewayUrl,
538
450
  projectId,
451
+ env,
539
452
  session: (id) => new Session(gatewayAuthContext(), id),
540
453
  conversation: (id) => clientDefaultSession().conversation(id),
541
454
  turn: () => clientDefaultSession().turn(),
package/src/context.ts CHANGED
@@ -49,10 +49,6 @@ function newId(): string {
49
49
  return crypto.randomUUID();
50
50
  }
51
51
 
52
- function provenanceOf(prompt: PromptForHeaders): GatewayPromptProvenance {
53
- return "metadata" in prompt ? prompt.metadata : prompt;
54
- }
55
-
56
52
  /**
57
53
  * One agent turn — a single trace. A turn's `traceparent` is fixed for its
58
54
  * lifetime, so every LLM-call made under it groups into one trace.
@@ -105,7 +101,9 @@ export class Turn {
105
101
  traceparent: this.traceparent,
106
102
  };
107
103
  if (prompt !== undefined) {
108
- Object.assign(headers, gatewayPromptHeaders(provenanceOf(prompt)));
104
+ // gatewayPromptHeaders accepts a RenderedPrompt or a format-shaped
105
+ // result and unwraps `metadata` itself.
106
+ Object.assign(headers, gatewayPromptHeaders(prompt));
109
107
  }
110
108
  return headers;
111
109
  }
package/src/errors.ts CHANGED
@@ -342,60 +342,54 @@ export class PromptNotFoundError extends To11aiApiError {
342
342
  }
343
343
 
344
344
  /**
345
- * Reason payload for `EnvNotFoundError`.
345
+ * Thrown when the label supplied to `prompts.render()` does not resolve to an
346
+ * existing (active) label on the server. Surfaced from the server's
347
+ * `ENV_NOT_FOUND` code; `label` carries the rejected slug when the server
348
+ * echoes it.
346
349
  *
347
- * `source` discriminates the two failure modes (D2):
348
- * - `"missing"` — no env was supplied at init OR at call time. Raised
349
- * CLIENT-side, eagerly, before any request (R4 / AC4); `env` is absent.
350
- * - `"unknown"` — an env was supplied but the server's registry has no
351
- * such (active) env. Surfaced from the `ENV_NOT_FOUND` server code; `env`
352
- * carries the rejected slug when the server echoes it.
350
+ * This is distinct from `MissingLabelError`, which is raised client-side when
351
+ * no label was supplied at all.
353
352
  */
354
- export interface EnvNotFoundReason {
355
- env?: string;
356
- source: "missing" | "unknown";
357
- }
358
-
359
- /**
360
- * Thrown when no serving environment can be resolved for a `prompts.render()`
361
- * call. One class, two `reason.source` discriminants (see
362
- * `EnvNotFoundReason`): client-eager `"missing"` (R4) and server-forwarded
363
- * `"unknown"` (`ENV_NOT_FOUND`).
364
- *
365
- * The client-eager variant carries no `statusCode`/`errorCode` from the wire
366
- * (no request was made); it reports status `0` and errorCode `ENV_NOT_FOUND`
367
- * for a uniform catch surface.
368
- */
369
- export class EnvNotFoundError extends To11aiApiError {
370
- readonly reason: EnvNotFoundReason;
353
+ export class LabelNotFoundError extends To11aiApiError {
354
+ /** The label slug that could not be found, when the server echoes it. */
355
+ readonly label?: string;
371
356
 
372
357
  constructor(
373
358
  message: string,
374
- reason: EnvNotFoundReason,
359
+ label: string | undefined,
375
360
  details: unknown,
376
- // Required (no misleading default): the client-eager `missing()` variant
377
- // passes 0 (no HTTP call) and the transport mapper passes the real server
378
- // status, so a hardcoded default would never reflect reality.
379
361
  statusCode: number,
380
362
  ) {
363
+ // The wire code is unchanged (`ENV_NOT_FOUND`); only the SDK class name
364
+ // is label-oriented, decoupled from the server error code.
381
365
  super(message, statusCode, "ENV_NOT_FOUND", details);
382
- this.name = "EnvNotFoundError";
383
- this.reason = reason;
366
+ this.name = "LabelNotFoundError";
367
+ this.label = label;
384
368
  Object.setPrototypeOf(this, new.target.prototype);
385
369
  }
386
370
 
387
- /** Build the client-eager (missing-at-both) variant — no transport call. */
388
- static missing(env?: string): EnvNotFoundError {
389
- return new EnvNotFoundError(
390
- "[to11ai/sdk] No environment resolvable for prompts.render(): none supplied at client init and none supplied per call. Pass `env` to createClient or render(slug, { env }).",
391
- { env, source: "missing" },
392
- undefined,
393
- 0,
394
- );
371
+ static is(err: unknown): err is LabelNotFoundError {
372
+ return err instanceof LabelNotFoundError;
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Thrown CLIENT-side, eagerly (before any request), when `prompts.render()` is
378
+ * called but no label is resolvable at all: none supplied per call, and no
379
+ * client `env` (or `TO11_ENV`) to fall back to. A usage error, not a lookup
380
+ * failure — distinct from `LabelNotFoundError`.
381
+ */
382
+ export class MissingLabelError extends To11aiError {
383
+ constructor(
384
+ message = "[to11ai/sdk] No label resolvable for prompts.render(): none supplied per call, and no client env (or TO11_ENV) to fall back to. Pass `label` to render(slug, { label }) or set `env` on createClient.",
385
+ ) {
386
+ super(message);
387
+ this.name = "MissingLabelError";
388
+ Object.setPrototypeOf(this, new.target.prototype);
395
389
  }
396
390
 
397
- static is(err: unknown): err is EnvNotFoundError {
398
- return err instanceof EnvNotFoundError;
391
+ static is(err: unknown): err is MissingLabelError {
392
+ return err instanceof MissingLabelError;
399
393
  }
400
394
  }
401
395
 
@@ -480,44 +474,6 @@ export class PromptPolicyViolationError extends To11aiApiError {
480
474
  }
481
475
  }
482
476
 
483
- /**
484
- * Reason payload for `AmbiguousEnvLabelsError`.
485
- *
486
- * Carried when the supplied env-label matched more than one candidate
487
- * version and the server cannot pick deterministically
488
- * (`AMBIGUOUS_ENV_LABELS`).
489
- */
490
- export interface AmbiguousEnvLabelsReason {
491
- env?: string;
492
- slug?: string;
493
- }
494
-
495
- /**
496
- * Thrown when the env→label resolution is ambiguous on the server
497
- * (`AMBIGUOUS_ENV_LABELS`) — the label matched multiple candidates and the
498
- * server declines to guess. Surfaces (rather than 500ing) so callers get a
499
- * typed, actionable error.
500
- */
501
- export class AmbiguousEnvLabelsError extends To11aiApiError {
502
- readonly reason: AmbiguousEnvLabelsReason;
503
-
504
- constructor(
505
- message: string,
506
- reason: AmbiguousEnvLabelsReason,
507
- details?: unknown,
508
- statusCode = 409,
509
- ) {
510
- super(message, statusCode, "AMBIGUOUS_ENV_LABELS", details);
511
- this.name = "AmbiguousEnvLabelsError";
512
- this.reason = reason;
513
- Object.setPrototypeOf(this, new.target.prototype);
514
- }
515
-
516
- static is(err: unknown): err is AmbiguousEnvLabelsError {
517
- return err instanceof AmbiguousEnvLabelsError;
518
- }
519
- }
520
-
521
477
  /**
522
478
  * Thrown when version create rejects the snapshot for a structural
523
479
  * reason — invalid variant params, malformed `when` clause, illegal
package/src/gateway.ts CHANGED
@@ -73,29 +73,26 @@ export function gatewayAuthHeaders(
73
73
  }
74
74
 
75
75
  /**
76
- * Build prompt provenance headers from a resolved prompt (TO11-980 / R7).
76
+ * Build prompt provenance headers from a `render()` result (or a format-shaped
77
+ * result's `metadata`, unwrapped automatically).
77
78
  *
78
- * Emits the full provenance set for every gateway-bound LLM request that
79
- * follows from a resolved prompt:
80
- * - `x-to11-prompt-id`, `x-to11-prompt-version` — always.
81
- * - `x-to11-release-id` — when the result carries a non-null `releaseId`
82
- * (the enriched `render()` result; a legacy `resolve()` result has no
83
- * `releaseId`, so it is omitted — non-breaking).
84
- * - `x-to11-variant-name` — IFF `variantName` is present (i.e. the resolved
85
- * release was weighted); omitted for `single`-strategy releases.
86
- * - `x-to11-prompt-label` — the single label the prompt resolved through (the
87
- * env slug today); omitted when empty (the gateway rejects empty values).
88
- * - `x-to11-prompt-slug` — the prompt's human-readable slug, so traces show a
89
- * readable name alongside the opaque `x-to11-prompt-id`; omitted when empty
90
- * or absent (a legacy `resolve()` result carries no slug).
79
+ * No provenance header is required, and the gateway rejects empty header
80
+ * values — so each header is emitted only when its value is present and valid,
81
+ * and the function never throws:
82
+ * - `x-to11-prompt-id` — a non-empty string.
83
+ * - `x-to11-prompt-version` — a finite number (a non-number/NaN is dropped
84
+ * rather than stringified to junk like `"[object Object]"`).
85
+ * - `x-to11-release-id` — a non-empty `releaseId` (the offline-fallback path
86
+ * can carry an explicit `null`, which is omitted).
87
+ * - `x-to11-variant-name` — present IFF the resolved release was weighted.
88
+ * - `x-to11-prompt-label` — the single label the prompt resolved through.
89
+ * - `x-to11-prompt-slug` — the prompt's human-readable slug.
91
90
  *
92
- * The parameter type is WIDENED to accept either the enriched `render()`
93
- * result or the legacy `ResolvedPrompt`. The new fields are read when present
94
- * and omitted otherwise, so an old `resolve()` result still produces exactly
95
- * the original three headers (non-breaking; D1/D7 shape coexistence).
91
+ * An input with no usable provenance yields an empty header bag — the same as
92
+ * a prompt-less `turn.headers()` call.
96
93
  *
97
94
  * @example
98
- * const result = await client.prompts.render(slug, { env: "prod" });
95
+ * const result = await client.prompts.render(slug, { label: "prod" });
99
96
  * const headers = gatewayPromptHeaders(result);
100
97
  */
101
98
  export interface GatewayPromptProvenance {
@@ -115,30 +112,38 @@ export interface GatewayPromptProvenance {
115
112
  }
116
113
 
117
114
  export function gatewayPromptHeaders(
118
- prompt: GatewayPromptProvenance,
115
+ prompt: GatewayPromptProvenance | { metadata: GatewayPromptProvenance },
119
116
  ): Record<string, string> {
120
- const headers: Record<string, string> = {
121
- "x-to11-prompt-id": prompt.promptId,
122
- "x-to11-prompt-version": String(prompt.version),
123
- };
124
- // Emit the release id only when it is a non-null, non-empty string — the
125
- // offline-fallback path can carry an explicit `null`, and the gateway
126
- // rejects empty header values.
127
- if (typeof prompt.releaseId === "string" && prompt.releaseId.length > 0) {
128
- headers["x-to11-release-id"] = prompt.releaseId;
117
+ // A format-shaped result carries provenance under `metadata`; unwrap it so
118
+ // the whole shaped result works, not just the bare provenance (mirrors the
119
+ // Python SDK).
120
+ const p = prompt != null && "metadata" in prompt ? prompt.metadata : prompt;
121
+ // No prompt provenance header is required, and the gateway rejects empty
122
+ // header values — so emit each one only when its value is present and valid,
123
+ // and never throw. An all-empty input just yields an empty header bag (the
124
+ // same as a prompt-less `turn.headers()` call).
125
+ const headers: Record<string, string> = {};
126
+ if (p == null) return headers;
127
+ if (typeof p.promptId === "string" && p.promptId.length > 0) {
128
+ headers["x-to11-prompt-id"] = p.promptId;
129
+ }
130
+ // A non-finite/non-number version would stringify to junk (e.g. NaN or
131
+ // "[object Object]"); only emit a real finite number.
132
+ if (typeof p.version === "number" && Number.isFinite(p.version)) {
133
+ headers["x-to11-prompt-version"] = String(p.version);
134
+ }
135
+ if (typeof p.releaseId === "string" && p.releaseId.length > 0) {
136
+ headers["x-to11-release-id"] = p.releaseId;
129
137
  }
130
- if (typeof prompt.variantName === "string" && prompt.variantName.length > 0) {
131
- headers["x-to11-variant-name"] = prompt.variantName;
138
+ // variantName is present IFF the resolved release was weighted.
139
+ if (typeof p.variantName === "string" && p.variantName.length > 0) {
140
+ headers["x-to11-variant-name"] = p.variantName;
132
141
  }
133
- // The single label the prompt resolved through. Omit when empty — the
134
- // gateway rejects empty header values.
135
- if (prompt.label.length > 0) {
136
- headers["x-to11-prompt-label"] = prompt.label;
142
+ if (typeof p.label === "string" && p.label.length > 0) {
143
+ headers["x-to11-prompt-label"] = p.label;
137
144
  }
138
- // The prompt's human-readable slug. Omit when absent/empty — a legacy
139
- // `resolve()` result carries no slug, and the gateway rejects empty values.
140
- if (typeof prompt.slug === "string" && prompt.slug.length > 0) {
141
- headers["x-to11-prompt-slug"] = prompt.slug;
145
+ if (typeof p.slug === "string" && p.slug.length > 0) {
146
+ headers["x-to11-prompt-slug"] = p.slug;
142
147
  }
143
148
  return headers;
144
149
  }
@@ -175,13 +180,25 @@ export interface OpenAIToolCall {
175
180
  function: { name: string; arguments: string };
176
181
  }
177
182
 
178
- /** OpenAI Chat Completions `messages[]` entry. */
179
- export interface OpenAIChatMessage {
180
- role: "system" | "developer" | "user" | "assistant" | "tool";
181
- content?: string | null;
182
- tool_calls?: OpenAIToolCall[];
183
- tool_call_id?: string;
184
- }
183
+ /**
184
+ * OpenAI Chat Completions `messages[]` entry — a per-role discriminated union
185
+ * matching what Chat Completions requires for each role, so an
186
+ * `OpenAIChatMessage[]` is structurally assignable to the `openai` package's
187
+ * `ChatCompletionMessageParam[]` (a `render({ format: "openai" })` result spreads
188
+ * straight into `openai.chat.completions.create({ messages })` with no cast).
189
+ * The SDK carries no dependency on the `openai` package — this only mirrors its
190
+ * shape. (Assignability is regression-guarded in `gateway.provider-types.test.ts`.)
191
+ */
192
+ export type OpenAIChatMessage =
193
+ | { role: "system"; content: string }
194
+ | { role: "developer"; content: string }
195
+ | { role: "user"; content: string }
196
+ | {
197
+ role: "assistant";
198
+ content?: string | null;
199
+ tool_calls?: OpenAIToolCall[];
200
+ }
201
+ | { role: "tool"; tool_call_id: string; content: string };
185
202
 
186
203
  /** Anthropic Messages `tools[]` entry. */
187
204
  export interface AnthropicTool {
@@ -405,24 +422,37 @@ export function toOpenAIMessages(
405
422
  return;
406
423
  }
407
424
  if (message.role === "assistant") {
408
- const msg: OpenAIChatMessage = {
409
- role: "assistant",
410
- content: message.content ?? null,
411
- };
412
- if (message.toolCalls != null && message.toolCalls.length > 0) {
413
- msg.tool_calls = message.toolCalls.map((call) => ({
414
- id: requireToolCallId(call.id),
415
- type: "function",
416
- function: {
417
- name: call.name,
418
- arguments: JSON.stringify(call.arguments),
419
- },
420
- }));
421
- }
422
- out.push(msg);
425
+ const content = message.content ?? null;
426
+ out.push(
427
+ message.toolCalls != null && message.toolCalls.length > 0
428
+ ? {
429
+ role: "assistant",
430
+ content,
431
+ tool_calls: message.toolCalls.map((call) => ({
432
+ id: requireToolCallId(call.id),
433
+ type: "function",
434
+ function: {
435
+ name: call.name,
436
+ arguments: JSON.stringify(call.arguments),
437
+ },
438
+ })),
439
+ }
440
+ : { role: "assistant", content },
441
+ );
423
442
  return;
424
443
  }
425
- out.push({ role: message.role, content: message.content ?? "" });
444
+ // Remaining plain-text roles: `system` | `developer` | `user`. Push a
445
+ // literal-role object per branch so each element lands on a single union
446
+ // member (a union-typed `role` wouldn't be assignable to the discriminated
447
+ // ChatCompletionMessageParam).
448
+ const content = message.content ?? "";
449
+ if (message.role === "user") {
450
+ out.push({ role: "user", content });
451
+ } else if (message.role === "developer") {
452
+ out.push({ role: "developer", content });
453
+ } else {
454
+ out.push({ role: "system", content });
455
+ }
426
456
  });
427
457
  return out;
428
458
  }