@mupt-ai/dari-router 0.1.0

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.
Files changed (179) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +100 -0
  3. package/dist/anonymous_actions.d.ts +55 -0
  4. package/dist/anonymous_actions.js +277 -0
  5. package/dist/anonymous_actions.js.map +1 -0
  6. package/dist/auto_router.d.ts +10 -0
  7. package/dist/auto_router.js +56 -0
  8. package/dist/auto_router.js.map +1 -0
  9. package/dist/cache_behavior.d.ts +14 -0
  10. package/dist/cache_behavior.js +112 -0
  11. package/dist/cache_behavior.js.map +1 -0
  12. package/dist/cache_scope.d.ts +5 -0
  13. package/dist/cache_scope.js +49 -0
  14. package/dist/cache_scope.js.map +1 -0
  15. package/dist/cache_switch_decision.d.ts +24 -0
  16. package/dist/cache_switch_decision.js +21 -0
  17. package/dist/cache_switch_decision.js.map +1 -0
  18. package/dist/candidate_pruning.d.ts +45 -0
  19. package/dist/candidate_pruning.js +154 -0
  20. package/dist/candidate_pruning.js.map +1 -0
  21. package/dist/candidate_resolution.d.ts +41 -0
  22. package/dist/candidate_resolution.js +111 -0
  23. package/dist/candidate_resolution.js.map +1 -0
  24. package/dist/compatibility.d.ts +22 -0
  25. package/dist/compatibility.js +76 -0
  26. package/dist/compatibility.js.map +1 -0
  27. package/dist/continuation_state.d.ts +36 -0
  28. package/dist/continuation_state.js +249 -0
  29. package/dist/continuation_state.js.map +1 -0
  30. package/dist/cost.d.ts +31 -0
  31. package/dist/cost.js +314 -0
  32. package/dist/cost.js.map +1 -0
  33. package/dist/dari_policy.d.ts +34 -0
  34. package/dist/dari_policy.js +267 -0
  35. package/dist/dari_policy.js.map +1 -0
  36. package/dist/errors.d.ts +7 -0
  37. package/dist/errors.js +14 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/fingerprint.d.ts +13 -0
  40. package/dist/fingerprint.js +130 -0
  41. package/dist/fingerprint.js.map +1 -0
  42. package/dist/fixed_turn_cost_config.d.ts +9 -0
  43. package/dist/fixed_turn_cost_config.js +22 -0
  44. package/dist/fixed_turn_cost_config.js.map +1 -0
  45. package/dist/framework_error.d.ts +8 -0
  46. package/dist/framework_error.js +24 -0
  47. package/dist/framework_error.js.map +1 -0
  48. package/dist/framework_lifecycle.d.ts +11 -0
  49. package/dist/framework_lifecycle.js +57 -0
  50. package/dist/framework_lifecycle.js.map +1 -0
  51. package/dist/framework_models.d.ts +3 -0
  52. package/dist/framework_models.js +120 -0
  53. package/dist/framework_models.js.map +1 -0
  54. package/dist/framework_router.d.ts +2 -0
  55. package/dist/framework_router.js +384 -0
  56. package/dist/framework_router.js.map +1 -0
  57. package/dist/framework_streaming.d.ts +26 -0
  58. package/dist/framework_streaming.js +486 -0
  59. package/dist/framework_streaming.js.map +1 -0
  60. package/dist/framework_types.d.ts +272 -0
  61. package/dist/framework_types.js +5 -0
  62. package/dist/framework_types.js.map +1 -0
  63. package/dist/framework_validation.d.ts +7 -0
  64. package/dist/framework_validation.js +169 -0
  65. package/dist/framework_validation.js.map +1 -0
  66. package/dist/index.d.ts +35 -0
  67. package/dist/index.js +38 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/json.d.ts +10 -0
  70. package/dist/json.js +29 -0
  71. package/dist/json.js.map +1 -0
  72. package/dist/model_ids.d.ts +4 -0
  73. package/dist/model_ids.js +26 -0
  74. package/dist/model_ids.js.map +1 -0
  75. package/dist/pi_context.d.ts +4 -0
  76. package/dist/pi_context.js +206 -0
  77. package/dist/pi_context.js.map +1 -0
  78. package/dist/pi_options.d.ts +3 -0
  79. package/dist/pi_options.js +240 -0
  80. package/dist/pi_options.js.map +1 -0
  81. package/dist/pi_output.d.ts +4 -0
  82. package/dist/pi_output.js +328 -0
  83. package/dist/pi_output.js.map +1 -0
  84. package/dist/pi_reasoning_signature.d.ts +10 -0
  85. package/dist/pi_reasoning_signature.js +33 -0
  86. package/dist/pi_reasoning_signature.js.map +1 -0
  87. package/dist/pi_runtime.d.ts +3 -0
  88. package/dist/pi_runtime.js +155 -0
  89. package/dist/pi_runtime.js.map +1 -0
  90. package/dist/pi_types.d.ts +35 -0
  91. package/dist/pi_types.js +2 -0
  92. package/dist/pi_types.js.map +1 -0
  93. package/dist/portable_reasoning_state.d.ts +7 -0
  94. package/dist/portable_reasoning_state.js +81 -0
  95. package/dist/portable_reasoning_state.js.map +1 -0
  96. package/dist/previous_decision.d.ts +13 -0
  97. package/dist/previous_decision.js +82 -0
  98. package/dist/previous_decision.js.map +1 -0
  99. package/dist/prompts.d.ts +3 -0
  100. package/dist/prompts.js +65 -0
  101. package/dist/prompts.js.map +1 -0
  102. package/dist/protocol_anthropic.d.ts +18 -0
  103. package/dist/protocol_anthropic.js +443 -0
  104. package/dist/protocol_anthropic.js.map +1 -0
  105. package/dist/protocol_openai_chat.d.ts +25 -0
  106. package/dist/protocol_openai_chat.js +503 -0
  107. package/dist/protocol_openai_chat.js.map +1 -0
  108. package/dist/protocol_validation.d.ts +23 -0
  109. package/dist/protocol_validation.js +134 -0
  110. package/dist/protocol_validation.js.map +1 -0
  111. package/dist/route.d.ts +105 -0
  112. package/dist/route.js +226 -0
  113. package/dist/route.js.map +1 -0
  114. package/dist/routing_history.d.ts +7 -0
  115. package/dist/routing_history.js +132 -0
  116. package/dist/routing_history.js.map +1 -0
  117. package/dist/selector_input.d.ts +42 -0
  118. package/dist/selector_input.js +153 -0
  119. package/dist/selector_input.js.map +1 -0
  120. package/dist/selector_parse.d.ts +13 -0
  121. package/dist/selector_parse.js +95 -0
  122. package/dist/selector_parse.js.map +1 -0
  123. package/dist/selector_request.d.ts +26 -0
  124. package/dist/selector_request.js +85 -0
  125. package/dist/selector_request.js.map +1 -0
  126. package/dist/selector_truncation.d.ts +2 -0
  127. package/dist/selector_truncation.js +48 -0
  128. package/dist/selector_truncation.js.map +1 -0
  129. package/dist/selector_wire.d.ts +10 -0
  130. package/dist/selector_wire.js +113 -0
  131. package/dist/selector_wire.js.map +1 -0
  132. package/dist/types.d.ts +152 -0
  133. package/dist/types.js +14 -0
  134. package/dist/types.js.map +1 -0
  135. package/package.json +64 -0
  136. package/src/anonymous_actions.ts +434 -0
  137. package/src/auto_router.ts +95 -0
  138. package/src/cache_behavior.ts +130 -0
  139. package/src/cache_scope.ts +58 -0
  140. package/src/cache_switch_decision.ts +53 -0
  141. package/src/candidate_pruning.ts +257 -0
  142. package/src/candidate_resolution.ts +208 -0
  143. package/src/compatibility.ts +162 -0
  144. package/src/continuation_state.ts +368 -0
  145. package/src/cost.ts +455 -0
  146. package/src/dari_policy.ts +360 -0
  147. package/src/errors.ts +27 -0
  148. package/src/fingerprint.ts +148 -0
  149. package/src/fixed_turn_cost_config.ts +24 -0
  150. package/src/framework_error.ts +38 -0
  151. package/src/framework_lifecycle.ts +74 -0
  152. package/src/framework_models.ts +186 -0
  153. package/src/framework_router.ts +490 -0
  154. package/src/framework_streaming.ts +613 -0
  155. package/src/framework_types.ts +372 -0
  156. package/src/framework_validation.ts +235 -0
  157. package/src/index.ts +37 -0
  158. package/src/json.ts +43 -0
  159. package/src/model_ids.ts +29 -0
  160. package/src/pi_context.ts +251 -0
  161. package/src/pi_options.ts +295 -0
  162. package/src/pi_output.ts +387 -0
  163. package/src/pi_reasoning_signature.ts +41 -0
  164. package/src/pi_runtime.ts +238 -0
  165. package/src/pi_types.ts +71 -0
  166. package/src/portable_reasoning_state.ts +129 -0
  167. package/src/previous_decision.ts +109 -0
  168. package/src/prompts.ts +69 -0
  169. package/src/protocol_anthropic.ts +571 -0
  170. package/src/protocol_openai_chat.ts +645 -0
  171. package/src/protocol_validation.ts +202 -0
  172. package/src/route.ts +490 -0
  173. package/src/routing_history.ts +157 -0
  174. package/src/selector_input.ts +246 -0
  175. package/src/selector_parse.ts +138 -0
  176. package/src/selector_request.ts +130 -0
  177. package/src/selector_truncation.ts +48 -0
  178. package/src/selector_wire.ts +167 -0
  179. package/src/types.ts +231 -0
@@ -0,0 +1,368 @@
1
+ import { RouterFrameworkError } from "./framework_error.js";
2
+ import { isRecord } from "./types.js";
3
+
4
+ export type ProviderIdentity = {
5
+ provider: string;
6
+ api: string;
7
+ model: string;
8
+ };
9
+
10
+ export type ProviderContinuationState =
11
+ | {
12
+ kind: "openai_reasoning";
13
+ source: ProviderIdentity;
14
+ encryptedContent: string;
15
+ providerItemId?: string;
16
+ hostedToolCallIds?: string[];
17
+ hostedToolCallOffsets?: number[];
18
+ }
19
+ | {
20
+ kind: "anthropic_thinking";
21
+ source: ProviderIdentity;
22
+ thinking: string;
23
+ signature: string;
24
+ }
25
+ | {
26
+ kind: "anthropic_redacted_thinking";
27
+ source: ProviderIdentity;
28
+ data: string;
29
+ };
30
+
31
+ /** @deprecated Alias kept for the framework-prefixed vocabulary; use {@link ProviderIdentity}. */
32
+ export type RouterProviderIdentity = ProviderIdentity;
33
+ /** @deprecated Alias kept for the framework-prefixed vocabulary; use {@link ProviderContinuationState}. */
34
+ export type RouterContinuationState = ProviderContinuationState;
35
+
36
+ const PREFIX = "dari-pcs-v1.";
37
+ const FAMILY_PREFIX = "dari-pcs-";
38
+ const MAX_JSON_BYTES = 4 * 1024 * 1024;
39
+ const MAX_ENCODED_CHARS = Math.ceil(MAX_JSON_BYTES / 3) * 4;
40
+ const MAX_PROVIDER_LENGTH = 128;
41
+ const MAX_API_LENGTH = 128;
42
+ const MAX_MODEL_LENGTH = 512;
43
+ const MAX_ITEM_ID_LENGTH = 512;
44
+ const MAX_HOSTED_TOOL_CALL_IDS = 256;
45
+ const MAX_HOSTED_TOOL_CALL_OFFSET = 1_000_000;
46
+
47
+ type EnvelopeV1 = {
48
+ v: 1;
49
+ kind: ProviderContinuationState["kind"];
50
+ source: ProviderIdentity;
51
+ state: Record<string, string>;
52
+ };
53
+
54
+ export function encodeProviderContinuationState(value: ProviderContinuationState): string {
55
+ const envelope = envelopeFor(value);
56
+ validateEnvelope(envelope, "configuration");
57
+ const json = JSON.stringify(envelope);
58
+ if (Buffer.byteLength(json, "utf8") > MAX_JSON_BYTES) {
59
+ throw invalidState(
60
+ "configuration",
61
+ "Provider continuation state exceeds the encoded size limit.",
62
+ );
63
+ }
64
+ return `${PREFIX}${Buffer.from(json, "utf8").toString("base64url")}`;
65
+ }
66
+
67
+ export function decodeUntrustedProviderContinuationState(
68
+ value: string,
69
+ ): ProviderContinuationState | null {
70
+ if (!value.startsWith(FAMILY_PREFIX)) return null;
71
+ if (!value.startsWith(PREFIX)) {
72
+ throw invalidState("invalid_request", "Provider continuation state version is not supported.");
73
+ }
74
+
75
+ const encoded = value.slice(PREFIX.length);
76
+ if (!encoded || encoded.length > MAX_ENCODED_CHARS || !/^[A-Za-z0-9_-]+$/.test(encoded)) {
77
+ throw invalidState("invalid_request", "Provider continuation state encoding is invalid.");
78
+ }
79
+
80
+ const bytes = Buffer.from(encoded, "base64url");
81
+ if (bytes.length > MAX_JSON_BYTES || bytes.toString("base64url") !== encoded) {
82
+ throw invalidState("invalid_request", "Provider continuation state encoding is invalid.");
83
+ }
84
+
85
+ let decoded: unknown;
86
+ try {
87
+ decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
88
+ } catch {
89
+ throw invalidState("invalid_request", "Provider continuation state payload is invalid.");
90
+ }
91
+ return stateFromEnvelope(validateEnvelope(decoded, "invalid_request"));
92
+ }
93
+
94
+ export function isOpenAIResponsesApi(api: string): boolean {
95
+ return api === "openai-responses" || api === "azure-openai-responses";
96
+ }
97
+
98
+ export function compatibleProviderContinuation(
99
+ continuation: ProviderContinuationState | undefined,
100
+ target: ProviderIdentity | null,
101
+ ): ProviderContinuationState | null {
102
+ if (!continuation || !target || !sameProviderIdentity(continuation.source, target)) return null;
103
+ if (continuation.kind === "openai_reasoning") {
104
+ return isOpenAIResponsesApi(target.api) ? continuation : null;
105
+ }
106
+ return target.api === "anthropic-messages" ? continuation : null;
107
+ }
108
+
109
+ export function sameProviderIdentity(a: ProviderIdentity, b: ProviderIdentity): boolean {
110
+ return a.provider === b.provider && a.api === b.api && a.model === b.model;
111
+ }
112
+
113
+ export function providerIdentityFromModel(model: {
114
+ provider?: unknown;
115
+ api?: unknown;
116
+ id?: unknown;
117
+ }): ProviderIdentity {
118
+ if (typeof model.provider !== "string" || typeof model.api !== "string" || typeof model.id !== "string") {
119
+ throw new RouterFrameworkError(
120
+ "configuration",
121
+ "Selected model metadata is missing provider identity.",
122
+ "provider_configuration_error",
123
+ );
124
+ }
125
+ return { provider: model.provider, api: model.api, model: model.id };
126
+ }
127
+
128
+ function envelopeFor(value: ProviderContinuationState): EnvelopeV1 {
129
+ if (value.kind === "openai_reasoning") {
130
+ return {
131
+ v: 1,
132
+ kind: value.kind,
133
+ source: value.source,
134
+ state: {
135
+ encrypted_content: value.encryptedContent,
136
+ ...(value.providerItemId ? { item_id: value.providerItemId } : {}),
137
+ ...(value.hostedToolCallIds?.length
138
+ ? { hosted_tool_call_ids: JSON.stringify(value.hostedToolCallIds) }
139
+ : {}),
140
+ ...(value.hostedToolCallOffsets?.length
141
+ ? { hosted_tool_call_offsets: JSON.stringify(value.hostedToolCallOffsets) }
142
+ : {}),
143
+ },
144
+ };
145
+ }
146
+ if (value.kind === "anthropic_thinking") {
147
+ return {
148
+ v: 1,
149
+ kind: value.kind,
150
+ source: value.source,
151
+ state: { thinking: value.thinking, signature: value.signature },
152
+ };
153
+ }
154
+ return { v: 1, kind: value.kind, source: value.source, state: { data: value.data } };
155
+ }
156
+
157
+ function stateFromEnvelope(envelope: EnvelopeV1): ProviderContinuationState {
158
+ if (envelope.kind === "openai_reasoning") {
159
+ return {
160
+ kind: envelope.kind,
161
+ source: envelope.source,
162
+ encryptedContent: envelope.state.encrypted_content,
163
+ ...(envelope.state.item_id ? { providerItemId: envelope.state.item_id } : {}),
164
+ ...(envelope.state.hosted_tool_call_ids
165
+ ? {
166
+ hostedToolCallIds: hostedToolCallIds(
167
+ envelope.state.hosted_tool_call_ids,
168
+ "invalid_request",
169
+ ),
170
+ ...(envelope.state.hosted_tool_call_offsets
171
+ ? {
172
+ hostedToolCallOffsets: hostedToolCallOffsets(
173
+ envelope.state.hosted_tool_call_offsets,
174
+ "invalid_request",
175
+ ),
176
+ }
177
+ : {}),
178
+ }
179
+ : {}),
180
+ };
181
+ }
182
+ if (envelope.kind === "anthropic_thinking") {
183
+ return {
184
+ kind: envelope.kind,
185
+ source: envelope.source,
186
+ thinking: envelope.state.thinking,
187
+ signature: envelope.state.signature,
188
+ };
189
+ }
190
+ return { kind: envelope.kind, source: envelope.source, data: envelope.state.data };
191
+ }
192
+
193
+ function validateEnvelope(
194
+ value: unknown,
195
+ kind: "configuration" | "invalid_request",
196
+ ): EnvelopeV1 {
197
+ if (!isRecord(value) || !hasExactKeys(value, ["v", "kind", "source", "state"]) || value.v !== 1) {
198
+ throw invalidState(kind, "Provider continuation state schema is invalid.");
199
+ }
200
+ if (
201
+ value.kind !== "openai_reasoning" &&
202
+ value.kind !== "anthropic_thinking" &&
203
+ value.kind !== "anthropic_redacted_thinking"
204
+ ) {
205
+ throw invalidState(kind, "Provider continuation state kind is invalid.");
206
+ }
207
+ return {
208
+ v: 1,
209
+ kind: value.kind,
210
+ source: validateSource(value.source, kind),
211
+ state: validateState(value.kind, value.state, kind),
212
+ };
213
+ }
214
+
215
+ function validateSource(
216
+ value: unknown,
217
+ kind: "configuration" | "invalid_request",
218
+ ): ProviderIdentity {
219
+ if (!isRecord(value) || !hasExactKeys(value, ["provider", "api", "model"])) {
220
+ throw invalidState(kind, "Provider continuation state source is invalid.");
221
+ }
222
+ return {
223
+ provider: boundedString(value.provider, MAX_PROVIDER_LENGTH, kind, "provider"),
224
+ api: boundedString(value.api, MAX_API_LENGTH, kind, "api"),
225
+ model: boundedString(value.model, MAX_MODEL_LENGTH, kind, "model"),
226
+ };
227
+ }
228
+
229
+ function validateState(
230
+ continuationKind: ProviderContinuationState["kind"],
231
+ value: unknown,
232
+ errorKind: "configuration" | "invalid_request",
233
+ ): Record<string, string> {
234
+ if (!isRecord(value)) {
235
+ throw invalidState(errorKind, "Provider continuation state payload is invalid.");
236
+ }
237
+ if (continuationKind === "openai_reasoning") {
238
+ const keys = Object.keys(value);
239
+ const keysValid = keys.includes("encrypted_content")
240
+ && keys.every((key) =>
241
+ key === "encrypted_content"
242
+ || key === "item_id"
243
+ || key === "hosted_tool_call_ids"
244
+ || key === "hosted_tool_call_offsets"
245
+ );
246
+ if (!keysValid || (value.hosted_tool_call_offsets !== undefined && value.hosted_tool_call_ids === undefined)) {
247
+ throw invalidState(errorKind, "OpenAI continuation state schema is invalid.");
248
+ }
249
+ const ids = value.hosted_tool_call_ids === undefined
250
+ ? undefined
251
+ : hostedToolCallIds(
252
+ nonEmptyString(value.hosted_tool_call_ids, errorKind, "hosted_tool_call_ids"),
253
+ errorKind,
254
+ );
255
+ const offsets = value.hosted_tool_call_offsets === undefined
256
+ ? undefined
257
+ : hostedToolCallOffsets(
258
+ nonEmptyString(value.hosted_tool_call_offsets, errorKind, "hosted_tool_call_offsets"),
259
+ errorKind,
260
+ );
261
+ if (ids && offsets && ids.length !== offsets.length) {
262
+ throw invalidState(errorKind, "OpenAI hosted tool continuation state is invalid.");
263
+ }
264
+ return {
265
+ encrypted_content: nonEmptyString(value.encrypted_content, errorKind, "encrypted_content"),
266
+ ...(value.item_id === undefined
267
+ ? {}
268
+ : { item_id: boundedString(value.item_id, MAX_ITEM_ID_LENGTH, errorKind, "item_id") }),
269
+ ...(ids ? { hosted_tool_call_ids: JSON.stringify(ids) } : {}),
270
+ ...(offsets ? { hosted_tool_call_offsets: JSON.stringify(offsets) } : {}),
271
+ };
272
+ }
273
+ if (continuationKind === "anthropic_thinking") {
274
+ if (!hasExactKeys(value, ["thinking", "signature"])) {
275
+ throw invalidState(errorKind, "Anthropic thinking state schema is invalid.");
276
+ }
277
+ if (typeof value.thinking !== "string") {
278
+ throw invalidState(errorKind, "Anthropic thinking text is invalid.");
279
+ }
280
+ return {
281
+ thinking: value.thinking,
282
+ signature: nonEmptyString(value.signature, errorKind, "signature"),
283
+ };
284
+ }
285
+ if (!hasExactKeys(value, ["data"])) {
286
+ throw invalidState(errorKind, "Anthropic redacted thinking state schema is invalid.");
287
+ }
288
+ return { data: nonEmptyString(value.data, errorKind, "data") };
289
+ }
290
+
291
+ function hostedToolCallIds(
292
+ value: string,
293
+ kind: "configuration" | "invalid_request",
294
+ ): string[] {
295
+ let parsed: unknown;
296
+ try {
297
+ parsed = JSON.parse(value);
298
+ } catch {
299
+ throw invalidState(kind, "OpenAI hosted tool continuation state is invalid.");
300
+ }
301
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > MAX_HOSTED_TOOL_CALL_IDS) {
302
+ throw invalidState(kind, "OpenAI hosted tool continuation state is invalid.");
303
+ }
304
+ return parsed.map((id) => boundedString(id, MAX_ITEM_ID_LENGTH, kind, "hosted_tool_call_id"));
305
+ }
306
+
307
+ function hostedToolCallOffsets(
308
+ value: string,
309
+ kind: "configuration" | "invalid_request",
310
+ ): number[] {
311
+ let parsed: unknown;
312
+ try {
313
+ parsed = JSON.parse(value);
314
+ } catch {
315
+ throw invalidState(kind, "OpenAI hosted tool continuation state is invalid.");
316
+ }
317
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > MAX_HOSTED_TOOL_CALL_IDS) {
318
+ throw invalidState(kind, "OpenAI hosted tool continuation state is invalid.");
319
+ }
320
+ return parsed.map((offset) => {
321
+ if (!Number.isSafeInteger(offset) || offset === 0 || Math.abs(offset) > MAX_HOSTED_TOOL_CALL_OFFSET) {
322
+ throw invalidState(kind, "OpenAI hosted tool continuation state is invalid.");
323
+ }
324
+ return offset as number;
325
+ });
326
+ }
327
+
328
+ function boundedString(
329
+ value: unknown,
330
+ maxLength: number,
331
+ kind: "configuration" | "invalid_request",
332
+ field: string,
333
+ ): string {
334
+ const text = nonEmptyString(value, kind, field);
335
+ if (text.length > maxLength) {
336
+ throw invalidState(kind, `Provider continuation state ${field} is too long.`);
337
+ }
338
+ return text;
339
+ }
340
+
341
+ function nonEmptyString(
342
+ value: unknown,
343
+ kind: "configuration" | "invalid_request",
344
+ field: string,
345
+ ): string {
346
+ if (typeof value !== "string" || value.length === 0) {
347
+ throw invalidState(kind, `Provider continuation state ${field} must be a non-empty string.`);
348
+ }
349
+ return value;
350
+ }
351
+
352
+ function hasExactKeys(value: Record<string, unknown>, expected: string[]): boolean {
353
+ const keys = Object.keys(value).sort();
354
+ const expectedKeys = [...expected].sort();
355
+ return keys.length === expectedKeys.length && keys.every((key, index) => key === expectedKeys[index]);
356
+ }
357
+
358
+ function invalidState(
359
+ kind: "configuration" | "invalid_request",
360
+ message: string,
361
+ ): RouterFrameworkError {
362
+ return new RouterFrameworkError(
363
+ kind,
364
+ message,
365
+ "invalid_provider_continuation_state",
366
+ "input.reasoning.encrypted_content",
367
+ );
368
+ }