@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.
- package/dist/client.cjs +88 -18
- package/dist/client.d.ts +141 -39
- package/dist/client.js +94 -19
- package/dist/config.d.ts +9 -0
- package/dist/context.d.ts +124 -0
- package/dist/errors.cjs +18 -1
- package/dist/errors.d.ts +18 -1
- package/dist/errors.js +18 -1
- package/dist/gateway.cjs +65 -22
- package/dist/gateway.d.ts +65 -9
- package/dist/gateway.js +65 -22
- package/dist/generated/index.d.ts +1 -1
- package/dist/generated/types.gen.d.ts +1299 -167
- package/dist/generated/zod.gen.d.ts +393 -232
- package/dist/prompts/cache.d.ts +9 -10
- package/dist/prompts/resource.cjs +29 -67
- package/dist/prompts/resource.d.ts +52 -32
- package/dist/prompts/resource.js +30 -68
- package/dist/prompts/shape.d.ts +56 -0
- package/dist/prompts/types.d.ts +44 -31
- package/package.json +1 -1
- package/src/client.ts +337 -82
- package/src/config.ts +13 -0
- package/src/context.ts +198 -0
- package/src/errors.ts +33 -3
- package/src/gateway.ts +148 -33
- package/src/generated/index.ts +1 -1
- package/src/generated/types.gen.ts +1310 -167
- package/src/generated/zod.gen.ts +361 -271
- package/src/prompts/cache.ts +11 -15
- package/src/prompts/resource.ts +129 -142
- package/src/prompts/shape.ts +152 -0
- package/src/prompts/types.ts +45 -34
package/src/context.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability context — the `session → conversation → turn` hierarchy that
|
|
3
|
+
* maps onto to11's telemetry grouping (Session → Conversation → Turn →
|
|
4
|
+
* LLM-Call). Each level provides a lazy default for the level below; explicit
|
|
5
|
+
* control is opt-in per use case. See
|
|
6
|
+
* docs/product-specs/2026-07-14-to11-2691-sdk-simpler-setup.md.
|
|
7
|
+
*
|
|
8
|
+
* Header-only: `turn.headers(prompt?)` returns a plain object the caller applies
|
|
9
|
+
* to their own provider client. It never sends a request.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type GatewayPromptProvenance,
|
|
14
|
+
gatewayAuthHeaders,
|
|
15
|
+
gatewayPromptHeaders,
|
|
16
|
+
} from "./gateway";
|
|
17
|
+
|
|
18
|
+
/** Auth/config the client hands to the context for gateway header building. */
|
|
19
|
+
export interface TurnAuthContext {
|
|
20
|
+
readonly apiKey: string;
|
|
21
|
+
readonly projectId: string;
|
|
22
|
+
readonly env?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A prompt argument accepted by `turn.headers()`. Either the flat provenance
|
|
27
|
+
* (a neutral `RenderedPrompt`) or a `format`-shaped result carrying it under
|
|
28
|
+
* `metadata` — both are accepted so the format and neutral render paths carry
|
|
29
|
+
* provenance identically.
|
|
30
|
+
*/
|
|
31
|
+
export type PromptForHeaders =
|
|
32
|
+
| GatewayPromptProvenance
|
|
33
|
+
| { metadata: GatewayPromptProvenance };
|
|
34
|
+
|
|
35
|
+
function randomHex(bytes: number): string {
|
|
36
|
+
const buf = new Uint8Array(bytes);
|
|
37
|
+
crypto.getRandomValues(buf);
|
|
38
|
+
let out = "";
|
|
39
|
+
for (const b of buf) out += b.toString(16).padStart(2, "0");
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A fresh, sampled W3C `traceparent` (`00-<32hex>-<16hex>-01`). */
|
|
44
|
+
function newTraceparent(): string {
|
|
45
|
+
return `00-${randomHex(16)}-${randomHex(8)}-01`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function newId(): string {
|
|
49
|
+
return crypto.randomUUID();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function provenanceOf(prompt: PromptForHeaders): GatewayPromptProvenance {
|
|
53
|
+
return "metadata" in prompt ? prompt.metadata : prompt;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One agent turn — a single trace. A turn's `traceparent` is fixed for its
|
|
58
|
+
* lifetime, so every LLM-call made under it groups into one trace.
|
|
59
|
+
*/
|
|
60
|
+
export class Turn {
|
|
61
|
+
/** The fixed W3C traceparent for this turn (one trace). */
|
|
62
|
+
readonly traceparent: string;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
private readonly auth: TurnAuthContext,
|
|
66
|
+
readonly sessionId: string,
|
|
67
|
+
readonly conversationId: string,
|
|
68
|
+
traceparent?: string,
|
|
69
|
+
) {
|
|
70
|
+
this.traceparent = traceparent ?? newTraceparent();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The complete gateway header bag: auth + session + conversation +
|
|
75
|
+
* traceparent, plus prompt provenance when a `prompt` is supplied. A pure
|
|
76
|
+
* read of the turn's fixed ids — reuse it across a tool-loop or call per
|
|
77
|
+
* request; the result is identical.
|
|
78
|
+
*
|
|
79
|
+
* Only valid for requests targeting the gateway (`GATEWAY_URL`); it carries
|
|
80
|
+
* the to11 API key and must never be attached to a direct provider endpoint.
|
|
81
|
+
*
|
|
82
|
+
* @example One call (inline turn)
|
|
83
|
+
* const res = await openai.chat.completions.create(
|
|
84
|
+
* { ...prompt.config, messages: prompt.messages },
|
|
85
|
+
* { headers: to11.turn().headers(prompt) },
|
|
86
|
+
* );
|
|
87
|
+
*
|
|
88
|
+
* @example Tool loop — hoist the turn so every call shares one trace
|
|
89
|
+
* const turn = to11.turn();
|
|
90
|
+
* const headers = turn.headers(prompt);
|
|
91
|
+
* while (true) {
|
|
92
|
+
* const res = await openai.chat.completions.create({ ...prompt.config, messages, tools }, { headers });
|
|
93
|
+
* // …reuse `headers` for every call in the loop…
|
|
94
|
+
* }
|
|
95
|
+
*/
|
|
96
|
+
headers(prompt?: PromptForHeaders): Record<string, string> {
|
|
97
|
+
const headers: Record<string, string> = {
|
|
98
|
+
...gatewayAuthHeaders({
|
|
99
|
+
apiKey: this.auth.apiKey,
|
|
100
|
+
projectId: this.auth.projectId,
|
|
101
|
+
...(this.auth.env !== undefined ? { env: this.auth.env } : {}),
|
|
102
|
+
}),
|
|
103
|
+
"x-to11-session-id": this.sessionId,
|
|
104
|
+
"x-to11-conversation-id": this.conversationId,
|
|
105
|
+
traceparent: this.traceparent,
|
|
106
|
+
};
|
|
107
|
+
if (prompt !== undefined) {
|
|
108
|
+
Object.assign(headers, gatewayPromptHeaders(provenanceOf(prompt)));
|
|
109
|
+
}
|
|
110
|
+
return headers;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** One agent thread — a stable conversation id shared across all its turns. */
|
|
115
|
+
export class Conversation {
|
|
116
|
+
/** Stable conversation id, reused by every turn created from this object. */
|
|
117
|
+
readonly id: string;
|
|
118
|
+
|
|
119
|
+
constructor(
|
|
120
|
+
private readonly auth: TurnAuthContext,
|
|
121
|
+
readonly sessionId: string,
|
|
122
|
+
id?: string,
|
|
123
|
+
) {
|
|
124
|
+
this.id = id ?? newId();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Start a new turn (fresh trace) in this conversation. */
|
|
128
|
+
turn(): Turn {
|
|
129
|
+
return new Turn(this.auth, this.sessionId, this.id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* One unit of work. Groups conversations under a single session id. A session
|
|
135
|
+
* lazily owns one default conversation, used by `session.turn()`; call
|
|
136
|
+
* `session.conversation()` for additional (side/parallel) agent threads.
|
|
137
|
+
*/
|
|
138
|
+
export class Session {
|
|
139
|
+
/** Stable session id, carried by every conversation and turn beneath it. */
|
|
140
|
+
readonly id: string;
|
|
141
|
+
private defaultConversation: Conversation | undefined;
|
|
142
|
+
|
|
143
|
+
constructor(
|
|
144
|
+
private readonly auth: TurnAuthContext,
|
|
145
|
+
id?: string,
|
|
146
|
+
) {
|
|
147
|
+
this.id = id ?? newId();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a distinct conversation (a separate agent thread) in this session.
|
|
152
|
+
* Use this when one unit of work runs multiple agents (side / parallel
|
|
153
|
+
* agents): each conversation gets its own `x-to11-conversation-id`, all under
|
|
154
|
+
* the one session.
|
|
155
|
+
*
|
|
156
|
+
* @example Multiple agents in one session
|
|
157
|
+
* const session = to11.session();
|
|
158
|
+
* const main = session.conversation(); // main agent thread
|
|
159
|
+
* const research = session.conversation(); // separate research thread, same session
|
|
160
|
+
*
|
|
161
|
+
* await openai.chat.completions.create(
|
|
162
|
+
* { ...mainPrompt.config, messages: mainMessages },
|
|
163
|
+
* { headers: main.turn().headers(mainPrompt) },
|
|
164
|
+
* );
|
|
165
|
+
* await openai.chat.completions.create(
|
|
166
|
+
* { ...researchPrompt.config, messages: researchMessages },
|
|
167
|
+
* { headers: research.turn().headers(researchPrompt) },
|
|
168
|
+
* );
|
|
169
|
+
*/
|
|
170
|
+
conversation(id?: string): Conversation {
|
|
171
|
+
return new Conversation(this.auth, this.id, id);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Start a turn in the session's single implicit conversation. The default
|
|
176
|
+
* conversation is created lazily on first call and reused, so consecutive
|
|
177
|
+
* `session.turn()` calls share one `x-to11-conversation-id` (each with its
|
|
178
|
+
* own `traceparent`).
|
|
179
|
+
*
|
|
180
|
+
* @example Multi-turn chat — every turn shares the session's conversation
|
|
181
|
+
* const session = to11.session();
|
|
182
|
+
* for await (const input of inputs) {
|
|
183
|
+
* messages.push({ role: "user", content: input });
|
|
184
|
+
* const turn = session.turn(); // fresh trace, same conversation id
|
|
185
|
+
* const res = await openai.chat.completions.create(
|
|
186
|
+
* { ...prompt.config, messages },
|
|
187
|
+
* { headers: turn.headers(prompt) },
|
|
188
|
+
* );
|
|
189
|
+
* messages.push(res.choices[0].message);
|
|
190
|
+
* }
|
|
191
|
+
*/
|
|
192
|
+
turn(): Turn {
|
|
193
|
+
if (this.defaultConversation === undefined) {
|
|
194
|
+
this.defaultConversation = this.conversation();
|
|
195
|
+
}
|
|
196
|
+
return this.defaultConversation.turn();
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -113,6 +113,36 @@ export class To11aiResponseValidationError extends To11aiError {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Thrown by the provider-shape converters (`toOpenAIMessages` /
|
|
118
|
+
* `toAnthropicMessages`, and `render({ format })`) in `onUnknownRole: "error"`
|
|
119
|
+
* mode when a message carries a role outside the known set
|
|
120
|
+
* (`system | user | assistant | developer | tool`). `code` is the stable
|
|
121
|
+
* `"unknown_role"` slug; `role` / `index` locate the offending turn.
|
|
122
|
+
*
|
|
123
|
+
* A client-side conversion error — it extends `To11aiError` (not
|
|
124
|
+
* `To11aiApiError`), so it never carries an HTTP status.
|
|
125
|
+
*/
|
|
126
|
+
export class UnknownRoleError extends To11aiError {
|
|
127
|
+
readonly code = "unknown_role";
|
|
128
|
+
readonly role: string;
|
|
129
|
+
readonly index: number;
|
|
130
|
+
|
|
131
|
+
constructor(role: string, index: number) {
|
|
132
|
+
super(
|
|
133
|
+
`[to11ai/sdk] message[${index}] has unknown role ${JSON.stringify(role)}; expected system|user|assistant|developer|tool`,
|
|
134
|
+
);
|
|
135
|
+
this.name = "UnknownRoleError";
|
|
136
|
+
this.role = role;
|
|
137
|
+
this.index = index;
|
|
138
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static is(err: unknown): err is UnknownRoleError {
|
|
142
|
+
return err instanceof UnknownRoleError;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
/**
|
|
117
147
|
* Thrown when strict-mode rejects a label move for an unregistered label.
|
|
118
148
|
*
|
|
@@ -252,7 +282,7 @@ export class EnvNotEnabledForProviderError extends To11aiApiError {
|
|
|
252
282
|
|
|
253
283
|
// ─── Prompt resolution typed errors (TO11-980 / S9) ────────────────────────
|
|
254
284
|
//
|
|
255
|
-
// These four subclasses give `prompts.
|
|
285
|
+
// These four subclasses give `prompts.render()` callers a discriminable error
|
|
256
286
|
// family for prompt resolution. Each:
|
|
257
287
|
// - extends `To11aiApiError`, so existing broad `catch (err instanceof
|
|
258
288
|
// To11aiApiError)` blocks keep matching (AC8),
|
|
@@ -327,7 +357,7 @@ export interface EnvNotFoundReason {
|
|
|
327
357
|
}
|
|
328
358
|
|
|
329
359
|
/**
|
|
330
|
-
* Thrown when no serving environment can be resolved for a `prompts.
|
|
360
|
+
* Thrown when no serving environment can be resolved for a `prompts.render()`
|
|
331
361
|
* call. One class, two `reason.source` discriminants (see
|
|
332
362
|
* `EnvNotFoundReason`): client-eager `"missing"` (R4) and server-forwarded
|
|
333
363
|
* `"unknown"` (`ENV_NOT_FOUND`).
|
|
@@ -357,7 +387,7 @@ export class EnvNotFoundError extends To11aiApiError {
|
|
|
357
387
|
/** Build the client-eager (missing-at-both) variant — no transport call. */
|
|
358
388
|
static missing(env?: string): EnvNotFoundError {
|
|
359
389
|
return new EnvNotFoundError(
|
|
360
|
-
"[to11ai/sdk] No environment resolvable for prompts.
|
|
390
|
+
"[to11ai/sdk] No environment resolvable for prompts.render(): none supplied at client init and none supplied per call. Pass `env` to createClient or render(slug, { env }).",
|
|
361
391
|
{ env, source: "missing" },
|
|
362
392
|
undefined,
|
|
363
393
|
0,
|
package/src/gateway.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { UnknownRoleError } from "./errors";
|
|
1
2
|
import {
|
|
2
3
|
ENV_SLUG_REGEX,
|
|
3
|
-
type FetchedPrompt,
|
|
4
4
|
type RenderedChatMessage,
|
|
5
5
|
type ResolvedPrompt,
|
|
6
6
|
type ResolvedToolDefinition,
|
|
7
|
+
type UnknownRoleMode,
|
|
7
8
|
} from "./prompts/types";
|
|
8
9
|
|
|
9
10
|
export interface GatewayAuthOptions {
|
|
@@ -57,7 +58,7 @@ export function gatewayAuthHeaders(
|
|
|
57
58
|
}
|
|
58
59
|
// Enforce the project-environments registry slug rule (lowercase-kebab,
|
|
59
60
|
// 1-63 chars) eagerly — same `ENV_SLUG_REGEX` already applied on the
|
|
60
|
-
// createClient /
|
|
61
|
+
// createClient / render() paths — so a misconfigured env (e.g. "Prod",
|
|
61
62
|
// "bad env", >63 chars) fails locally with a clear SDK error instead of
|
|
62
63
|
// being copied verbatim into x-to11-env and rejected later at the
|
|
63
64
|
// gateway/API boundary (TO11-1424).
|
|
@@ -78,44 +79,66 @@ export function gatewayAuthHeaders(
|
|
|
78
79
|
* follows from a resolved prompt:
|
|
79
80
|
* - `x-to11-prompt-id`, `x-to11-prompt-version` — always.
|
|
80
81
|
* - `x-to11-release-id` — when the result carries a non-null `releaseId`
|
|
81
|
-
* (the enriched `
|
|
82
|
+
* (the enriched `render()` result; a legacy `resolve()` result has no
|
|
82
83
|
* `releaseId`, so it is omitted — non-breaking).
|
|
83
84
|
* - `x-to11-variant-name` — IFF `variantName` is present (i.e. the resolved
|
|
84
85
|
* release was weighted); omitted for `single`-strategy releases.
|
|
85
|
-
* - `x-to11-prompt-
|
|
86
|
-
*
|
|
86
|
+
* - `x-to11-prompt-label` — the single label the prompt resolved through (the
|
|
87
|
+
* env slug today); omitted when empty (the gateway rejects empty values).
|
|
88
|
+
* - `x-to11-prompt-slug` — the prompt's human-readable slug, so traces show a
|
|
89
|
+
* readable name alongside the opaque `x-to11-prompt-id`; omitted when empty
|
|
90
|
+
* or absent (a legacy `resolve()` result carries no slug).
|
|
87
91
|
*
|
|
88
|
-
* The parameter type is WIDENED to accept either the enriched `
|
|
92
|
+
* The parameter type is WIDENED to accept either the enriched `render()`
|
|
89
93
|
* result or the legacy `ResolvedPrompt`. The new fields are read when present
|
|
90
94
|
* and omitted otherwise, so an old `resolve()` result still produces exactly
|
|
91
95
|
* the original three headers (non-breaking; D1/D7 shape coexistence).
|
|
92
96
|
*
|
|
93
97
|
* @example
|
|
94
|
-
* const result = await client.prompts.
|
|
98
|
+
* const result = await client.prompts.render(slug, { env: "prod" });
|
|
95
99
|
* const headers = gatewayPromptHeaders(result);
|
|
96
100
|
*/
|
|
101
|
+
export interface GatewayPromptProvenance {
|
|
102
|
+
promptId: string;
|
|
103
|
+
version: number;
|
|
104
|
+
/** The single label the prompt resolved through (env slug today). */
|
|
105
|
+
label: string;
|
|
106
|
+
/**
|
|
107
|
+
* The prompt's human-readable slug (the `render()`/`resolve()` input). Present
|
|
108
|
+
* on an enriched `render()` result; a legacy `ResolvedPrompt` omits it.
|
|
109
|
+
*/
|
|
110
|
+
slug?: string;
|
|
111
|
+
/** Present on an enriched result; a legacy `resolve()` result omits it. */
|
|
112
|
+
releaseId?: string | null;
|
|
113
|
+
/** Present IFF the resolved release was weighted. */
|
|
114
|
+
variantName?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
97
117
|
export function gatewayPromptHeaders(
|
|
98
|
-
prompt:
|
|
118
|
+
prompt: GatewayPromptProvenance,
|
|
99
119
|
): Record<string, string> {
|
|
100
120
|
const headers: Record<string, string> = {
|
|
101
121
|
"x-to11-prompt-id": prompt.promptId,
|
|
102
122
|
"x-to11-prompt-version": String(prompt.version),
|
|
103
123
|
};
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
// Emit the release id only when it is a non-null, non-empty string — the
|
|
125
|
+
// offline-fallback path can carry an explicit `null`, and the gateway
|
|
126
|
+
// rejects empty header values.
|
|
127
|
+
if (typeof prompt.releaseId === "string" && prompt.releaseId.length > 0) {
|
|
128
|
+
headers["x-to11-release-id"] = prompt.releaseId;
|
|
129
|
+
}
|
|
130
|
+
if (typeof prompt.variantName === "string" && prompt.variantName.length > 0) {
|
|
131
|
+
headers["x-to11-variant-name"] = prompt.variantName;
|
|
111
132
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
headers["x-to11-
|
|
133
|
+
// The single label the prompt resolved through. Omit when empty — the
|
|
134
|
+
// gateway rejects empty header values.
|
|
135
|
+
if (prompt.label.length > 0) {
|
|
136
|
+
headers["x-to11-prompt-label"] = prompt.label;
|
|
116
137
|
}
|
|
117
|
-
|
|
118
|
-
|
|
138
|
+
// The prompt's human-readable slug. Omit when absent/empty — a legacy
|
|
139
|
+
// `resolve()` result carries no slug, and the gateway rejects empty values.
|
|
140
|
+
if (typeof prompt.slug === "string" && prompt.slug.length > 0) {
|
|
141
|
+
headers["x-to11-prompt-slug"] = prompt.slug;
|
|
119
142
|
}
|
|
120
143
|
return headers;
|
|
121
144
|
}
|
|
@@ -307,6 +330,47 @@ function requireToolCallId(id: string | undefined | null): string {
|
|
|
307
330
|
return id;
|
|
308
331
|
}
|
|
309
332
|
|
|
333
|
+
/** Options shared by the message converters. */
|
|
334
|
+
export interface ConvertMessagesOptions {
|
|
335
|
+
/**
|
|
336
|
+
* Policy for a message whose role is outside the known set
|
|
337
|
+
* (`system | user | assistant | developer | tool`). `"drop"` (default) omits
|
|
338
|
+
* it silently; `"warn"` omits it and emits a `console.warn`; `"error"` throws
|
|
339
|
+
* {@link UnknownRoleError}. Both converters apply this identically.
|
|
340
|
+
*/
|
|
341
|
+
onUnknownRole?: UnknownRoleMode;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const KNOWN_RENDERED_ROLES: ReadonlySet<string> = new Set([
|
|
345
|
+
"system",
|
|
346
|
+
"user",
|
|
347
|
+
"assistant",
|
|
348
|
+
"developer",
|
|
349
|
+
"tool",
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Apply the unknown-role policy to a message. Returns `true` when the caller
|
|
354
|
+
* should skip (drop) the message; throws in `"error"` mode. A known role
|
|
355
|
+
* returns `false` (keep). Both converters route through this so their handling
|
|
356
|
+
* of unrecognized roles is identical — no silent divergence.
|
|
357
|
+
*/
|
|
358
|
+
function skipUnknownRole(
|
|
359
|
+
role: string,
|
|
360
|
+
index: number,
|
|
361
|
+
mode: UnknownRoleMode,
|
|
362
|
+
): boolean {
|
|
363
|
+
if (KNOWN_RENDERED_ROLES.has(role)) return false;
|
|
364
|
+
if (mode === "error") throw new UnknownRoleError(role, index);
|
|
365
|
+
if (mode === "warn") {
|
|
366
|
+
console.warn(
|
|
367
|
+
`[to11ai/sdk] dropped message[${index}] with unknown role ${JSON.stringify(role)} (expected system|user|assistant|developer|tool)`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
// "drop": silent.
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
310
374
|
/**
|
|
311
375
|
* Shape rendered turns for the OpenAI Chat Completions `messages` field.
|
|
312
376
|
*
|
|
@@ -316,28 +380,37 @@ function requireToolCallId(id: string | undefined | null): string {
|
|
|
316
380
|
* become `tool_calls[]` with JSON-stringified `arguments` (the OpenAI wire
|
|
317
381
|
* shape). A pure tool-call turn keeps `content: null`.
|
|
318
382
|
* - `tool` (result) → `{ role: "tool", tool_call_id, content }`.
|
|
383
|
+
* - an unrecognized role is handled per `options.onUnknownRole` (default
|
|
384
|
+
* `"drop"`) — dropped, dropped-with-warning, or throws `UnknownRoleError`.
|
|
319
385
|
*
|
|
320
386
|
* @example
|
|
321
387
|
* const messages = toOpenAIMessages(prompt.messages);
|
|
388
|
+
* @example Fail loudly on a malformed message array
|
|
389
|
+
* const messages = toOpenAIMessages(history, { onUnknownRole: "error" });
|
|
322
390
|
*/
|
|
323
391
|
export function toOpenAIMessages(
|
|
324
392
|
messages: ReadonlyArray<RenderedChatMessage>,
|
|
393
|
+
options: ConvertMessagesOptions = {},
|
|
325
394
|
): OpenAIChatMessage[] {
|
|
326
|
-
|
|
395
|
+
const mode = options.onUnknownRole ?? "drop";
|
|
396
|
+
const out: OpenAIChatMessage[] = [];
|
|
397
|
+
messages.forEach((message, index) => {
|
|
398
|
+
if (skipUnknownRole(message.role, index, mode)) return;
|
|
327
399
|
if (message.role === "tool") {
|
|
328
|
-
|
|
400
|
+
out.push({
|
|
329
401
|
role: "tool",
|
|
330
402
|
tool_call_id: requireToolCallId(message.toolCallId),
|
|
331
403
|
content: message.content ?? "",
|
|
332
|
-
};
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
333
406
|
}
|
|
334
407
|
if (message.role === "assistant") {
|
|
335
|
-
const
|
|
408
|
+
const msg: OpenAIChatMessage = {
|
|
336
409
|
role: "assistant",
|
|
337
410
|
content: message.content ?? null,
|
|
338
411
|
};
|
|
339
412
|
if (message.toolCalls != null && message.toolCalls.length > 0) {
|
|
340
|
-
|
|
413
|
+
msg.tool_calls = message.toolCalls.map((call) => ({
|
|
341
414
|
id: requireToolCallId(call.id),
|
|
342
415
|
type: "function",
|
|
343
416
|
function: {
|
|
@@ -346,10 +419,12 @@ export function toOpenAIMessages(
|
|
|
346
419
|
},
|
|
347
420
|
}));
|
|
348
421
|
}
|
|
349
|
-
|
|
422
|
+
out.push(msg);
|
|
423
|
+
return;
|
|
350
424
|
}
|
|
351
|
-
|
|
425
|
+
out.push({ role: message.role, content: message.content ?? "" });
|
|
352
426
|
});
|
|
427
|
+
return out;
|
|
353
428
|
}
|
|
354
429
|
|
|
355
430
|
/**
|
|
@@ -364,18 +439,28 @@ export function toOpenAIMessages(
|
|
|
364
439
|
* ...tool_use] }`; without calls → `{ role: "assistant", content }`.
|
|
365
440
|
* - `tool` (result) → `{ role: "user", content: [{ type: "tool_result",
|
|
366
441
|
* tool_use_id, content }] }`.
|
|
442
|
+
* - an unrecognized role is handled per `options.onUnknownRole` (default
|
|
443
|
+
* `"drop"`) — dropped, dropped-with-warning, or throws `UnknownRoleError` —
|
|
444
|
+
* identically to {@link toOpenAIMessages}. (`system`/`developer` are known
|
|
445
|
+
* roles folded into `system`, not "unknown"; they are never dropped here.)
|
|
367
446
|
*
|
|
368
447
|
* @example
|
|
369
448
|
* const messages = toAnthropicMessages(prompt.messages);
|
|
449
|
+
* @example Fail loudly on a malformed message array
|
|
450
|
+
* const messages = toAnthropicMessages(history, { onUnknownRole: "error" });
|
|
370
451
|
*/
|
|
371
452
|
export function toAnthropicMessages(
|
|
372
453
|
messages: ReadonlyArray<RenderedChatMessage>,
|
|
454
|
+
options: ConvertMessagesOptions = {},
|
|
373
455
|
): AnthropicMessage[] {
|
|
456
|
+
const mode = options.onUnknownRole ?? "drop";
|
|
374
457
|
const out: AnthropicMessage[] = [];
|
|
375
|
-
|
|
458
|
+
messages.forEach((message, index) => {
|
|
376
459
|
if (message.role === "system" || message.role === "developer") {
|
|
377
|
-
|
|
460
|
+
// Folded into the top-level `system` via toAnthropicSystem — not dropped.
|
|
461
|
+
return;
|
|
378
462
|
}
|
|
463
|
+
if (skipUnknownRole(message.role, index, mode)) return;
|
|
379
464
|
if (message.role === "tool") {
|
|
380
465
|
out.push({
|
|
381
466
|
role: "user",
|
|
@@ -387,7 +472,7 @@ export function toAnthropicMessages(
|
|
|
387
472
|
},
|
|
388
473
|
],
|
|
389
474
|
});
|
|
390
|
-
|
|
475
|
+
return;
|
|
391
476
|
}
|
|
392
477
|
if (message.role === "assistant") {
|
|
393
478
|
if (message.toolCalls != null && message.toolCalls.length > 0) {
|
|
@@ -407,10 +492,40 @@ export function toAnthropicMessages(
|
|
|
407
492
|
} else {
|
|
408
493
|
out.push({ role: "assistant", content: message.content ?? "" });
|
|
409
494
|
}
|
|
410
|
-
|
|
495
|
+
return;
|
|
411
496
|
}
|
|
412
497
|
// `user`
|
|
413
498
|
out.push({ role: "user", content: message.content ?? "" });
|
|
414
|
-
}
|
|
499
|
+
});
|
|
415
500
|
return out;
|
|
416
501
|
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Fold the `system` and `developer` instruction turns into Anthropic's
|
|
505
|
+
* top-level `system` string. Anthropic has no `developer` role and takes system
|
|
506
|
+
* instructions as a separate request field, so pair this with
|
|
507
|
+
* {@link toAnthropicMessages} (which omits those turns). Returns `undefined`
|
|
508
|
+
* when there is no system/developer content.
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* const res = await anthropic.messages.create(
|
|
512
|
+
* {
|
|
513
|
+
* ...prompt.config,
|
|
514
|
+
* system: toAnthropicSystem(prompt.messages),
|
|
515
|
+
* messages: toAnthropicMessages(prompt.messages),
|
|
516
|
+
* },
|
|
517
|
+
* { headers: to11.turn().headers(prompt) },
|
|
518
|
+
* );
|
|
519
|
+
*/
|
|
520
|
+
export function toAnthropicSystem(
|
|
521
|
+
messages: ReadonlyArray<RenderedChatMessage>,
|
|
522
|
+
): string | undefined {
|
|
523
|
+
const parts: string[] = [];
|
|
524
|
+
for (const message of messages) {
|
|
525
|
+
if (message.role !== "system" && message.role !== "developer") continue;
|
|
526
|
+
if (typeof message.content === "string" && message.content.length > 0) {
|
|
527
|
+
parts.push(message.content);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
|
531
|
+
}
|