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

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.
@@ -236,7 +236,7 @@ export const zPrompt = z.object({
236
236
  id: z.string(),
237
237
  name: z.string().min(1).max(200),
238
238
  projectId: z.string(),
239
- slug: z.string().min(1).max(200).regex(/^[a-z0-9-]+$/),
239
+ slug: z.string().min(1).max(128).regex(/^[a-z0-9-]+$/),
240
240
  status: z.enum(['active', 'archived']),
241
241
  tags: z.array(z.string()),
242
242
  updatedAt: z.iso.datetime()
@@ -7359,7 +7359,7 @@ export const zPromptsListPromptsResponse = z.object({
7359
7359
  latestVersion: z.number().nullable(),
7360
7360
  name: z.string().min(1).max(200),
7361
7361
  projectId: z.string(),
7362
- slug: z.string().min(1).max(200).regex(/^[a-z0-9-]+$/),
7362
+ slug: z.string().min(1).max(128).regex(/^[a-z0-9-]+$/),
7363
7363
  status: z.enum(['active', 'archived']),
7364
7364
  tags: z.array(z.string()),
7365
7365
  updatedAt: z.iso.datetime()
@@ -7390,7 +7390,7 @@ export const zPromptsListPromptsResponse = z.object({
7390
7390
  export const zPromptsCreatePromptBody = z.object({
7391
7391
  description: z.string().nullish(),
7392
7392
  name: z.string().min(1).max(200),
7393
- slug: z.string().min(1).max(200).regex(/^[a-z0-9-]+$/),
7393
+ slug: z.string().min(1).max(128).regex(/^[a-z0-9-]+$/),
7394
7394
  tags: z.array(z.string()).nullish()
7395
7395
  });
7396
7396
 
@@ -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
- * `render()` 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
@@ -58,7 +57,7 @@ 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,
@@ -66,7 +65,7 @@ export function buildFetchCacheKey(
66
65
  variables?: Record<string, unknown>,
67
66
  ): string {
68
67
  const base = buildCacheKey(projectId, slug, [env], variables);
69
- return `fetch|${base}|${JSON.stringify(env)}|${JSON.stringify(subject ?? null)}`;
68
+ return `render|${base}|${JSON.stringify(env)}|${JSON.stringify(subject ?? null)}`;
70
69
  }
71
70
 
72
71
  function sortObjectKeys(obj: unknown): unknown {
@@ -80,7 +79,7 @@ function sortObjectKeys(obj: unknown): unknown {
80
79
  }
81
80
 
82
81
  /**
83
- * In-memory LRU cache for ResolvedPrompt objects.
82
+ * In-memory LRU cache for RenderedPrompt objects.
84
83
  *
85
84
  * Uses a Map (insertion-ordered) as the backing store. On every access, the
86
85
  * entry is deleted and re-inserted to maintain LRU order — oldest entries sit
@@ -97,7 +96,7 @@ export class PromptCache {
97
96
  this.store = new Map();
98
97
  }
99
98
 
100
- get(key: string): CacheableResolved | undefined {
99
+ get(key: string): CacheableRendered | undefined {
101
100
  const entry = this.store.get(key);
102
101
  if (!entry) return undefined;
103
102
 
@@ -113,7 +112,7 @@ export class PromptCache {
113
112
  return entry.value;
114
113
  }
115
114
 
116
- set(key: string, value: CacheableResolved): void {
115
+ set(key: string, value: CacheableRendered): void {
117
116
  // maxEntries: 0 (or negative) means caching is disabled — drop the entry.
118
117
  if (this.maxEntries <= 0) {
119
118
  return;
@@ -3,7 +3,7 @@ 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
7
  import type {
8
8
  AnthropicRenderedPrompt,
9
9
  OpenAiRenderedPrompt,
@@ -21,8 +21,6 @@ import type {
21
21
  CreatePromptVersionResult,
22
22
  DeleteProjectPromptLabelInput,
23
23
  DeleteProjectPromptLabelResult,
24
- FetchedPrompt,
25
- FetchPromptOptions,
26
24
  GetProjectPromptLabelInput,
27
25
  GetProjectPromptLabelResult,
28
26
  GetPromptInput,
@@ -50,8 +48,6 @@ import type {
50
48
  RenderOptions,
51
49
  ResolvedPrompt,
52
50
  ResolvedPromptEnvelope,
53
- ResolvePromptInput,
54
- ResolvePromptResult,
55
51
  UnknownRoleMode,
56
52
  UpdateProjectPromptLabelInput,
57
53
  UpdateProjectPromptLabelResult,
@@ -75,8 +71,6 @@ export type {
75
71
  CreatePromptVersionResult,
76
72
  DeleteProjectPromptLabelInput,
77
73
  DeleteProjectPromptLabelResult,
78
- FetchedPrompt,
79
- FetchPromptOptions,
80
74
  GetProjectPromptLabelInput,
81
75
  GetProjectPromptLabelResult,
82
76
  GetPromptInput,
@@ -110,33 +104,22 @@ export type {
110
104
  PromptVersion,
111
105
  RenderedChatMessage,
112
106
  RenderedPrompt,
107
+ RenderFormat,
108
+ RenderOptions,
113
109
  ResolvedPrompt,
114
110
  ResolvedPromptEnvelope,
115
111
  ResolvedToolCall,
116
112
  ResolvedToolDefinition,
117
- ResolvePromptInput,
118
- ResolvePromptResult,
119
113
  RowError,
120
114
  TextChatMessage,
121
115
  TextChatRole,
116
+ UnknownRoleMode,
122
117
  UpdateProjectPromptLabelInput,
123
118
  UpdateProjectPromptLabelResult,
124
119
  UpdatePromptInput,
125
120
  UpdatePromptResult,
126
121
  } from "./types";
127
122
 
128
- const zResolvedPromptLegacy = zResolvedPrompt.pick({
129
- promptId: true,
130
- versionId: true,
131
- version: true,
132
- label: true,
133
- contentHash: true,
134
- rendered: true,
135
- tools: true,
136
- toolChoice: true,
137
- modelConfig: true,
138
- });
139
-
140
123
  const zResolvedPromptEnvelope = zResolvedPrompt
141
124
  .partial({ releaseId: true })
142
125
  .extend({ strategy: z.enum(["single", "weighted"]).optional() });
@@ -153,22 +136,28 @@ function getProjectPromptsPath(projectId: string): string {
153
136
  }
154
137
 
155
138
  /**
156
- * Project a server `rendered` payload into the `messages` array of the R5
157
- * `FetchedPrompt` (D5):
158
- * - `format: "chat"` → the message array directly.
159
- * - `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).
160
145
  */
161
- function renderedToMessages(rendered: RenderedPrompt): RenderedChatMessage[] {
162
- if (rendered.format === "chat") {
163
- 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
+ );
164
153
  }
165
- return [{ role: "user", content: rendered.text }];
154
+ return rendered.messages;
166
155
  }
167
156
 
168
157
  /**
169
- * Normalize the server resolve envelope into the enriched R5 `FetchedPrompt`.
158
+ * Normalize the server resolve envelope into the enriched `RenderedPrompt`.
170
159
  *
171
- * - `messages` is synthesized from `rendered` via the D5 projection.
160
+ * - `messages` is derived from the `rendered` chat payload.
172
161
  * - `releaseId` is ALWAYS a key: a string when the server sends one, else
173
162
  * explicit `null` (R5 `string | null`).
174
163
  * - `variantName` is present IFF the server sent one (i.e. weighted release);
@@ -177,9 +166,10 @@ function renderedToMessages(rendered: RenderedPrompt): RenderedChatMessage[] {
177
166
  */
178
167
  function normalizeResolvedEnvelope(
179
168
  raw: ResolvedPrompt | ResolvedPromptEnvelope,
180
- ): FetchedPrompt {
169
+ slug: string,
170
+ ): RenderedPrompt {
181
171
  const provenance = raw as ResolvedPromptEnvelope;
182
- const result: FetchedPrompt = {
172
+ const result: RenderedPrompt = {
183
173
  messages: renderedToMessages(raw.rendered),
184
174
  version: raw.version,
185
175
  releaseId: provenance.releaseId ?? null,
@@ -188,7 +178,10 @@ function normalizeResolvedEnvelope(
188
178
  versionId: raw.versionId,
189
179
  contentHash: raw.contentHash,
190
180
  label: raw.label,
191
- rendered: raw.rendered,
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,
192
185
  modelConfig: provenance.modelConfig,
193
186
  };
194
187
  // `tools` is present IFF the resolved version declares tool definitions; the
@@ -219,20 +212,26 @@ function normalizeResolvedEnvelope(
219
212
  }
220
213
 
221
214
  /**
222
- * Lift a caller-supplied offline `fallback` into the enriched `FetchedPrompt`
223
- * shape (D7), so `render()` returns an identical shape on the online and
224
- * offline paths. An already-enriched `FetchedPrompt` is returned as-is; a
225
- * legacy `ResolvedPrompt` is lifted (messages synthesized; `releaseId` → its
226
- * own value or explicit `null`; `blockRenderRecord` → `{}`; `variantName`
227
- * 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.
228
219
  */
229
- function liftFallbackToFetched(
230
- fallback: ResolvedPrompt | FetchedPrompt,
231
- ): FetchedPrompt {
232
- if ("messages" in fallback) {
233
- 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
+ );
234
232
  }
235
- return normalizeResolvedEnvelope(fallback);
233
+ // `||` (not `??`) so an empty-string slug also falls through to the input.
234
+ return { ...structuredClone(fallback), slug: fallback.slug || slug };
236
235
  }
237
236
 
238
237
  function getProjectPromptPath(projectId: string, promptId: string): string {
@@ -316,10 +315,29 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
316
315
  this.onUnknownRole = config?.onUnknownRole;
317
316
  }
318
317
 
318
+ /**
319
+ * Resolve the project for a control-plane call: the per-call `projectId`
320
+ * wins, else the client-bound one from `createClient({ projectId })`. Throws
321
+ * if neither is set. Mirrors how `render()` resolves its project, so every
322
+ * method honors the bound `projectId` and callers need not repeat it.
323
+ */
324
+ private resolveProjectId(perCall?: string): string {
325
+ const projectId = perCall ?? this.projectId;
326
+ if (projectId === undefined || projectId.trim() === "") {
327
+ throw new Error(
328
+ "[to11ai/sdk] this call needs a `projectId` — pass it on the call or bind one via createClient({ projectId, ... }).",
329
+ );
330
+ }
331
+ // Trim so a whitespace-padded per-call id normalizes like the client-bound
332
+ // one (resolved via `pick()`), instead of reaching the wire untrimmed.
333
+ return projectId.trim();
334
+ }
335
+
319
336
  // ─── Create ──────────────────────────────────────────────────────────────
320
337
 
321
338
  async create(input: CreatePromptInput): Promise<CreatePromptResult> {
322
- const { projectId, ...body } = input;
339
+ const { projectId: perCallProjectId, ...body } = input;
340
+ const projectId = this.resolveProjectId(perCallProjectId);
323
341
  const envelope = await this.transport.request<CreatePromptResult>(
324
342
  "POST",
325
343
  getProjectPromptsPath(projectId),
@@ -331,9 +349,10 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
331
349
  // ─── List ─────────────────────────────────────────────────────────────────
332
350
 
333
351
  async list(
334
- input: ListPromptsInput,
352
+ input: ListPromptsInput = {},
335
353
  ): Promise<PaginatedResult<ListPromptsResult>> {
336
- const { projectId, ...queryRest } = input;
354
+ const { projectId: perCallProjectId, ...queryRest } = input;
355
+ const projectId = this.resolveProjectId(perCallProjectId);
337
356
  const envelope = await this.transport.request<
338
357
  | ListPromptsResult[]
339
358
  | {
@@ -365,7 +384,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
365
384
  // ─── Get ──────────────────────────────────────────────────────────────────
366
385
 
367
386
  async get(input: GetPromptInput): Promise<GetPromptResult> {
368
- const { projectId, promptId } = input;
387
+ const { projectId: perCallProjectId, promptId } = input;
388
+ const projectId = this.resolveProjectId(perCallProjectId);
369
389
  const envelope = await this.transport.request<GetPromptResult>(
370
390
  "GET",
371
391
  getProjectPromptPath(projectId, promptId),
@@ -376,7 +396,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
376
396
  // ─── Update ───────────────────────────────────────────────────────────────
377
397
 
378
398
  async update(input: UpdatePromptInput): Promise<UpdatePromptResult> {
379
- const { projectId, promptId, ...body } = input;
399
+ const { projectId: perCallProjectId, promptId, ...body } = input;
400
+ const projectId = this.resolveProjectId(perCallProjectId);
380
401
  const envelope = await this.transport.request<UpdatePromptResult>(
381
402
  "PATCH",
382
403
  getProjectPromptPath(projectId, promptId),
@@ -388,7 +409,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
388
409
  // ─── Archive ──────────────────────────────────────────────────────────────
389
410
 
390
411
  async archive(input: ArchivePromptInput): Promise<ArchivePromptResult> {
391
- const { projectId, promptId } = input;
412
+ const { projectId: perCallProjectId, promptId } = input;
413
+ const projectId = this.resolveProjectId(perCallProjectId);
392
414
  const envelope = await this.transport.request<ArchivePromptResult>(
393
415
  "DELETE",
394
416
  getProjectPromptPath(projectId, promptId),
@@ -401,7 +423,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
401
423
  async createVersion(
402
424
  input: CreatePromptVersionInput,
403
425
  ): Promise<CreatePromptVersionResult> {
404
- const { projectId, promptId, ...body } = input;
426
+ const { projectId: perCallProjectId, promptId, ...body } = input;
427
+ const projectId = this.resolveProjectId(perCallProjectId);
405
428
  const envelope = await this.transport.request<CreatePromptVersionResult>(
406
429
  "POST",
407
430
  `${getProjectPromptPath(projectId, promptId)}/versions`,
@@ -415,7 +438,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
415
438
  async listVersions(
416
439
  input: ListPromptVersionsInput,
417
440
  ): Promise<ListPromptVersionsResult> {
418
- const { projectId, promptId } = input;
441
+ const { projectId: perCallProjectId, promptId } = input;
442
+ const projectId = this.resolveProjectId(perCallProjectId);
419
443
  const envelope = await this.transport.request<ListPromptVersionsResult>(
420
444
  "GET",
421
445
  `${getProjectPromptPath(projectId, promptId)}/versions`,
@@ -428,7 +452,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
428
452
  async getVersion(
429
453
  input: GetPromptVersionInput,
430
454
  ): Promise<GetPromptVersionResult> {
431
- const { projectId, promptId, versionNumber } = input;
455
+ const { projectId: perCallProjectId, promptId, versionNumber } = input;
456
+ const projectId = this.resolveProjectId(perCallProjectId);
432
457
  const envelope = await this.transport.request<GetPromptVersionResult>(
433
458
  "GET",
434
459
  `${getProjectPromptPath(projectId, promptId)}/versions/${encodeURIComponent(String(versionNumber))}`,
@@ -439,7 +464,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
439
464
  // ─── Move Label ───────────────────────────────────────────────────────────
440
465
 
441
466
  async moveLabel(input: MovePromptLabelInput): Promise<MovePromptLabelResult> {
442
- const { projectId, promptId, label, ...body } = input;
467
+ const { projectId: perCallProjectId, promptId, label, ...body } = input;
468
+ const projectId = this.resolveProjectId(perCallProjectId);
443
469
  const envelope = await this.transport.request<MovePromptLabelResult>(
444
470
  "PUT",
445
471
  `${getProjectPromptPath(projectId, promptId)}/labels/${encodeURIComponent(label)}`,
@@ -453,7 +479,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
453
479
  async listLabels(
454
480
  input: ListPromptLabelsInput,
455
481
  ): Promise<ListPromptLabelsResult> {
456
- const { projectId, promptId } = input;
482
+ const { projectId: perCallProjectId, promptId } = input;
483
+ const projectId = this.resolveProjectId(perCallProjectId);
457
484
  const envelope = await this.transport.request<ListPromptLabelsResult>(
458
485
  "GET",
459
486
  `${getProjectPromptPath(projectId, promptId)}/labels`,
@@ -468,8 +495,7 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
468
495
  * `projectId` + `env`, in one control-plane request. With a `format` (per
469
496
  * call or inherited from the client) the result is provider-shaped
470
497
  * `{ 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()`}.
498
+ * `RenderedPrompt`. Resolves by the client's configured `env`.
473
499
  *
474
500
  * @example Render an OpenAI-shaped prompt and send it through the gateway
475
501
  * const prompt = await to11.prompts.render("weather-concierge", {
@@ -492,15 +518,15 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
492
518
  async render(
493
519
  slug: string,
494
520
  options: RenderOptions = {},
495
- ): Promise<FetchedPrompt | OpenAiRenderedPrompt | AnthropicRenderedPrompt> {
496
- const fetched = await this.resolveToFetched(slug, options);
497
- return shapePrompt(fetched, options.format ?? this.format, {
521
+ ): Promise<RenderedPrompt | OpenAiRenderedPrompt | AnthropicRenderedPrompt> {
522
+ const rendered = await this.resolveToRendered(slug, options);
523
+ return shapePrompt(rendered, options.format ?? this.format, {
498
524
  onUnknownRole: options.onUnknownRole ?? this.onUnknownRole,
499
525
  });
500
526
  }
501
527
 
502
528
  /**
503
- * Env-aware resolution → neutral `FetchedPrompt`. Carries the resolve /
529
+ * Env-aware resolution → neutral `RenderedPrompt`. Carries the resolve /
504
530
  * cache / offline-fallback logic; `render()` is the public shaping surface.
505
531
  *
506
532
  * - **env→label wire mapping:** the resolve route is label-based
@@ -513,10 +539,10 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
513
539
  * WITH a `subject`; BYPASSES the cache for weighted releases with NO
514
540
  * `subject` (a uniform-random assignment must not be pinned for the TTL).
515
541
  */
516
- private async resolveToFetched(
542
+ private async resolveToRendered(
517
543
  slug: string,
518
- options: FetchPromptOptions = {},
519
- ): Promise<FetchedPrompt> {
544
+ options: RenderOptions = {},
545
+ ): Promise<RenderedPrompt> {
520
546
  if (this.projectId === undefined || this.projectId.trim() === "") {
521
547
  throw new Error(
522
548
  "[to11ai/sdk] `prompts.render()` requires a `projectId` on the client — pass it to createClient({ projectId, ... }).",
@@ -543,7 +569,7 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
543
569
 
544
570
  const { subject, variables, cache: useCache = true, fallback } = options;
545
571
 
546
- const cacheKey = buildFetchCacheKey(
572
+ const cacheKey = buildRenderCacheKey(
547
573
  this.projectId,
548
574
  slug,
549
575
  env,
@@ -553,9 +579,7 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
553
579
 
554
580
  if (useCache) {
555
581
  const cached = this.cache.get(cacheKey);
556
- // Only enriched (fetch) entries are valid hits here; the fetch key
557
- // namespace ("fetch|…") never collides with the legacy resolve key.
558
- if (cached && "messages" in cached) return structuredClone(cached);
582
+ if (cached) return structuredClone(cached);
559
583
  }
560
584
 
561
585
  // env→label mapping: the env slug is the single resolution label.
@@ -574,7 +598,7 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
574
598
  },
575
599
  );
576
600
  const raw = unwrap(envelope);
577
- const result = normalizeResolvedEnvelope(raw);
601
+ const result = normalizeResolvedEnvelope(raw, slug);
578
602
 
579
603
  // D8: bypass the cache for a weighted release with NO subject —
580
604
  // caching a uniform-random assignment would pin one variant for the
@@ -599,64 +623,7 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
599
623
  // client-eager throw already happened above the try, so there is no
600
624
  // network error to trigger it on the env-missing path.
601
625
  if (err instanceof To11aiNetworkError && fallback !== undefined) {
602
- return liftFallbackToFetched(fallback);
603
- }
604
- throw err;
605
- }
606
- }
607
-
608
- // ─── Resolve ──────────────────────────────────────────────────────────────
609
-
610
- /**
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.
618
- */
619
- async resolve(input: ResolvePromptInput): Promise<ResolvePromptResult> {
620
- const {
621
- projectId,
622
- slug,
623
- labels,
624
- variables,
625
- cache: useCache = true,
626
- fallback,
627
- } = input;
628
-
629
- const cacheKey = buildCacheKey(projectId, slug, labels, variables);
630
-
631
- if (useCache) {
632
- const cached = this.cache.get(cacheKey);
633
- // The legacy resolve key namespace only ever stores ResolvedPrompt
634
- // entries (the fetch namespace is "fetch|…"); the `messages` guard
635
- // keeps the static return type as ResolvedPrompt.
636
- if (cached && !("messages" in cached)) return structuredClone(cached);
637
- }
638
-
639
- try {
640
- const envelope = await this.transport.request<ResolvePromptResult>(
641
- "POST",
642
- `${getProjectPromptsPath(projectId)}:resolve`,
643
- {
644
- body: { slug, labels, variables },
645
- decode: (data) =>
646
- zResolvedPromptLegacy.parse(data) as ResolvePromptResult,
647
- },
648
- );
649
- const result = unwrap(envelope);
650
-
651
- if (useCache) {
652
- this.cache.set(cacheKey, structuredClone(result));
653
- }
654
-
655
- return result;
656
- } catch (err) {
657
- // If network error and we have a fallback, serve it
658
- if (err instanceof To11aiNetworkError && fallback !== undefined) {
659
- return fallback;
626
+ return liftFallbackToRendered(fallback, slug);
660
627
  }
661
628
  throw err;
662
629
  }
@@ -672,9 +639,10 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
672
639
  * affordances). Gated on `prompt:read` server-side.
673
640
  */
674
641
  async listProjectLabels(
675
- input: ListProjectPromptLabelsInput,
642
+ input: ListProjectPromptLabelsInput = {},
676
643
  ): Promise<ListProjectPromptLabelsResult> {
677
- const { projectId, includeArchived } = input;
644
+ const { projectId: perCallProjectId, includeArchived } = input;
645
+ const projectId = this.resolveProjectId(perCallProjectId);
678
646
  const envelope =
679
647
  await this.transport.request<ListProjectPromptLabelsResult>(
680
648
  "GET",
@@ -696,7 +664,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
696
664
  async createProjectLabel(
697
665
  input: CreateProjectPromptLabelInput,
698
666
  ): Promise<CreateProjectPromptLabelResult> {
699
- const { projectId, ...body } = input;
667
+ const { projectId: perCallProjectId, ...body } = input;
668
+ const projectId = this.resolveProjectId(perCallProjectId);
700
669
  const envelope =
701
670
  await this.transport.request<CreateProjectPromptLabelResult>(
702
671
  "POST",
@@ -709,9 +678,10 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
709
678
  async getProjectLabel(
710
679
  input: GetProjectPromptLabelInput,
711
680
  ): Promise<GetProjectPromptLabelResult> {
681
+ const projectId = this.resolveProjectId(input.projectId);
712
682
  const envelope = await this.transport.request<GetProjectPromptLabelResult>(
713
683
  "GET",
714
- getProjectPromptLabelPath(input.projectId, input.label),
684
+ getProjectPromptLabelPath(projectId, input.label),
715
685
  );
716
686
  return unwrap(envelope);
717
687
  }
@@ -727,7 +697,8 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
727
697
  async updateProjectLabel(
728
698
  input: UpdateProjectPromptLabelInput,
729
699
  ): Promise<UpdateProjectPromptLabelResult> {
730
- const { projectId, label, ...rest } = input;
700
+ const { projectId: perCallProjectId, label, ...rest } = input;
701
+ const projectId = this.resolveProjectId(perCallProjectId);
731
702
  const body: Record<string, unknown> = {};
732
703
  if (rest.isDefault !== undefined) body.isDefault = rest.isDefault;
733
704
  if (rest.description !== undefined) body.description = rest.description;
@@ -753,10 +724,11 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
753
724
  async deleteProjectLabel(
754
725
  input: DeleteProjectPromptLabelInput,
755
726
  ): Promise<DeleteProjectPromptLabelResult> {
727
+ const projectId = this.resolveProjectId(input.projectId);
756
728
  const envelope =
757
729
  await this.transport.request<DeleteProjectPromptLabelResult>(
758
730
  "DELETE",
759
- getProjectPromptLabelPath(input.projectId, input.label),
731
+ getProjectPromptLabelPath(projectId, input.label),
760
732
  );
761
733
  return unwrap(envelope);
762
734
  }
@@ -775,7 +747,7 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
775
747
  const envelope =
776
748
  await this.transport.request<ListProjectPromptLabelUsagesResult>(
777
749
  "GET",
778
- `${getProjectPromptLabelPath(input.projectId, input.label)}/usages`,
750
+ `${getProjectPromptLabelPath(this.resolveProjectId(input.projectId), input.label)}/usages`,
779
751
  );
780
752
  return unwrap(envelope);
781
753
  }
@@ -800,7 +772,9 @@ export class PromptResource<F extends RenderFormat | undefined = undefined> {
800
772
  async listLabelEvents(
801
773
  input: ListLabelEventsInput,
802
774
  ): Promise<ListLabelEventsResult> {
803
- const projectSeg = encodeURIComponent(input.projectId);
775
+ const projectSeg = encodeURIComponent(
776
+ this.resolveProjectId(input.projectId),
777
+ );
804
778
  let path: string;
805
779
  switch (input.scope) {
806
780
  case "label":