@to11ai/sdk 0.6.0 → 0.8.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.
@@ -53,7 +53,9 @@ var zResolvedPromptLegacy = import_zod2.zResolvedPrompt.pick({
53
53
  version: true,
54
54
  labels: true,
55
55
  contentHash: true,
56
- rendered: true
56
+ rendered: true,
57
+ tools: true,
58
+ toolChoice: true
57
59
  });
58
60
  var zResolvedPromptEnvelope = import_zod2.zResolvedPrompt.partial({ releaseId: true }).extend({ strategy: import_zod.z.enum(["single", "weighted"]).optional() });
59
61
  function unwrap(envelope) {
@@ -84,6 +86,12 @@ function normalizeResolvedEnvelope(raw) {
84
86
  labels: raw.labels,
85
87
  rendered: raw.rendered
86
88
  };
89
+ if (raw.tools != null && raw.tools.length > 0) {
90
+ result.tools = raw.tools;
91
+ }
92
+ if (raw.toolChoice != null) {
93
+ result.toolChoice = raw.toolChoice;
94
+ }
87
95
  if (typeof provenance.variantName === "string" && provenance.variantName.length > 0) {
88
96
  result.variantName = provenance.variantName;
89
97
  }
@@ -215,9 +223,10 @@ class PromptResource {
215
223
  subject,
216
224
  variables,
217
225
  cache: useCache = true,
218
- fallback
226
+ fallback,
227
+ developerRole
219
228
  } = options;
220
- const cacheKey = import_cache.buildFetchCacheKey(this.projectId, slug, env, context, subject, variables);
229
+ const cacheKey = import_cache.buildFetchCacheKey(this.projectId, slug, env, context, subject, variables, developerRole);
221
230
  if (useCache) {
222
231
  const cached = this.cache.get(cacheKey);
223
232
  if (cached && "messages" in cached)
@@ -230,6 +239,8 @@ class PromptResource {
230
239
  body.subject = subject;
231
240
  if (variables !== undefined)
232
241
  body.variables = variables;
242
+ if (developerRole !== undefined)
243
+ body.developerRole = developerRole;
233
244
  try {
234
245
  const envelope = await this.transport.request("POST", `${getProjectPromptsPath(this.projectId)}:resolve`, {
235
246
  body,
@@ -2,7 +2,7 @@ import type { Transport } from "../transport";
2
2
  import type { CacheConfig } from "./cache";
3
3
  import type { ArchivePromptInput, ArchivePromptResult, CreateProjectPromptLabelInput, CreateProjectPromptLabelResult, CreatePromptInput, CreatePromptResult, CreatePromptVersionInput, CreatePromptVersionResult, DeleteProjectPromptLabelInput, DeleteProjectPromptLabelResult, FetchedPrompt, FetchPromptOptions, GetProjectPromptLabelInput, GetProjectPromptLabelResult, GetPromptInput, GetPromptResult, GetPromptVersionInput, GetPromptVersionResult, ListLabelEventsInput, ListLabelEventsResult, ListProjectPromptLabelsInput, ListProjectPromptLabelsResult, ListProjectPromptLabelUsagesInput, ListProjectPromptLabelUsagesResult, ListPromptLabelsInput, ListPromptLabelsResult, ListPromptsInput, ListPromptsResult, ListPromptVersionsInput, ListPromptVersionsResult, MovePromptLabelInput, MovePromptLabelResult, PaginatedResult, ResolvePromptInput, ResolvePromptResult, UpdateProjectPromptLabelInput, UpdateProjectPromptLabelResult, UpdatePromptInput, UpdatePromptResult } from "./types";
4
4
  export type { CacheConfig } from "./cache";
5
- export type { ArchivePromptInput, ArchivePromptResult, BlockRenderRecord, ChatMessage, CreateProjectPromptLabelInput, CreateProjectPromptLabelResult, CreatePromptInput, CreatePromptResult, CreatePromptVersionInput, CreatePromptVersionResult, DeleteProjectPromptLabelInput, DeleteProjectPromptLabelResult, FetchedPrompt, FetchPromptOptions, GetProjectPromptLabelInput, GetProjectPromptLabelResult, GetPromptInput, GetPromptResult, GetPromptVersionInput, GetPromptVersionResult, ListLabelEventsInput, ListLabelEventsResult, ListProjectPromptLabelsInput, ListProjectPromptLabelsResult, ListProjectPromptLabelUsagesInput, ListProjectPromptLabelUsagesResult, ListPromptLabelsInput, ListPromptLabelsResult, ListPromptsInput, ListPromptsResult, ListPromptVersionsInput, ListPromptVersionsResult, MovePromptLabelInput, MovePromptLabelResult, PaginatedResult, PaginationInfo, PaletteToken, ProjectPromptLabel, ProjectPromptLabelUsage, Prompt, PromptContext, PromptFormat, PromptLabelEvent, PromptLabelPointer, PromptStatus, PromptVersion, RenderedChatMessage, RenderedPrompt, ResolvedPrompt, ResolvedPromptEnvelope, ResolvePromptInput, ResolvePromptResult, RowError, TextChatMessage, TextChatRole, ToolChatMessage, UpdateProjectPromptLabelInput, UpdateProjectPromptLabelResult, UpdatePromptInput, UpdatePromptResult, } from "./types";
5
+ export type { ArchivePromptInput, ArchivePromptResult, BlockRenderRecord, ChatMessage, CreateProjectPromptLabelInput, CreateProjectPromptLabelResult, CreatePromptInput, CreatePromptResult, CreatePromptVersionInput, CreatePromptVersionResult, DeleteProjectPromptLabelInput, DeleteProjectPromptLabelResult, FetchedPrompt, FetchPromptOptions, GetProjectPromptLabelInput, GetProjectPromptLabelResult, GetPromptInput, GetPromptResult, GetPromptVersionInput, GetPromptVersionResult, ListLabelEventsInput, ListLabelEventsResult, ListProjectPromptLabelsInput, ListProjectPromptLabelsResult, ListProjectPromptLabelUsagesInput, ListProjectPromptLabelUsagesResult, ListPromptLabelsInput, ListPromptLabelsResult, ListPromptsInput, ListPromptsResult, ListPromptVersionsInput, ListPromptVersionsResult, MovePromptLabelInput, MovePromptLabelResult, PaginatedResult, PaginationInfo, PaletteToken, ProjectPromptLabel, ProjectPromptLabelUsage, Prompt, PromptContext, PromptFormat, PromptLabelEvent, PromptLabelPointer, PromptStatus, PromptVersion, RenderedChatMessage, RenderedPrompt, ResolvedPrompt, ResolvedPromptEnvelope, ResolvedToolCall, ResolvedToolDefinition, ResolvePromptInput, ResolvePromptResult, RowError, TextChatMessage, TextChatRole, UpdateProjectPromptLabelInput, UpdateProjectPromptLabelResult, UpdatePromptInput, UpdatePromptResult, } from "./types";
6
6
  /**
7
7
  * Init-time config for the env-aware `fetch()` surface (TO11-980 / S9).
8
8
  *
@@ -10,7 +10,9 @@ var zResolvedPromptLegacy = zResolvedPrompt.pick({
10
10
  version: true,
11
11
  labels: true,
12
12
  contentHash: true,
13
- rendered: true
13
+ rendered: true,
14
+ tools: true,
15
+ toolChoice: true
14
16
  });
15
17
  var zResolvedPromptEnvelope = zResolvedPrompt.partial({ releaseId: true }).extend({ strategy: z.enum(["single", "weighted"]).optional() });
16
18
  function unwrap(envelope) {
@@ -41,6 +43,12 @@ function normalizeResolvedEnvelope(raw) {
41
43
  labels: raw.labels,
42
44
  rendered: raw.rendered
43
45
  };
46
+ if (raw.tools != null && raw.tools.length > 0) {
47
+ result.tools = raw.tools;
48
+ }
49
+ if (raw.toolChoice != null) {
50
+ result.toolChoice = raw.toolChoice;
51
+ }
44
52
  if (typeof provenance.variantName === "string" && provenance.variantName.length > 0) {
45
53
  result.variantName = provenance.variantName;
46
54
  }
@@ -172,9 +180,10 @@ class PromptResource {
172
180
  subject,
173
181
  variables,
174
182
  cache: useCache = true,
175
- fallback
183
+ fallback,
184
+ developerRole
176
185
  } = options;
177
- const cacheKey = buildFetchCacheKey(this.projectId, slug, env, context, subject, variables);
186
+ const cacheKey = buildFetchCacheKey(this.projectId, slug, env, context, subject, variables, developerRole);
178
187
  if (useCache) {
179
188
  const cached = this.cache.get(cacheKey);
180
189
  if (cached && "messages" in cached)
@@ -187,6 +196,8 @@ class PromptResource {
187
196
  body.subject = subject;
188
197
  if (variables !== undefined)
189
198
  body.variables = variables;
199
+ if (developerRole !== undefined)
200
+ body.developerRole = developerRole;
190
201
  try {
191
202
  const envelope = await this.transport.request("POST", `${getProjectPromptsPath(this.projectId)}:resolve`, {
192
203
  body,
@@ -1,4 +1,4 @@
1
- import type { ProjectPromptLabel as GenProjectPromptLabel, ProjectPromptLabelUsage as GenProjectPromptLabelUsage, Prompt as GenPrompt, PromptLabelEvent as GenPromptLabelEvent, PromptLabelPointer as GenPromptLabelPointer, PromptVersion as GenPromptVersion, RenderedChatPrompt as GenRenderedChatPrompt, RenderedPrompt as GenRenderedPrompt, ResolvedPrompt as GenResolvedPrompt } from "../generated/types.gen";
1
+ import type { ProjectPromptLabel as GenProjectPromptLabel, ProjectPromptLabelUsage as GenProjectPromptLabelUsage, Prompt as GenPrompt, PromptLabelEvent as GenPromptLabelEvent, PromptLabelPointer as GenPromptLabelPointer, PromptVersion as GenPromptVersion, RenderedChatPrompt as GenRenderedChatPrompt, RenderedPrompt as GenRenderedPrompt, ResolvedPrompt as GenResolvedPrompt, ResolvedToolCall as GenResolvedToolCall, ResolvedToolDefinition as GenResolvedToolDefinition } from "../generated/types.gen";
2
2
  export interface RowError {
3
3
  path: ReadonlyArray<string | number>;
4
4
  code: string;
@@ -15,17 +15,21 @@ export interface TextChatMessage {
15
15
  content: string;
16
16
  disabled?: boolean;
17
17
  }
18
- export interface ToolChatMessage {
19
- role: "tool";
20
- name: string;
21
- description?: string;
22
- parameters: unknown;
23
- disabled?: boolean;
24
- }
25
- export type ChatMessage = TextChatMessage | ToolChatMessage;
18
+ export type ChatMessage = TextChatMessage;
26
19
  export type RenderedChatMessage = GenRenderedChatPrompt["messages"][number];
27
20
  export type RenderedPrompt = GenRenderedPrompt;
28
- export type ResolvedPrompt = Pick<GenResolvedPrompt, "promptId" | "versionId" | "version" | "labels" | "contentHash" | "rendered">;
21
+ /**
22
+ * A provider-neutral tool definition (`{ name, description?, parameters }`),
23
+ * sourced from authored tool-definition blocks. Surfaced on the resolved
24
+ * contract and shaped for a provider via `toOpenAITools` / `toAnthropicTools`.
25
+ */
26
+ export type ResolvedToolDefinition = GenResolvedToolDefinition;
27
+ /**
28
+ * A structured assistant tool call (`{ id, name, arguments }`) carried inline
29
+ * on a rendered `assistant` turn (few-shot multi-tool history).
30
+ */
31
+ export type ResolvedToolCall = GenResolvedToolCall;
32
+ export type ResolvedPrompt = Pick<GenResolvedPrompt, "promptId" | "versionId" | "version" | "labels" | "contentHash" | "rendered" | "tools" | "toolChoice">;
29
33
  export interface PaginationInfo {
30
34
  page: number;
31
35
  limit: number;
@@ -77,6 +81,7 @@ export interface CreatePromptVersionInput {
77
81
  format: PromptFormat;
78
82
  templateJson: unknown;
79
83
  variablesSchema: unknown;
84
+ contextSchema?: unknown;
80
85
  modelConfig?: unknown;
81
86
  changelog?: string;
82
87
  }
@@ -130,11 +135,34 @@ export interface FetchedPrompt {
130
135
  contentHash: string;
131
136
  labels: string[];
132
137
  rendered: RenderedPrompt;
138
+ /**
139
+ * Provider-neutral tool definitions lifted from authored tool-definition
140
+ * blocks. Absent (rather than `[]`) when the resolved version declares no
141
+ * tools. Shape for a provider with `toOpenAITools` / `toAnthropicTools`.
142
+ */
143
+ tools?: ResolvedToolDefinition[];
144
+ /**
145
+ * Provider-neutral tool-choice directive lifted from the resolved version.
146
+ * Absent (rather than `null`) when the version declares none. Feed straight
147
+ * into `toOpenAIToolChoice` / `toAnthropicToolChoice`.
148
+ *
149
+ * `NonNullable` because `normalizeResolvedEnvelope` maps wire-`null` to absent,
150
+ * so a present `FetchedPrompt.toolChoice` is never `null` — callers get the
151
+ * key or nothing, and never have to branch on `null`.
152
+ */
153
+ toolChoice?: NonNullable<ResolvedPrompt["toolChoice"]>;
133
154
  }
134
155
  export type ResolvedPromptEnvelope = GenResolvedPrompt & {
135
156
  /** Active release strategy. Used to decide weighted-no-subject cache bypass (D8). */
136
157
  strategy?: "single" | "weighted";
137
158
  };
159
+ /**
160
+ * How an authored `developer` block is rendered. The caller controls this
161
+ * per-call because it knows the upstream it targets. Omitted ⇒ `"user"`
162
+ * (back-compatible; safe for any upstream). `"developer"` preserves the role
163
+ * (OpenAI); `"system"` folds it into a system message.
164
+ */
165
+ export type DeveloperRole = "user" | "developer" | "system";
138
166
  export interface FetchPromptOptions {
139
167
  env?: string;
140
168
  context?: PromptContext;
@@ -142,6 +170,12 @@ export interface FetchPromptOptions {
142
170
  variables?: Record<string, unknown>;
143
171
  cache?: boolean;
144
172
  fallback?: ResolvedPrompt | FetchedPrompt;
173
+ /**
174
+ * How authored `developer` blocks render. Default `"user"`. Forwarded on the
175
+ * resolve request and folded into the cache key so two calls differing only
176
+ * by `developerRole` never share a cached result.
177
+ */
178
+ developerRole?: DeveloperRole;
145
179
  }
146
180
  export type PaletteToken = ProjectPromptLabel["displayColor"];
147
181
  export type ProjectPromptLabel = GenProjectPromptLabel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@to11ai/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.8.1",
4
4
  "license": "MPL-2.0",
5
5
  "repository": {
6
6
  "type": "git",
package/src/client.ts CHANGED
@@ -66,6 +66,7 @@ export { PromptResource } from "./prompts/resource";
66
66
  // TO11-980 (S9): env-aware fetch() surface types at the package entrypoint (AC9).
67
67
  export type {
68
68
  BlockRenderRecord,
69
+ DeveloperRole,
69
70
  FetchedPrompt,
70
71
  FetchPromptOptions,
71
72
  PromptContext,
package/src/gateway.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  ENV_SLUG_REGEX,
3
3
  type FetchedPrompt,
4
+ type RenderedChatMessage,
4
5
  type ResolvedPrompt,
6
+ type ResolvedToolDefinition,
5
7
  } from "./prompts/types";
6
8
 
7
9
  export interface GatewayAuthOptions {
@@ -117,3 +119,298 @@ export function gatewayPromptHeaders(
117
119
  }
118
120
  return headers;
119
121
  }
122
+
123
+ // ─── Provider-shape converters (TO11-2592) ───────────────────────────────────
124
+ //
125
+ // The to11 SDK is HEADER-ONLY: it never builds an LLM request body. The
126
+ // customer's own provider client sends the messages + tools. These converters
127
+ // reshape the provider-neutral resolved data (`tools`, rendered `messages`)
128
+ // into the shapes the OpenAI / Anthropic clients expect, so callers don't
129
+ // hand-roll the translation. They are pure and side-effect free.
130
+
131
+ /** OpenAI Chat Completions `tools[]` entry (`function` tool). */
132
+ export interface OpenAITool {
133
+ type: "function";
134
+ function: {
135
+ name: string;
136
+ description?: string;
137
+ parameters: Record<string, unknown>;
138
+ };
139
+ }
140
+
141
+ /** OpenAI `tool_choice` value for a Chat Completions request. */
142
+ export type OpenAIToolChoice =
143
+ | "auto"
144
+ | "none"
145
+ | "required"
146
+ | { type: "function"; function: { name: string } };
147
+
148
+ /** OpenAI Chat Completions assistant `tool_calls[]` entry. */
149
+ export interface OpenAIToolCall {
150
+ id: string;
151
+ type: "function";
152
+ function: { name: string; arguments: string };
153
+ }
154
+
155
+ /** OpenAI Chat Completions `messages[]` entry. */
156
+ export interface OpenAIChatMessage {
157
+ role: "system" | "developer" | "user" | "assistant" | "tool";
158
+ content?: string | null;
159
+ tool_calls?: OpenAIToolCall[];
160
+ tool_call_id?: string;
161
+ }
162
+
163
+ /** Anthropic Messages `tools[]` entry. */
164
+ export interface AnthropicTool {
165
+ name: string;
166
+ description?: string;
167
+ input_schema: Record<string, unknown>;
168
+ }
169
+
170
+ /** Anthropic Messages content block (the subset the converters emit). */
171
+ export type AnthropicContentBlock =
172
+ | { type: "text"; text: string }
173
+ | {
174
+ type: "tool_use";
175
+ id: string;
176
+ name: string;
177
+ input: Record<string, unknown>;
178
+ }
179
+ | { type: "tool_result"; tool_use_id: string; content: string };
180
+
181
+ /** Anthropic Messages `messages[]` entry. */
182
+ export interface AnthropicMessage {
183
+ role: "user" | "assistant";
184
+ content: string | AnthropicContentBlock[];
185
+ }
186
+
187
+ /** Anthropic Messages `tool_choice` value. */
188
+ export type AnthropicToolChoice =
189
+ | { type: "auto" }
190
+ | { type: "any" }
191
+ | { type: "none" }
192
+ | { type: "tool"; name: string };
193
+
194
+ /**
195
+ * Shape resolved tool definitions for the OpenAI Chat Completions `tools`
196
+ * field: `{ type: "function", function: { name, description?, parameters } }`.
197
+ *
198
+ * Accepts the optional `prompt.tools` directly (nullish ⇒ `[]`). `description`
199
+ * is emitted only when present (OpenAI rejects a `null` description).
200
+ *
201
+ * @example
202
+ * const tools = toOpenAITools(prompt.tools);
203
+ * const tool_choice = toOpenAIToolChoice(prompt.toolChoice);
204
+ * await openai.chat.completions.create({ model, messages, tools, tool_choice });
205
+ */
206
+ export function toOpenAITools(
207
+ tools: ReadonlyArray<ResolvedToolDefinition> | null | undefined,
208
+ ): OpenAITool[] {
209
+ if (tools == null) return [];
210
+ return tools.map((tool) => {
211
+ const fn: OpenAITool["function"] = {
212
+ name: tool.name,
213
+ parameters: tool.parameters,
214
+ };
215
+ if (typeof tool.description === "string") {
216
+ fn.description = tool.description;
217
+ }
218
+ return { type: "function", function: fn };
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Map a resolved prompt's provider-neutral `toolChoice` to the OpenAI
224
+ * Chat Completions `tool_choice` field.
225
+ *
226
+ * - `null` / `undefined` → `undefined` (omit the field)
227
+ * - `"auto"` → `"auto"`, `"none"` → `"none"`, `"required"` → `"required"`
228
+ * - `{ name }` → `{ type: "function", function: { name } }`
229
+ *
230
+ * @example
231
+ * const tools = toOpenAITools(prompt.tools);
232
+ * const tool_choice = toOpenAIToolChoice(prompt.toolChoice);
233
+ * await openai.chat.completions.create({ model, messages, tools, tool_choice });
234
+ */
235
+ export function toOpenAIToolChoice(
236
+ toolChoice: ResolvedPrompt["toolChoice"],
237
+ ): OpenAIToolChoice | undefined {
238
+ if (toolChoice == null) return undefined;
239
+ if (typeof toolChoice === "string") return toolChoice;
240
+ return { type: "function", function: { name: toolChoice.name } };
241
+ }
242
+
243
+ /**
244
+ * Shape resolved tool definitions for the Anthropic Messages `tools` field:
245
+ * `{ name, description?, input_schema }` (Anthropic names the JSON Schema field
246
+ * `input_schema`, mapping from the provider-neutral `parameters`).
247
+ *
248
+ * @example
249
+ * const tools = toAnthropicTools(prompt.tools);
250
+ * const tool_choice = toAnthropicToolChoice(prompt.toolChoice);
251
+ * await anthropic.messages.create({ model, system, messages, tools, tool_choice });
252
+ */
253
+ export function toAnthropicTools(
254
+ tools: ReadonlyArray<ResolvedToolDefinition> | null | undefined,
255
+ ): AnthropicTool[] {
256
+ if (tools == null) return [];
257
+ return tools.map((tool) => {
258
+ const out: AnthropicTool = {
259
+ name: tool.name,
260
+ input_schema: tool.parameters,
261
+ };
262
+ if (typeof tool.description === "string") {
263
+ out.description = tool.description;
264
+ }
265
+ return out;
266
+ });
267
+ }
268
+
269
+ /**
270
+ * Map a resolved prompt's provider-neutral `toolChoice` to the Anthropic
271
+ * Messages `tool_choice` field.
272
+ *
273
+ * - `null` / `undefined` → `undefined` (omit the field)
274
+ * - `"auto"` → `{ type: "auto" }`
275
+ * - `"none"` → `{ type: "none" }` (the Anthropic API supports this natively)
276
+ * - `"required"` → `{ type: "any" }` (Anthropic's name for "must invoke a tool")
277
+ * - `{ name }` → `{ type: "tool", name }`
278
+ *
279
+ * @example
280
+ * const tools = toAnthropicTools(prompt.tools);
281
+ * const tool_choice = toAnthropicToolChoice(prompt.toolChoice);
282
+ * await anthropic.messages.create({ model, system, messages, tools, tool_choice });
283
+ */
284
+ export function toAnthropicToolChoice(
285
+ toolChoice: ResolvedPrompt["toolChoice"],
286
+ ): AnthropicToolChoice | undefined {
287
+ if (toolChoice == null) return undefined;
288
+ if (toolChoice === "auto") return { type: "auto" };
289
+ if (toolChoice === "none") return { type: "none" };
290
+ if (toolChoice === "required") return { type: "any" };
291
+ return { type: "tool", name: toolChoice.name };
292
+ }
293
+
294
+ /**
295
+ * Guard a tool-call linkage id before it is serialized into a provider payload.
296
+ * A missing/empty id means malformed tool history (an assistant tool call or a
297
+ * tool result authored without its `id`/`toolCallId`); emitting an empty string
298
+ * would produce a request the provider rejects with an opaque error, so fail
299
+ * fast with a clear message instead.
300
+ */
301
+ function requireToolCallId(id: string | undefined | null): string {
302
+ if (typeof id !== "string" || id.length === 0) {
303
+ throw new Error(
304
+ "to11 SDK: malformed tool history — a tool call/result is missing its id (toolCallId). Author a non-empty id before converting messages.",
305
+ );
306
+ }
307
+ return id;
308
+ }
309
+
310
+ /**
311
+ * Shape rendered turns for the OpenAI Chat Completions `messages` field.
312
+ *
313
+ * - `system` / `developer` / `user` → `{ role, content }` (OpenAI carries
314
+ * `developer` natively).
315
+ * - `assistant` → `{ role, content, tool_calls? }`; structured `toolCalls`
316
+ * become `tool_calls[]` with JSON-stringified `arguments` (the OpenAI wire
317
+ * shape). A pure tool-call turn keeps `content: null`.
318
+ * - `tool` (result) → `{ role: "tool", tool_call_id, content }`.
319
+ *
320
+ * @example
321
+ * const messages = toOpenAIMessages(prompt.messages);
322
+ */
323
+ export function toOpenAIMessages(
324
+ messages: ReadonlyArray<RenderedChatMessage>,
325
+ ): OpenAIChatMessage[] {
326
+ return messages.map((message) => {
327
+ if (message.role === "tool") {
328
+ return {
329
+ role: "tool",
330
+ tool_call_id: requireToolCallId(message.toolCallId),
331
+ content: message.content ?? "",
332
+ };
333
+ }
334
+ if (message.role === "assistant") {
335
+ const out: OpenAIChatMessage = {
336
+ role: "assistant",
337
+ content: message.content ?? null,
338
+ };
339
+ if (message.toolCalls != null && message.toolCalls.length > 0) {
340
+ out.tool_calls = message.toolCalls.map((call) => ({
341
+ id: requireToolCallId(call.id),
342
+ type: "function",
343
+ function: {
344
+ name: call.name,
345
+ arguments: JSON.stringify(call.arguments),
346
+ },
347
+ }));
348
+ }
349
+ return out;
350
+ }
351
+ return { role: message.role, content: message.content ?? "" };
352
+ });
353
+ }
354
+
355
+ /**
356
+ * Shape rendered turns for the Anthropic Messages `messages` field.
357
+ *
358
+ * - `system` / `developer` turns are OMITTED — Anthropic carries instruction
359
+ * content in the top-level `system` parameter, not the `messages` array
360
+ * (mirrors the gateway's `developer`→`system` fold). Pass them to `system`
361
+ * yourself.
362
+ * - `user` → `{ role: "user", content }`.
363
+ * - `assistant` with `toolCalls` → `{ role: "assistant", content: [text?,
364
+ * ...tool_use] }`; without calls → `{ role: "assistant", content }`.
365
+ * - `tool` (result) → `{ role: "user", content: [{ type: "tool_result",
366
+ * tool_use_id, content }] }`.
367
+ *
368
+ * @example
369
+ * const messages = toAnthropicMessages(prompt.messages);
370
+ */
371
+ export function toAnthropicMessages(
372
+ messages: ReadonlyArray<RenderedChatMessage>,
373
+ ): AnthropicMessage[] {
374
+ const out: AnthropicMessage[] = [];
375
+ for (const message of messages) {
376
+ if (message.role === "system" || message.role === "developer") {
377
+ continue;
378
+ }
379
+ if (message.role === "tool") {
380
+ out.push({
381
+ role: "user",
382
+ content: [
383
+ {
384
+ type: "tool_result",
385
+ tool_use_id: requireToolCallId(message.toolCallId),
386
+ content: message.content ?? "",
387
+ },
388
+ ],
389
+ });
390
+ continue;
391
+ }
392
+ if (message.role === "assistant") {
393
+ if (message.toolCalls != null && message.toolCalls.length > 0) {
394
+ const blocks: AnthropicContentBlock[] = [];
395
+ if (typeof message.content === "string" && message.content.length > 0) {
396
+ blocks.push({ type: "text", text: message.content });
397
+ }
398
+ for (const call of message.toolCalls) {
399
+ blocks.push({
400
+ type: "tool_use",
401
+ id: requireToolCallId(call.id),
402
+ name: call.name,
403
+ input: call.arguments,
404
+ });
405
+ }
406
+ out.push({ role: "assistant", content: blocks });
407
+ } else {
408
+ out.push({ role: "assistant", content: message.content ?? "" });
409
+ }
410
+ continue;
411
+ }
412
+ // `user`
413
+ out.push({ role: "user", content: message.content ?? "" });
414
+ }
415
+ return out;
416
+ }