@to11ai/sdk 1.1.0 → 2.0.0-rc.2

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.
@@ -1,4 +1,4 @@
1
- import type { FetchedPrompt, ResolvedPrompt } from "./types";
1
+ import type { RenderedPrompt } from "./types";
2
2
 
3
3
  export interface CacheConfig {
4
4
  /** Time-to-live in seconds. Default: 300 (5 minutes) */
@@ -8,18 +8,17 @@ export interface CacheConfig {
8
8
  }
9
9
 
10
10
  /**
11
- * The cache stores either a legacy `resolve()` result or an enriched
12
- * `fetch()` result. Both are JSON-cloneable; the cache is shape-agnostic.
11
+ * The cache stores enriched `render()` results, which are JSON-cloneable.
13
12
  */
14
- export type CacheableResolved = ResolvedPrompt | FetchedPrompt;
13
+ export type CacheableRendered = RenderedPrompt;
15
14
 
16
15
  interface CacheEntry {
17
- value: CacheableResolved;
16
+ value: CacheableRendered;
18
17
  expiresAt: number;
19
18
  }
20
19
 
21
20
  /**
22
- * Build a canonical cache key for a resolve() call.
21
+ * Build a canonical base cache key from project/slug/labels/variables.
23
22
  *
24
23
  * - projectId is included to prevent cross-project cache collisions: a single
25
24
  * PromptResource can serve multiple projects, so the same slug+labels must
@@ -44,7 +43,7 @@ export function buildCacheKey(
44
43
  }
45
44
 
46
45
  /**
47
- * Build a cache key for an env-aware `fetch()` call.
46
+ * Build a cache key for an env-aware `render()` call.
48
47
  *
49
48
  * Folds in `env` and `subject` ON TOP of the base
50
49
  * project/slug/labels/variables key so two calls that differ only by
@@ -58,18 +57,15 @@ export function buildCacheKey(
58
57
  * caches as the same deterministic variant on repeat (D8); the
59
58
  * weighted-no-subject bypass is handled in the resource, not here.
60
59
  */
61
- export function buildFetchCacheKey(
60
+ export function buildRenderCacheKey(
62
61
  projectId: string,
63
62
  slug: string,
64
63
  env: string,
65
64
  subject?: string,
66
65
  variables?: Record<string, unknown>,
67
- developerRole?: string,
68
66
  ): string {
69
67
  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")}`;
68
+ return `render|${base}|${JSON.stringify(env)}|${JSON.stringify(subject ?? null)}`;
73
69
  }
74
70
 
75
71
  function sortObjectKeys(obj: unknown): unknown {
@@ -83,7 +79,7 @@ function sortObjectKeys(obj: unknown): unknown {
83
79
  }
84
80
 
85
81
  /**
86
- * In-memory LRU cache for ResolvedPrompt objects.
82
+ * In-memory LRU cache for RenderedPrompt objects.
87
83
  *
88
84
  * Uses a Map (insertion-ordered) as the backing store. On every access, the
89
85
  * entry is deleted and re-inserted to maintain LRU order — oldest entries sit
@@ -100,7 +96,7 @@ export class PromptCache {
100
96
  this.store = new Map();
101
97
  }
102
98
 
103
- get(key: string): CacheableResolved | undefined {
99
+ get(key: string): CacheableRendered | undefined {
104
100
  const entry = this.store.get(key);
105
101
  if (!entry) return undefined;
106
102
 
@@ -116,7 +112,7 @@ export class PromptCache {
116
112
  return entry.value;
117
113
  }
118
114
 
119
- set(key: string, value: CacheableResolved): void {
115
+ set(key: string, value: CacheableRendered): void {
120
116
  // maxEntries: 0 (or negative) means caching is disabled — drop the entry.
121
117
  if (this.maxEntries <= 0) {
122
118
  return;
@@ -3,7 +3,13 @@ import { EnvNotFoundError, To11aiNetworkError } from "../errors";
3
3
  import { zResolvedPrompt } from "../generated/zod.gen";
4
4
  import type { ApiEnvelope, Transport } from "../transport";
5
5
  import type { CacheConfig } from "./cache";
6
- import { buildCacheKey, buildFetchCacheKey, PromptCache } from "./cache";
6
+ import { buildRenderCacheKey, 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,
@@ -15,8 +21,6 @@ import type {
15
21
  CreatePromptVersionResult,
16
22
  DeleteProjectPromptLabelInput,
17
23
  DeleteProjectPromptLabelResult,
18
- FetchedPrompt,
19
- FetchPromptOptions,
20
24
  GetProjectPromptLabelInput,
21
25
  GetProjectPromptLabelResult,
22
26
  GetPromptInput,
@@ -40,10 +44,11 @@ import type {
40
44
  PaginatedResult,
41
45
  RenderedChatMessage,
42
46
  RenderedPrompt,
47
+ RenderFormat,
48
+ RenderOptions,
43
49
  ResolvedPrompt,
44
50
  ResolvedPromptEnvelope,
45
- ResolvePromptInput,
46
- ResolvePromptResult,
51
+ UnknownRoleMode,
47
52
  UpdateProjectPromptLabelInput,
48
53
  UpdateProjectPromptLabelResult,
49
54
  UpdatePromptInput,
@@ -66,8 +71,6 @@ export type {
66
71
  CreatePromptVersionResult,
67
72
  DeleteProjectPromptLabelInput,
68
73
  DeleteProjectPromptLabelResult,
69
- FetchedPrompt,
70
- FetchPromptOptions,
71
74
  GetProjectPromptLabelInput,
72
75
  GetProjectPromptLabelResult,
73
76
  GetPromptInput,
@@ -101,32 +104,22 @@ export type {
101
104
  PromptVersion,
102
105
  RenderedChatMessage,
103
106
  RenderedPrompt,
107
+ RenderFormat,
108
+ RenderOptions,
104
109
  ResolvedPrompt,
105
110
  ResolvedPromptEnvelope,
106
111
  ResolvedToolCall,
107
112
  ResolvedToolDefinition,
108
- ResolvePromptInput,
109
- ResolvePromptResult,
110
113
  RowError,
111
114
  TextChatMessage,
112
115
  TextChatRole,
116
+ UnknownRoleMode,
113
117
  UpdateProjectPromptLabelInput,
114
118
  UpdateProjectPromptLabelResult,
115
119
  UpdatePromptInput,
116
120
  UpdatePromptResult,
117
121
  } from "./types";
118
122
 
119
- const zResolvedPromptLegacy = zResolvedPrompt.pick({
120
- promptId: true,
121
- versionId: true,
122
- version: true,
123
- labels: true,
124
- contentHash: true,
125
- rendered: true,
126
- tools: true,
127
- toolChoice: true,
128
- });
129
-
130
123
  const zResolvedPromptEnvelope = zResolvedPrompt
131
124
  .partial({ releaseId: true })
132
125
  .extend({ strategy: z.enum(["single", "weighted"]).optional() });
@@ -143,22 +136,28 @@ function getProjectPromptsPath(projectId: string): string {
143
136
  }
144
137
 
145
138
  /**
146
- * Project a server `rendered` payload into the `messages` array of the R5
147
- * `FetchedPrompt` (D5):
148
- * - `format: "chat"` → the message array directly.
149
- * - `format: "text"` → a single synthesized `{ role: "user", content }`.
139
+ * Project a server `rendered` payload into the enriched result's `messages`.
140
+ *
141
+ * Prompts resolve as chat blocks. The deprecated `text` format is no longer a
142
+ * first-class case: a non-chat payload throws rather than being silently
143
+ * coerced (a text prompt is sugar for a single-block chat and is being removed
144
+ * from the API).
150
145
  */
151
- function renderedToMessages(rendered: RenderedPrompt): RenderedChatMessage[] {
152
- if (rendered.format === "chat") {
153
- return rendered.messages;
146
+ function renderedToMessages(
147
+ rendered: ResolvedPrompt["rendered"],
148
+ ): RenderedChatMessage[] {
149
+ if (rendered.format !== "chat") {
150
+ throw new Error(
151
+ "[to11ai/sdk] Unsupported non-chat prompt format; author the prompt as chat blocks.",
152
+ );
154
153
  }
155
- return [{ role: "user", content: rendered.text }];
154
+ return rendered.messages;
156
155
  }
157
156
 
158
157
  /**
159
- * Normalize the server resolve envelope into the enriched R5 `FetchedPrompt`.
158
+ * Normalize the server resolve envelope into the enriched `RenderedPrompt`.
160
159
  *
161
- * - `messages` is synthesized from `rendered` via the D5 projection.
160
+ * - `messages` is derived from the `rendered` chat payload.
162
161
  * - `releaseId` is ALWAYS a key: a string when the server sends one, else
163
162
  * explicit `null` (R5 `string | null`).
164
163
  * - `variantName` is present IFF the server sent one (i.e. weighted release);
@@ -167,9 +166,10 @@ function renderedToMessages(rendered: RenderedPrompt): RenderedChatMessage[] {
167
166
  */
168
167
  function normalizeResolvedEnvelope(
169
168
  raw: ResolvedPrompt | ResolvedPromptEnvelope,
170
- ): FetchedPrompt {
169
+ slug: string,
170
+ ): RenderedPrompt {
171
171
  const provenance = raw as ResolvedPromptEnvelope;
172
- const result: FetchedPrompt = {
172
+ const result: RenderedPrompt = {
173
173
  messages: renderedToMessages(raw.rendered),
174
174
  version: raw.version,
175
175
  releaseId: provenance.releaseId ?? null,
@@ -177,8 +177,12 @@ function normalizeResolvedEnvelope(
177
177
  promptId: raw.promptId,
178
178
  versionId: raw.versionId,
179
179
  contentHash: raw.contentHash,
180
- labels: raw.labels,
181
- rendered: raw.rendered,
180
+ label: raw.label,
181
+ // The requested slug — the API resolve envelope does not echo it, so it
182
+ // is threaded from the `render()` input (TO11-2727). Surfaced as
183
+ // `x-to11-prompt-slug` for trace readability.
184
+ slug,
185
+ modelConfig: provenance.modelConfig,
182
186
  };
183
187
  // `tools` is present IFF the resolved version declares tool definitions; the
184
188
  // wire may send `null` (or omit it) — normalize both to absent so callers
@@ -208,20 +212,26 @@ function normalizeResolvedEnvelope(
208
212
  }
209
213
 
210
214
  /**
211
- * Lift a caller-supplied offline `fallback` into the enriched `FetchedPrompt`
212
- * shape (D7), so `fetch()` returns an identical shape on the online and
213
- * offline paths. An already-enriched `FetchedPrompt` is returned as-is; a
214
- * legacy `ResolvedPrompt` is lifted (messages synthesized; `releaseId` → its
215
- * own value or explicit `null`; `blockRenderRecord` → `{}`; `variantName`
216
- * stays absent).
215
+ * Lift a caller-supplied offline `fallback` into the returned shape (D7), so
216
+ * `render()` returns an identical shape on the online and offline paths. The
217
+ * fallback's own slug is preferred, falling back to the `render()` input when it
218
+ * carries none, so `x-to11-prompt-slug` is still emitted.
217
219
  */
218
- function liftFallbackToFetched(
219
- fallback: ResolvedPrompt | FetchedPrompt,
220
- ): FetchedPrompt {
221
- if ("messages" in fallback) {
222
- return structuredClone(fallback);
220
+ function liftFallbackToRendered(
221
+ fallback: RenderedPrompt,
222
+ slug: string,
223
+ ): RenderedPrompt {
224
+ // Defensive: `fallback` is typed `RenderedPrompt`, but an untyped JS caller,
225
+ // an `as` cast, or a persisted pre-2.0 `resolve()` result could pass the old
226
+ // `ResolvedPrompt` shape (no top-level `messages`). Fail loudly rather than
227
+ // return a malformed result with `undefined` messages.
228
+ if (!Array.isArray((fallback as { messages?: unknown }).messages)) {
229
+ throw new Error(
230
+ "[to11ai/sdk] `fallback` must be a RenderedPrompt (as returned by render()); the legacy resolve() shape is no longer supported.",
231
+ );
223
232
  }
224
- return normalizeResolvedEnvelope(fallback);
233
+ // `||` (not `??`) so an empty-string slug also falls through to the input.
234
+ return { ...structuredClone(fallback), slug: fallback.slug || slug };
225
235
  }
226
236
 
227
237
  function getProjectPromptPath(projectId: string, promptId: string): string {
@@ -261,34 +271,48 @@ function buildLabelEventsQuery(input: ListLabelEventsInput): string {
261
271
  }
262
272
 
263
273
  /**
264
- * Init-time config for the env-aware `fetch()` surface (TO11-980 / S9).
274
+ * Init-time config for the env-aware `render()` surface (TO11-980 / S9).
265
275
  *
266
276
  * `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.
277
+ * `render()` (it builds the project-scoped resolve path); `env` is the default
278
+ * serving environment inherited by every `render(slug)` call.
269
279
  */
270
280
  export interface PromptResourceConfig {
271
- /** Project the client is bound to. Required for `fetch()`; validated at createClient. */
281
+ /** Project the client is bound to. Required for `render()`; validated at createClient. */
272
282
  projectId?: string;
273
- /** Default serving env for `fetch()`. Validated against ENV_SLUG_REGEX at createClient. */
283
+ /** Default serving env for `render()`. Validated against ENV_SLUG_REGEX at createClient. */
274
284
  env?: string;
285
+ /** Default provider shape for `render()` output. Per-call override allowed. */
286
+ format?: RenderFormat;
287
+ /**
288
+ * Default unknown-role policy for `render({ format })` output conversion.
289
+ * Per-call override allowed; defaults to `"drop"`.
290
+ */
291
+ onUnknownRole?: UnknownRoleMode;
275
292
  }
276
293
 
277
- export class PromptResource {
294
+ export class PromptResource<F extends RenderFormat | undefined = undefined> {
278
295
  private readonly transport: Transport;
279
296
  private readonly cache: PromptCache;
280
297
  private readonly projectId: string | undefined;
281
298
  private readonly env: string | undefined;
299
+ private readonly format: RenderFormat | undefined;
300
+ private readonly onUnknownRole: UnknownRoleMode | undefined;
282
301
 
283
302
  constructor(
284
303
  transport: Transport,
285
304
  cacheConfig?: CacheConfig,
286
- config?: PromptResourceConfig,
305
+ // `format` is bound to the class generic `F` (not the wider `RenderFormat`)
306
+ // so the constructed default shape cannot disagree with what `render()`'s
307
+ // return type (`RenderResult<F>`) promises.
308
+ config?: Omit<PromptResourceConfig, "format"> & { format?: F },
287
309
  ) {
288
310
  this.transport = transport;
289
311
  this.cache = new PromptCache(cacheConfig);
290
312
  this.projectId = config?.projectId;
291
313
  this.env = config?.env;
314
+ this.format = config?.format;
315
+ this.onUnknownRole = config?.onUnknownRole;
292
316
  }
293
317
 
294
318
  // ─── Create ──────────────────────────────────────────────────────────────
@@ -436,35 +460,64 @@ export class PromptResource {
436
460
  return unwrap(envelope);
437
461
  }
438
462
 
439
- // ─── Fetch (env-aware resolution, TO11-980 / S9) ──────────────────────────
463
+ // ─── Render (env-aware resolution + format shaping, TO11-980 / TO11-2691) ──
464
+
465
+ /**
466
+ * Resolve and (optionally) shape a prompt for the client's configured
467
+ * `projectId` + `env`, in one control-plane request. With a `format` (per
468
+ * call or inherited from the client) the result is provider-shaped
469
+ * `{ messages, config, metadata }`; without one it is the neutral
470
+ * `RenderedPrompt`. Resolves by the client's configured `env`.
471
+ *
472
+ * @example Render an OpenAI-shaped prompt and send it through the gateway
473
+ * const prompt = await to11.prompts.render("weather-concierge", {
474
+ * variables: { city: "NYC" },
475
+ * });
476
+ * const res = await openai.chat.completions.create(
477
+ * { ...prompt.config, messages: prompt.messages },
478
+ * { headers: to11.turn().headers(prompt) },
479
+ * );
480
+ */
481
+ async render(
482
+ slug: string,
483
+ options: RenderOptions & { format: "openai" },
484
+ ): Promise<OpenAiRenderedPrompt>;
485
+ async render(
486
+ slug: string,
487
+ options: RenderOptions & { format: "anthropic" },
488
+ ): Promise<AnthropicRenderedPrompt>;
489
+ async render(slug: string, options?: RenderOptions): Promise<RenderResult<F>>;
490
+ async render(
491
+ slug: string,
492
+ options: RenderOptions = {},
493
+ ): Promise<RenderedPrompt | OpenAiRenderedPrompt | AnthropicRenderedPrompt> {
494
+ const rendered = await this.resolveToRendered(slug, options);
495
+ return shapePrompt(rendered, options.format ?? this.format, {
496
+ onUnknownRole: options.onUnknownRole ?? this.onUnknownRole,
497
+ });
498
+ }
440
499
 
441
500
  /**
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.
501
+ * Env-aware resolution → neutral `RenderedPrompt`. Carries the resolve /
502
+ * cache / offline-fallback logic; `render()` is the public shaping surface.
445
503
  *
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? }`.
504
+ * - **env→label wire mapping:** the resolve route is label-based
505
+ * (`/v1/projects/:projectId/prompts:resolve`, body `labels[]`); the
506
+ * resolved env slug is sent as the single resolution label.
452
507
  * - **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.
508
+ * none per call) this throws `EnvNotFoundError` CLIENT-side, eagerly,
509
+ * before any transport call.
455
510
  * - **Cache (D8):** caches `single`-strategy releases and weighted releases
456
511
  * WITH a `subject`; BYPASSES the cache for weighted releases with NO
457
512
  * `subject` (a uniform-random assignment must not be pinned for the TTL).
458
- *
459
- * Prefer this over the deprecated `resolve()`.
460
513
  */
461
- async fetch(
514
+ private async resolveToRendered(
462
515
  slug: string,
463
- options: FetchPromptOptions = {},
464
- ): Promise<FetchedPrompt> {
516
+ options: RenderOptions = {},
517
+ ): Promise<RenderedPrompt> {
465
518
  if (this.projectId === undefined || this.projectId.trim() === "") {
466
519
  throw new Error(
467
- "[to11ai/sdk] `prompts.fetch()` requires a `projectId` on the client — pass it to createClient({ projectId, ... }).",
520
+ "[to11ai/sdk] `prompts.render()` requires a `projectId` on the client — pass it to createClient({ projectId, ... }).",
468
521
  );
469
522
  }
470
523
 
@@ -486,35 +539,25 @@ export class PromptResource {
486
539
  );
487
540
  }
488
541
 
489
- const {
490
- subject,
491
- variables,
492
- cache: useCache = true,
493
- fallback,
494
- developerRole,
495
- } = options;
542
+ const { subject, variables, cache: useCache = true, fallback } = options;
496
543
 
497
- const cacheKey = buildFetchCacheKey(
544
+ const cacheKey = buildRenderCacheKey(
498
545
  this.projectId,
499
546
  slug,
500
547
  env,
501
548
  subject,
502
549
  variables,
503
- developerRole,
504
550
  );
505
551
 
506
552
  if (useCache) {
507
553
  const cached = this.cache.get(cacheKey);
508
- // Only enriched (fetch) entries are valid hits here; the fetch key
509
- // namespace ("fetch|…") never collides with the legacy resolve key.
510
- if (cached && "messages" in cached) return structuredClone(cached);
554
+ if (cached) return structuredClone(cached);
511
555
  }
512
556
 
513
557
  // env→label mapping: the env slug is the single resolution label.
514
558
  const body: Record<string, unknown> = { slug, labels: [env] };
515
559
  if (subject !== undefined) body.subject = subject;
516
560
  if (variables !== undefined) body.variables = variables;
517
- if (developerRole !== undefined) body.developerRole = developerRole;
518
561
 
519
562
  try {
520
563
  const envelope = await this.transport.request<ResolvedPromptEnvelope>(
@@ -527,7 +570,7 @@ export class PromptResource {
527
570
  },
528
571
  );
529
572
  const raw = unwrap(envelope);
530
- const result = normalizeResolvedEnvelope(raw);
573
+ const result = normalizeResolvedEnvelope(raw, slug);
531
574
 
532
575
  // D8: bypass the cache for a weighted release with NO subject —
533
576
  // caching a uniform-random assignment would pin one variant for the
@@ -552,63 +595,7 @@ export class PromptResource {
552
595
  // client-eager throw already happened above the try, so there is no
553
596
  // network error to trigger it on the env-missing path.
554
597
  if (err instanceof To11aiNetworkError && fallback !== undefined) {
555
- return liftFallbackToFetched(fallback);
556
- }
557
- throw err;
558
- }
559
- }
560
-
561
- // ─── Resolve ──────────────────────────────────────────────────────────────
562
-
563
- /**
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()`.
570
- */
571
- async resolve(input: ResolvePromptInput): Promise<ResolvePromptResult> {
572
- const {
573
- projectId,
574
- slug,
575
- labels,
576
- variables,
577
- cache: useCache = true,
578
- fallback,
579
- } = input;
580
-
581
- const cacheKey = buildCacheKey(projectId, slug, labels, variables);
582
-
583
- if (useCache) {
584
- const cached = this.cache.get(cacheKey);
585
- // The legacy resolve key namespace only ever stores ResolvedPrompt
586
- // entries (the fetch namespace is "fetch|…"); the `messages` guard
587
- // keeps the static return type as ResolvedPrompt.
588
- if (cached && !("messages" in cached)) return structuredClone(cached);
589
- }
590
-
591
- try {
592
- const envelope = await this.transport.request<ResolvePromptResult>(
593
- "POST",
594
- `${getProjectPromptsPath(projectId)}:resolve`,
595
- {
596
- body: { slug, labels, variables },
597
- decode: (data) =>
598
- zResolvedPromptLegacy.parse(data) as ResolvePromptResult,
599
- },
600
- );
601
- const result = unwrap(envelope);
602
-
603
- if (useCache) {
604
- this.cache.set(cacheKey, structuredClone(result));
605
- }
606
-
607
- return result;
608
- } catch (err) {
609
- // If network error and we have a fallback, serve it
610
- if (err instanceof To11aiNetworkError && fallback !== undefined) {
611
- return fallback;
598
+ return liftFallbackToRendered(fallback, slug);
612
599
  }
613
600
  throw err;
614
601
  }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Format-at-load shaping: turn a neutral `RenderedPrompt` 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 { RenderedPrompt, 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
+ /** The prompt's human-readable slug — surfaced as `x-to11-prompt-slug`. */
46
+ slug: string;
47
+ variantName?: string;
48
+ }
49
+
50
+ /** OpenAI-shaped render result: spread `{ ...config, messages }` into create(). */
51
+ export interface OpenAiRenderedPrompt {
52
+ messages: OpenAIChatMessage[];
53
+ config: PromptConfig;
54
+ metadata: PromptMetadata;
55
+ /** Authored tools, provider-shaped. Present only when the prompt declares tools. */
56
+ tools?: OpenAITool[];
57
+ /** Authored tool-choice directive. Present only when the prompt sets one. */
58
+ tool_choice?: OpenAIToolChoice;
59
+ }
60
+
61
+ /** Anthropic-shaped render result: system/developer folded into `system`. */
62
+ export interface AnthropicRenderedPrompt {
63
+ /** Folded system/developer instruction text (Anthropic's top-level `system`). */
64
+ system?: string;
65
+ messages: AnthropicMessage[];
66
+ config: PromptConfig;
67
+ metadata: PromptMetadata;
68
+ /** Authored tools, provider-shaped. Present only when the prompt declares tools. */
69
+ tools?: AnthropicTool[];
70
+ /** Authored tool-choice directive. Present only when the prompt sets one. */
71
+ tool_choice?: AnthropicToolChoice;
72
+ }
73
+
74
+ /** The render return type for a given client/call format. */
75
+ export type RenderResult<F extends RenderFormat | undefined> =
76
+ F extends "openai"
77
+ ? OpenAiRenderedPrompt
78
+ : F extends "anthropic"
79
+ ? AnthropicRenderedPrompt
80
+ : RenderedPrompt;
81
+
82
+ function toPromptConfig(modelConfig: unknown): PromptConfig {
83
+ return modelConfig !== null && typeof modelConfig === "object"
84
+ ? { ...(modelConfig as Record<string, unknown>) }
85
+ : {};
86
+ }
87
+
88
+ function toMetadata(prompt: RenderedPrompt): PromptMetadata {
89
+ const metadata: PromptMetadata = {
90
+ promptId: prompt.promptId,
91
+ versionId: prompt.versionId,
92
+ version: prompt.version,
93
+ releaseId: prompt.releaseId,
94
+ label: prompt.label,
95
+ slug: prompt.slug,
96
+ };
97
+ if (prompt.variantName !== undefined)
98
+ metadata.variantName = prompt.variantName;
99
+ return metadata;
100
+ }
101
+
102
+ export function shapeOpenAI(
103
+ prompt: RenderedPrompt,
104
+ options: ConvertMessagesOptions = {},
105
+ ): OpenAiRenderedPrompt {
106
+ const result: OpenAiRenderedPrompt = {
107
+ messages: toOpenAIMessages(prompt.messages, options),
108
+ config: toPromptConfig(prompt.modelConfig),
109
+ metadata: toMetadata(prompt),
110
+ };
111
+ // Carry authored tools so the spread-ready result is complete for
112
+ // tool-using prompts. Omit when empty/absent so we never send `tools: []`.
113
+ const tools = toOpenAITools(prompt.tools);
114
+ if (tools.length > 0) result.tools = tools;
115
+ const toolChoice = toOpenAIToolChoice(prompt.toolChoice);
116
+ if (toolChoice !== undefined) result.tool_choice = toolChoice;
117
+ return result;
118
+ }
119
+
120
+ export function shapeAnthropic(
121
+ prompt: RenderedPrompt,
122
+ options: ConvertMessagesOptions = {},
123
+ ): AnthropicRenderedPrompt {
124
+ const result: AnthropicRenderedPrompt = {
125
+ messages: toAnthropicMessages(prompt.messages, options),
126
+ config: toPromptConfig(prompt.modelConfig),
127
+ metadata: toMetadata(prompt),
128
+ };
129
+ const system = toAnthropicSystem(prompt.messages);
130
+ if (system !== undefined) result.system = system;
131
+ const tools = toAnthropicTools(prompt.tools);
132
+ if (tools.length > 0) result.tools = tools;
133
+ const toolChoice = toAnthropicToolChoice(prompt.toolChoice);
134
+ if (toolChoice !== undefined) result.tool_choice = toolChoice;
135
+ return result;
136
+ }
137
+
138
+ /** Shape a fetched prompt for `format`, or return it neutral when unset. */
139
+ export function shapePrompt(
140
+ prompt: RenderedPrompt,
141
+ format: RenderFormat | undefined,
142
+ options: ConvertMessagesOptions = {},
143
+ ): RenderedPrompt | OpenAiRenderedPrompt | AnthropicRenderedPrompt {
144
+ switch (format) {
145
+ case "openai":
146
+ return shapeOpenAI(prompt, options);
147
+ case "anthropic":
148
+ return shapeAnthropic(prompt, options);
149
+ default:
150
+ return prompt;
151
+ }
152
+ }