@to11ai/sdk 1.1.0 → 2.0.0-rc.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.
@@ -9,7 +9,7 @@ export interface CacheConfig {
9
9
 
10
10
  /**
11
11
  * The cache stores either a legacy `resolve()` result or an enriched
12
- * `fetch()` result. Both are JSON-cloneable; the cache is shape-agnostic.
12
+ * `render()` result. Both are JSON-cloneable; the cache is shape-agnostic.
13
13
  */
14
14
  export type CacheableResolved = ResolvedPrompt | FetchedPrompt;
15
15
 
@@ -44,7 +44,7 @@ export function buildCacheKey(
44
44
  }
45
45
 
46
46
  /**
47
- * Build a cache key for an env-aware `fetch()` call.
47
+ * Build a cache key for an env-aware `render()` call.
48
48
  *
49
49
  * Folds in `env` and `subject` ON TOP of the base
50
50
  * project/slug/labels/variables key so two calls that differ only by
@@ -64,12 +64,9 @@ export function buildFetchCacheKey(
64
64
  env: string,
65
65
  subject?: string,
66
66
  variables?: Record<string, unknown>,
67
- developerRole?: string,
68
67
  ): string {
69
68
  const base = buildCacheKey(projectId, slug, [env], variables);
70
- // `developerRole` partitions the cache; coalesce the default so omitted and
71
- // `"user"` (the documented default) share one entry, not two.
72
- return `fetch|${base}|${JSON.stringify(env)}|${JSON.stringify(subject ?? null)}|${JSON.stringify(developerRole ?? "user")}`;
69
+ return `fetch|${base}|${JSON.stringify(env)}|${JSON.stringify(subject ?? null)}`;
73
70
  }
74
71
 
75
72
  function sortObjectKeys(obj: unknown): unknown {
@@ -4,6 +4,12 @@ import { zResolvedPrompt } from "../generated/zod.gen";
4
4
  import type { ApiEnvelope, Transport } from "../transport";
5
5
  import type { CacheConfig } from "./cache";
6
6
  import { buildCacheKey, buildFetchCacheKey, PromptCache } from "./cache";
7
+ import type {
8
+ AnthropicRenderedPrompt,
9
+ OpenAiRenderedPrompt,
10
+ RenderResult,
11
+ } from "./shape";
12
+ import { shapePrompt } from "./shape";
7
13
  import type {
8
14
  ArchivePromptInput,
9
15
  ArchivePromptResult,
@@ -40,10 +46,13 @@ import type {
40
46
  PaginatedResult,
41
47
  RenderedChatMessage,
42
48
  RenderedPrompt,
49
+ RenderFormat,
50
+ RenderOptions,
43
51
  ResolvedPrompt,
44
52
  ResolvedPromptEnvelope,
45
53
  ResolvePromptInput,
46
54
  ResolvePromptResult,
55
+ UnknownRoleMode,
47
56
  UpdateProjectPromptLabelInput,
48
57
  UpdateProjectPromptLabelResult,
49
58
  UpdatePromptInput,
@@ -120,11 +129,12 @@ const zResolvedPromptLegacy = zResolvedPrompt.pick({
120
129
  promptId: true,
121
130
  versionId: true,
122
131
  version: true,
123
- labels: true,
132
+ label: true,
124
133
  contentHash: true,
125
134
  rendered: true,
126
135
  tools: true,
127
136
  toolChoice: true,
137
+ modelConfig: true,
128
138
  });
129
139
 
130
140
  const zResolvedPromptEnvelope = zResolvedPrompt
@@ -177,8 +187,9 @@ function normalizeResolvedEnvelope(
177
187
  promptId: raw.promptId,
178
188
  versionId: raw.versionId,
179
189
  contentHash: raw.contentHash,
180
- labels: raw.labels,
190
+ label: raw.label,
181
191
  rendered: raw.rendered,
192
+ modelConfig: provenance.modelConfig,
182
193
  };
183
194
  // `tools` is present IFF the resolved version declares tool definitions; the
184
195
  // wire may send `null` (or omit it) — normalize both to absent so callers
@@ -209,7 +220,7 @@ function normalizeResolvedEnvelope(
209
220
 
210
221
  /**
211
222
  * Lift a caller-supplied offline `fallback` into the enriched `FetchedPrompt`
212
- * shape (D7), so `fetch()` returns an identical shape on the online and
223
+ * shape (D7), so `render()` returns an identical shape on the online and
213
224
  * offline paths. An already-enriched `FetchedPrompt` is returned as-is; a
214
225
  * legacy `ResolvedPrompt` is lifted (messages synthesized; `releaseId` → its
215
226
  * own value or explicit `null`; `blockRenderRecord` → `{}`; `variantName`
@@ -261,34 +272,48 @@ function buildLabelEventsQuery(input: ListLabelEventsInput): string {
261
272
  }
262
273
 
263
274
  /**
264
- * Init-time config for the env-aware `fetch()` surface (TO11-980 / S9).
275
+ * Init-time config for the env-aware `render()` surface (TO11-980 / S9).
265
276
  *
266
277
  * `projectId` and `env` come from `createClient`. `projectId` is required for
267
- * `fetch()` (it builds the project-scoped resolve path); `env` is the default
268
- * serving environment inherited by every `fetch(slug)` call.
278
+ * `render()` (it builds the project-scoped resolve path); `env` is the default
279
+ * serving environment inherited by every `render(slug)` call.
269
280
  */
270
281
  export interface PromptResourceConfig {
271
- /** Project the client is bound to. Required for `fetch()`; validated at createClient. */
282
+ /** Project the client is bound to. Required for `render()`; validated at createClient. */
272
283
  projectId?: string;
273
- /** Default serving env for `fetch()`. Validated against ENV_SLUG_REGEX at createClient. */
284
+ /** Default serving env for `render()`. Validated against ENV_SLUG_REGEX at createClient. */
274
285
  env?: string;
286
+ /** Default provider shape for `render()` output. Per-call override allowed. */
287
+ format?: RenderFormat;
288
+ /**
289
+ * Default unknown-role policy for `render({ format })` output conversion.
290
+ * Per-call override allowed; defaults to `"drop"`.
291
+ */
292
+ onUnknownRole?: UnknownRoleMode;
275
293
  }
276
294
 
277
- export class PromptResource {
295
+ export class PromptResource<F extends RenderFormat | undefined = undefined> {
278
296
  private readonly transport: Transport;
279
297
  private readonly cache: PromptCache;
280
298
  private readonly projectId: string | undefined;
281
299
  private readonly env: string | undefined;
300
+ private readonly format: RenderFormat | undefined;
301
+ private readonly onUnknownRole: UnknownRoleMode | undefined;
282
302
 
283
303
  constructor(
284
304
  transport: Transport,
285
305
  cacheConfig?: CacheConfig,
286
- config?: PromptResourceConfig,
306
+ // `format` is bound to the class generic `F` (not the wider `RenderFormat`)
307
+ // so the constructed default shape cannot disagree with what `render()`'s
308
+ // return type (`RenderResult<F>`) promises.
309
+ config?: Omit<PromptResourceConfig, "format"> & { format?: F },
287
310
  ) {
288
311
  this.transport = transport;
289
312
  this.cache = new PromptCache(cacheConfig);
290
313
  this.projectId = config?.projectId;
291
314
  this.env = config?.env;
315
+ this.format = config?.format;
316
+ this.onUnknownRole = config?.onUnknownRole;
292
317
  }
293
318
 
294
319
  // ─── Create ──────────────────────────────────────────────────────────────
@@ -436,35 +461,65 @@ export class PromptResource {
436
461
  return unwrap(envelope);
437
462
  }
438
463
 
439
- // ─── Fetch (env-aware resolution, TO11-980 / S9) ──────────────────────────
464
+ // ─── Render (env-aware resolution + format shaping, TO11-980 / TO11-2691) ──
465
+
466
+ /**
467
+ * Resolve and (optionally) shape a prompt for the client's configured
468
+ * `projectId` + `env`, in one control-plane request. With a `format` (per
469
+ * call or inherited from the client) the result is provider-shaped
470
+ * `{ messages, config, metadata }`; without one it is the neutral
471
+ * `FetchedPrompt`. Resolves by the client's `env`; for label-addressed
472
+ * resolution see {@link PromptResource.resolve | `resolve()`}.
473
+ *
474
+ * @example Render an OpenAI-shaped prompt and send it through the gateway
475
+ * const prompt = await to11.prompts.render("weather-concierge", {
476
+ * variables: { city: "NYC" },
477
+ * });
478
+ * const res = await openai.chat.completions.create(
479
+ * { ...prompt.config, messages: prompt.messages },
480
+ * { headers: to11.turn().headers(prompt) },
481
+ * );
482
+ */
483
+ async render(
484
+ slug: string,
485
+ options: RenderOptions & { format: "openai" },
486
+ ): Promise<OpenAiRenderedPrompt>;
487
+ async render(
488
+ slug: string,
489
+ options: RenderOptions & { format: "anthropic" },
490
+ ): Promise<AnthropicRenderedPrompt>;
491
+ async render(slug: string, options?: RenderOptions): Promise<RenderResult<F>>;
492
+ async render(
493
+ slug: string,
494
+ options: RenderOptions = {},
495
+ ): Promise<FetchedPrompt | OpenAiRenderedPrompt | AnthropicRenderedPrompt> {
496
+ const fetched = await this.resolveToFetched(slug, options);
497
+ return shapePrompt(fetched, options.format ?? this.format, {
498
+ onUnknownRole: options.onUnknownRole ?? this.onUnknownRole,
499
+ });
500
+ }
440
501
 
441
502
  /**
442
- * Resolve a prompt for the client's configured `projectId` + `env`
443
- * (Style-A). The ergonomic slug-first surface: env is configured once at
444
- * `createClient` and inherited here, with an optional per-call override.
503
+ * Env-aware resolution → neutral `FetchedPrompt`. Carries the resolve /
504
+ * cache / offline-fallback logic; `render()` is the public shaping surface.
445
505
  *
446
- * - **env→label wire mapping (correction #3):** the resolve route is
447
- * label-based (`/v1/projects/:projectId/prompts:resolve`, body
448
- * `labels[]`); it does NOT accept a top-level `env` field. The resolved
449
- * env slug (lowercase-kebab — a valid label name) is therefore sent as
450
- * the single resolution label: body `{ slug, labels: [env], subject?,
451
- * variables? }`.
506
+ * - **env→label wire mapping:** the resolve route is label-based
507
+ * (`/v1/projects/:projectId/prompts:resolve`, body `labels[]`); the
508
+ * resolved env slug is sent as the single resolution label.
452
509
  * - **No silent fallback (R4):** if no env is resolvable (none at init AND
453
- * none per call) this throws `EnvNotFoundError` (`reason.source =
454
- * "missing"`) CLIENT-side, eagerly, before any transport call.
510
+ * none per call) this throws `EnvNotFoundError` CLIENT-side, eagerly,
511
+ * before any transport call.
455
512
  * - **Cache (D8):** caches `single`-strategy releases and weighted releases
456
513
  * WITH a `subject`; BYPASSES the cache for weighted releases with NO
457
514
  * `subject` (a uniform-random assignment must not be pinned for the TTL).
458
- *
459
- * Prefer this over the deprecated `resolve()`.
460
515
  */
461
- async fetch(
516
+ private async resolveToFetched(
462
517
  slug: string,
463
518
  options: FetchPromptOptions = {},
464
519
  ): Promise<FetchedPrompt> {
465
520
  if (this.projectId === undefined || this.projectId.trim() === "") {
466
521
  throw new Error(
467
- "[to11ai/sdk] `prompts.fetch()` requires a `projectId` on the client — pass it to createClient({ projectId, ... }).",
522
+ "[to11ai/sdk] `prompts.render()` requires a `projectId` on the client — pass it to createClient({ projectId, ... }).",
468
523
  );
469
524
  }
470
525
 
@@ -486,13 +541,7 @@ export class PromptResource {
486
541
  );
487
542
  }
488
543
 
489
- const {
490
- subject,
491
- variables,
492
- cache: useCache = true,
493
- fallback,
494
- developerRole,
495
- } = options;
544
+ const { subject, variables, cache: useCache = true, fallback } = options;
496
545
 
497
546
  const cacheKey = buildFetchCacheKey(
498
547
  this.projectId,
@@ -500,7 +549,6 @@ export class PromptResource {
500
549
  env,
501
550
  subject,
502
551
  variables,
503
- developerRole,
504
552
  );
505
553
 
506
554
  if (useCache) {
@@ -514,7 +562,6 @@ export class PromptResource {
514
562
  const body: Record<string, unknown> = { slug, labels: [env] };
515
563
  if (subject !== undefined) body.subject = subject;
516
564
  if (variables !== undefined) body.variables = variables;
517
- if (developerRole !== undefined) body.developerRole = developerRole;
518
565
 
519
566
  try {
520
567
  const envelope = await this.transport.request<ResolvedPromptEnvelope>(
@@ -561,12 +608,13 @@ export class PromptResource {
561
608
  // ─── Resolve ──────────────────────────────────────────────────────────────
562
609
 
563
610
  /**
564
- * @deprecated Use {@link PromptResource.fetch | `prompts.fetch(slug,
565
- * options?)`} instead. `resolve()` is env-agnostic (no `env` / `subject`,
566
- * no enriched release/variant provenance, no typed-env errors)
567
- * and is retained only for back-compat. It is NOT subject to the R4
568
- * no-silent-fallback env gate (it has no env concept). New code should
569
- * configure `projectId` + `env` at `createClient` and call `fetch()`.
611
+ * Resolve a prompt by explicit `labels` for a given `projectId`, in one
612
+ * control-plane request. This is the label-addressed counterpart to
613
+ * {@link PromptResource.render | `render()`}, which resolves by the client's
614
+ * configured `env`. Reach for `resolve()` when you address prompts by label
615
+ * rather than by environment; it is env-agnostic, so it carries no `env` /
616
+ * `subject`, no enriched release/variant provenance, and no typed-env errors,
617
+ * and is not subject to the env-based no-silent-fallback gate.
570
618
  */
571
619
  async resolve(input: ResolvePromptInput): Promise<ResolvePromptResult> {
572
620
  const {
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Format-at-load shaping: turn a neutral `FetchedPrompt` into a provider-shaped
3
+ * `{ messages, config, metadata }` result the caller spreads into their own
4
+ * provider `create()` call. Header-only — this only reshapes data, it never
5
+ * sends a request. See docs/product-specs/2026-07-14-to11-2691-sdk-simpler-setup.md.
6
+ */
7
+
8
+ import {
9
+ type AnthropicMessage,
10
+ type AnthropicTool,
11
+ type AnthropicToolChoice,
12
+ type ConvertMessagesOptions,
13
+ type OpenAIChatMessage,
14
+ type OpenAITool,
15
+ type OpenAIToolChoice,
16
+ toAnthropicMessages,
17
+ toAnthropicSystem,
18
+ toAnthropicToolChoice,
19
+ toAnthropicTools,
20
+ toOpenAIMessages,
21
+ toOpenAIToolChoice,
22
+ toOpenAITools,
23
+ } from "../gateway";
24
+ import type { FetchedPrompt, RenderFormat } from "./types";
25
+
26
+ /** Model params passed through verbatim from the resolved version's modelConfig. */
27
+ export interface PromptConfig {
28
+ model?: string;
29
+ temperature?: number;
30
+ // Snake_case to match the stored modelConfig and the provider wire key
31
+ // (spread verbatim into e.g. OpenAI's `create()`), per TO11-2691.
32
+ max_tokens?: number;
33
+ /** Any additional stored model-config keys are passed through unchanged. */
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ /** Prompt provenance for `turn.headers()` — kept apart from the request body. */
38
+ export interface PromptMetadata {
39
+ promptId: string;
40
+ versionId: string;
41
+ version: number;
42
+ releaseId: string | null;
43
+ /** The single label the prompt resolved through (env slug today). */
44
+ label: string;
45
+ variantName?: string;
46
+ }
47
+
48
+ /** OpenAI-shaped render result: spread `{ ...config, messages }` into create(). */
49
+ export interface OpenAiRenderedPrompt {
50
+ messages: OpenAIChatMessage[];
51
+ config: PromptConfig;
52
+ metadata: PromptMetadata;
53
+ /** Authored tools, provider-shaped. Present only when the prompt declares tools. */
54
+ tools?: OpenAITool[];
55
+ /** Authored tool-choice directive. Present only when the prompt sets one. */
56
+ tool_choice?: OpenAIToolChoice;
57
+ }
58
+
59
+ /** Anthropic-shaped render result: system/developer folded into `system`. */
60
+ export interface AnthropicRenderedPrompt {
61
+ /** Folded system/developer instruction text (Anthropic's top-level `system`). */
62
+ system?: string;
63
+ messages: AnthropicMessage[];
64
+ config: PromptConfig;
65
+ metadata: PromptMetadata;
66
+ /** Authored tools, provider-shaped. Present only when the prompt declares tools. */
67
+ tools?: AnthropicTool[];
68
+ /** Authored tool-choice directive. Present only when the prompt sets one. */
69
+ tool_choice?: AnthropicToolChoice;
70
+ }
71
+
72
+ /** The render return type for a given client/call format. */
73
+ export type RenderResult<F extends RenderFormat | undefined> =
74
+ F extends "openai"
75
+ ? OpenAiRenderedPrompt
76
+ : F extends "anthropic"
77
+ ? AnthropicRenderedPrompt
78
+ : FetchedPrompt;
79
+
80
+ function toPromptConfig(modelConfig: unknown): PromptConfig {
81
+ return modelConfig !== null && typeof modelConfig === "object"
82
+ ? { ...(modelConfig as Record<string, unknown>) }
83
+ : {};
84
+ }
85
+
86
+ function toMetadata(prompt: FetchedPrompt): PromptMetadata {
87
+ const metadata: PromptMetadata = {
88
+ promptId: prompt.promptId,
89
+ versionId: prompt.versionId,
90
+ version: prompt.version,
91
+ releaseId: prompt.releaseId,
92
+ label: prompt.label,
93
+ };
94
+ if (prompt.variantName !== undefined)
95
+ metadata.variantName = prompt.variantName;
96
+ return metadata;
97
+ }
98
+
99
+ export function shapeOpenAI(
100
+ prompt: FetchedPrompt,
101
+ options: ConvertMessagesOptions = {},
102
+ ): OpenAiRenderedPrompt {
103
+ const result: OpenAiRenderedPrompt = {
104
+ messages: toOpenAIMessages(prompt.messages, options),
105
+ config: toPromptConfig(prompt.modelConfig),
106
+ metadata: toMetadata(prompt),
107
+ };
108
+ // Carry authored tools so the spread-ready result is complete for
109
+ // tool-using prompts. Omit when empty/absent so we never send `tools: []`.
110
+ const tools = toOpenAITools(prompt.tools);
111
+ if (tools.length > 0) result.tools = tools;
112
+ const toolChoice = toOpenAIToolChoice(prompt.toolChoice);
113
+ if (toolChoice !== undefined) result.tool_choice = toolChoice;
114
+ return result;
115
+ }
116
+
117
+ export function shapeAnthropic(
118
+ prompt: FetchedPrompt,
119
+ options: ConvertMessagesOptions = {},
120
+ ): AnthropicRenderedPrompt {
121
+ const result: AnthropicRenderedPrompt = {
122
+ messages: toAnthropicMessages(prompt.messages, options),
123
+ config: toPromptConfig(prompt.modelConfig),
124
+ metadata: toMetadata(prompt),
125
+ };
126
+ const system = toAnthropicSystem(prompt.messages);
127
+ if (system !== undefined) result.system = system;
128
+ const tools = toAnthropicTools(prompt.tools);
129
+ if (tools.length > 0) result.tools = tools;
130
+ const toolChoice = toAnthropicToolChoice(prompt.toolChoice);
131
+ if (toolChoice !== undefined) result.tool_choice = toolChoice;
132
+ return result;
133
+ }
134
+
135
+ /** Shape a fetched prompt for `format`, or return it neutral when unset. */
136
+ export function shapePrompt(
137
+ prompt: FetchedPrompt,
138
+ format: RenderFormat | undefined,
139
+ options: ConvertMessagesOptions = {},
140
+ ): FetchedPrompt | OpenAiRenderedPrompt | AnthropicRenderedPrompt {
141
+ switch (format) {
142
+ case "openai":
143
+ return shapeOpenAI(prompt, options);
144
+ case "anthropic":
145
+ return shapeAnthropic(prompt, options);
146
+ default:
147
+ return prompt;
148
+ }
149
+ }
@@ -38,6 +38,18 @@ export type RenderedChatMessage = GenRenderedChatPrompt["messages"][number];
38
38
 
39
39
  export type RenderedPrompt = GenRenderedPrompt;
40
40
 
41
+ /** Provider shape for `prompts.render()` output. Omit for the neutral result. */
42
+ export type RenderFormat = "openai" | "anthropic";
43
+
44
+ /**
45
+ * Policy for a message whose role is outside the known set
46
+ * (`system | user | assistant | developer | tool`) when converting to a
47
+ * provider shape. `"drop"` (default) omits it silently; `"warn"` omits it and
48
+ * emits a `console.warn`; `"error"` throws `UnknownRoleError`. Applies to the
49
+ * standalone converters and to `render({ format })`.
50
+ */
51
+ export type UnknownRoleMode = "drop" | "warn" | "error";
52
+
41
53
  /**
42
54
  * A provider-neutral tool definition (`{ name, description?, parameters }`),
43
55
  * sourced from authored tool-definition blocks. Surfaced on the resolved
@@ -56,11 +68,12 @@ export type ResolvedPrompt = Pick<
56
68
  | "promptId"
57
69
  | "versionId"
58
70
  | "version"
59
- | "labels"
71
+ | "label"
60
72
  | "contentHash"
61
73
  | "rendered"
62
74
  | "tools"
63
75
  | "toolChoice"
76
+ | "modelConfig"
64
77
  >;
65
78
 
66
79
  export interface PaginationInfo {
@@ -190,8 +203,19 @@ export interface FetchedPrompt {
190
203
  promptId: string;
191
204
  versionId: string;
192
205
  contentHash: string;
193
- labels: string[];
206
+ /**
207
+ * The single label the prompt resolved through — the env slug today, a real
208
+ * label once labels ship. Surfaced as `x-to11-prompt-label` by
209
+ * `gatewayPromptHeaders` / `turn.headers()` (TO11-2725).
210
+ */
211
+ label: string;
194
212
  rendered: RenderedPrompt;
213
+ /**
214
+ * The served version's model config (model, temperature, max_tokens, …),
215
+ * passed through verbatim. Surfaced as `config` by `render({ format })`.
216
+ * Optional: absent when a pre-`modelConfig` API omits it.
217
+ */
218
+ modelConfig?: unknown;
195
219
  /**
196
220
  * Provider-neutral tool definitions lifted from authored tool-definition
197
221
  * blocks. Absent (rather than `[]`) when the resolved version declares no
@@ -215,26 +239,23 @@ export type ResolvedPromptEnvelope = GenResolvedPrompt & {
215
239
  strategy?: "single" | "weighted";
216
240
  };
217
241
 
218
- /**
219
- * How an authored `developer` block is rendered. The caller controls this
220
- * per-call because it knows the upstream it targets. Omitted ⇒ `"user"`
221
- * (back-compatible; safe for any upstream). `"developer"` preserves the role
222
- * (OpenAI); `"system"` folds it into a system message.
223
- */
224
- export type DeveloperRole = "user" | "developer" | "system";
225
-
226
242
  export interface FetchPromptOptions {
227
243
  env?: string;
228
244
  subject?: string;
229
245
  variables?: Record<string, unknown>;
230
246
  cache?: boolean;
231
247
  fallback?: ResolvedPrompt | FetchedPrompt;
248
+ }
249
+
250
+ export interface RenderOptions extends FetchPromptOptions {
251
+ /** Provider shape for this call's output; overrides the client default. */
252
+ format?: RenderFormat;
232
253
  /**
233
- * How authored `developer` blocks render. Default `"user"`. Forwarded on the
234
- * resolve request and folded into the cache key so two calls differing only
235
- * by `developerRole` never share a cached result.
254
+ * How to handle a message with an unrecognized role while shaping for a
255
+ * provider (`format` set). Overrides the client default; defaults to `"drop"`.
256
+ * Only meaningful with a `format` — the neutral result runs no converter.
236
257
  */
237
- developerRole?: DeveloperRole;
258
+ onUnknownRole?: UnknownRoleMode;
238
259
  }
239
260
 
240
261
  export type PaletteToken = ProjectPromptLabel["displayColor"];