@mupt-ai/dari-router 0.1.0 → 0.2.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.
Files changed (118) hide show
  1. package/README.md +89 -60
  2. package/dist/anonymous_actions.js +32 -10
  3. package/dist/anonymous_actions.js.map +1 -1
  4. package/dist/auto_router.d.ts +4 -1
  5. package/dist/auto_router.js +3 -3
  6. package/dist/auto_router.js.map +1 -1
  7. package/dist/cache_behavior.d.ts +8 -6
  8. package/dist/cache_behavior.js +60 -30
  9. package/dist/cache_behavior.js.map +1 -1
  10. package/dist/cache_scope.d.ts +2 -2
  11. package/dist/cache_scope.js +23 -18
  12. package/dist/cache_scope.js.map +1 -1
  13. package/dist/cost.d.ts +3 -2
  14. package/dist/cost.js +32 -23
  15. package/dist/cost.js.map +1 -1
  16. package/dist/dari_policy.d.ts +8 -2
  17. package/dist/dari_policy.js +39 -1
  18. package/dist/dari_policy.js.map +1 -1
  19. package/dist/eval_score_imputation.d.ts +26 -0
  20. package/dist/eval_score_imputation.js +121 -0
  21. package/dist/eval_score_imputation.js.map +1 -0
  22. package/dist/framework_lifecycle.d.ts +1 -1
  23. package/dist/framework_lifecycle.js +4 -1
  24. package/dist/framework_lifecycle.js.map +1 -1
  25. package/dist/framework_models.js +6 -0
  26. package/dist/framework_models.js.map +1 -1
  27. package/dist/framework_router.js +22 -11
  28. package/dist/framework_router.js.map +1 -1
  29. package/dist/framework_types.d.ts +9 -5
  30. package/dist/index.d.ts +9 -35
  31. package/dist/index.js +6 -37
  32. package/dist/index.js.map +1 -1
  33. package/dist/model_ids.js +4 -0
  34. package/dist/model_ids.js.map +1 -1
  35. package/dist/pi_runtime.js +19 -11
  36. package/dist/pi_runtime.js.map +1 -1
  37. package/dist/pi_types.d.ts +2 -2
  38. package/{src/index.ts → dist/policy-engine.d.ts} +2 -14
  39. package/dist/policy-engine.js +25 -0
  40. package/dist/policy-engine.js.map +1 -0
  41. package/dist/prompts.d.ts +1 -1
  42. package/dist/prompts.js +3 -3
  43. package/dist/prompts.js.map +1 -1
  44. package/dist/protocols.d.ts +5 -0
  45. package/dist/protocols.js +6 -0
  46. package/dist/protocols.js.map +1 -0
  47. package/dist/route.d.ts +3 -4
  48. package/dist/route.js +17 -10
  49. package/dist/route.js.map +1 -1
  50. package/dist/selector_input.d.ts +1 -0
  51. package/dist/selector_input.js +66 -78
  52. package/dist/selector_input.js.map +1 -1
  53. package/dist/selector_parse.d.ts +1 -0
  54. package/dist/selector_parse.js +2 -1
  55. package/dist/selector_parse.js.map +1 -1
  56. package/dist/selector_request.d.ts +1 -0
  57. package/dist/selector_request.js +1 -0
  58. package/dist/selector_request.js.map +1 -1
  59. package/dist/types.d.ts +1 -1
  60. package/examples/auto_router.ts +40 -0
  61. package/examples/basic_route.ts +130 -0
  62. package/examples/create_router.ts +114 -0
  63. package/examples/dari_policy.ts +86 -0
  64. package/examples/lease_stores.ts +159 -0
  65. package/examples/managed/README.md +50 -0
  66. package/examples/managed/byok-router/README.md +13 -0
  67. package/examples/managed/byok-router/router.yml +13 -0
  68. package/examples/managed/custom-rules-router/README.md +12 -0
  69. package/examples/managed/custom-rules-router/router.yml +24 -0
  70. package/examples/managed/eval-router/README.md +18 -0
  71. package/examples/managed/eval-router/router.yml +11 -0
  72. package/examples/managed/slm-router/README.md +12 -0
  73. package/examples/managed/slm-router/router.yml +8 -0
  74. package/examples/pi_runtime.ts +55 -0
  75. package/package.json +26 -7
  76. package/src/anonymous_actions.ts +0 -434
  77. package/src/auto_router.ts +0 -95
  78. package/src/cache_behavior.ts +0 -130
  79. package/src/cache_scope.ts +0 -58
  80. package/src/cache_switch_decision.ts +0 -53
  81. package/src/candidate_pruning.ts +0 -257
  82. package/src/candidate_resolution.ts +0 -208
  83. package/src/compatibility.ts +0 -162
  84. package/src/continuation_state.ts +0 -368
  85. package/src/cost.ts +0 -455
  86. package/src/dari_policy.ts +0 -360
  87. package/src/errors.ts +0 -27
  88. package/src/fingerprint.ts +0 -148
  89. package/src/fixed_turn_cost_config.ts +0 -24
  90. package/src/framework_error.ts +0 -38
  91. package/src/framework_lifecycle.ts +0 -74
  92. package/src/framework_models.ts +0 -186
  93. package/src/framework_router.ts +0 -490
  94. package/src/framework_streaming.ts +0 -613
  95. package/src/framework_types.ts +0 -372
  96. package/src/framework_validation.ts +0 -235
  97. package/src/json.ts +0 -43
  98. package/src/model_ids.ts +0 -29
  99. package/src/pi_context.ts +0 -251
  100. package/src/pi_options.ts +0 -295
  101. package/src/pi_output.ts +0 -387
  102. package/src/pi_reasoning_signature.ts +0 -41
  103. package/src/pi_runtime.ts +0 -238
  104. package/src/pi_types.ts +0 -71
  105. package/src/portable_reasoning_state.ts +0 -129
  106. package/src/previous_decision.ts +0 -109
  107. package/src/prompts.ts +0 -69
  108. package/src/protocol_anthropic.ts +0 -571
  109. package/src/protocol_openai_chat.ts +0 -645
  110. package/src/protocol_validation.ts +0 -202
  111. package/src/route.ts +0 -490
  112. package/src/routing_history.ts +0 -157
  113. package/src/selector_input.ts +0 -246
  114. package/src/selector_parse.ts +0 -138
  115. package/src/selector_request.ts +0 -130
  116. package/src/selector_truncation.ts +0 -48
  117. package/src/selector_wire.ts +0 -167
  118. package/src/types.ts +0 -231
@@ -1,162 +0,0 @@
1
- import { RouterCoreError } from "./errors.js";
2
- import {
3
- isReasoningEffort,
4
- type HardCapability,
5
- type ReasoningEffort,
6
- type RoutingCandidate,
7
- } from "./types.js";
8
-
9
- // Host-supplied capability facts for one candidate model. Hosts derive this
10
- // from their model registry/catalog; the core only applies the eligibility
11
- // rules and never inspects provider metadata records directly.
12
- export type CandidateModelMetadata = {
13
- // Provider identifier, e.g. "meta" or "anthropic".
14
- provider: string;
15
- // Provider API identifier, e.g. "anthropic-messages" or "openai-responses".
16
- api: string;
17
- supportsImageInput: boolean;
18
- supportsHostedWebSearch: boolean;
19
- // Whether the provider API can serialize response_format for structured
20
- // output (JSON schema) requests.
21
- supportsStructuredOutput: boolean;
22
- supportedThinkingLevels: string[];
23
- };
24
-
25
- export type CandidateMetadataLookup = (modelId: string) => CandidateModelMetadata;
26
-
27
- export type CompatibleCandidates = {
28
- candidates: RoutingCandidate[];
29
- };
30
-
31
- export function resolveCompatibleCandidates(args: {
32
- requiredCapabilities: readonly HardCapability[];
33
- // The client's explicit reasoning_effort request, when present. Pins every
34
- // candidate to that level.
35
- requestedReasoningEffort?: ReasoningEffort;
36
- // Some protocols carry an independent hard thinking-enabled constraint.
37
- thinkingEnabled?: boolean;
38
- // The request's tool_choice value in OpenAI wire shape ("none" | "auto" |
39
- // "required" | "any" | {..forced tool..}). Only forced-tool detection is
40
- // applied here.
41
- toolChoice?: unknown;
42
- candidateModels: string[];
43
- metadataLookup: CandidateMetadataLookup;
44
- modelThinkingLevels?: Readonly<Record<string, ReasoningEffort[]>>;
45
- }): CompatibleCandidates {
46
- const compatible: RoutingCandidate[] = [];
47
- let anthropicToolChoiceRejected = false;
48
- let metaToolChoiceRejected = false;
49
- let candidates = args.candidateModels.map((model) => ({
50
- model,
51
- metadata: args.metadataLookup(model),
52
- }));
53
-
54
- if (args.requiredCapabilities.includes("image_input")) {
55
- candidates = candidates.filter(({ metadata }) => metadata.supportsImageInput);
56
- if (candidates.length === 0) {
57
- throw new RouterCoreError(
58
- "invalid_request",
59
- "Image input is not supported by any enabled model for this router.",
60
- "unsupported_input_modality",
61
- );
62
- }
63
- }
64
-
65
- if (args.requiredCapabilities.includes("structured_output")) {
66
- candidates = candidates.filter(({ metadata }) => metadata.supportsStructuredOutput);
67
- if (candidates.length === 0) {
68
- throw new RouterCoreError(
69
- "invalid_request",
70
- "Structured output is not supported by any enabled model for this router.",
71
- "unsupported_response_format",
72
- "response_format",
73
- );
74
- }
75
- }
76
-
77
- if (args.requiredCapabilities.includes("openai_hosted_web_search")) {
78
- candidates = candidates.filter(({ metadata }) => metadata.supportsHostedWebSearch);
79
- if (candidates.length === 0) {
80
- throw new RouterCoreError(
81
- "invalid_request",
82
- "Hosted web search requires an enabled OpenAI or Azure Responses model for this router.",
83
- "unsupported_tool",
84
- "tools",
85
- );
86
- }
87
- }
88
-
89
- const requested = args.requestedReasoningEffort;
90
- for (const { model, metadata } of candidates) {
91
- if (
92
- metadata.provider === "meta" &&
93
- args.toolChoice !== undefined &&
94
- args.toolChoice !== "auto"
95
- ) {
96
- metaToolChoiceRejected = true;
97
- continue;
98
- }
99
- const supportedLevels = metadata.supportedThinkingLevels.filter(
100
- isReasoningEffort,
101
- );
102
- const configuredLevels = args.modelThinkingLevels?.[model] ?? supportedLevels;
103
- const enabledLevels = configuredLevels.filter(
104
- (level) => isReasoningEffort(level) && supportedLevels.includes(level),
105
- );
106
- const levels = requested === undefined
107
- ? args.thinkingEnabled
108
- ? enabledLevels.filter((level) => level !== "off")
109
- : enabledLevels
110
- : enabledLevels.includes(requested)
111
- ? [requested]
112
- : [];
113
- for (const reasoningEffort of levels) {
114
- if (
115
- reasoningEffort !== "off" &&
116
- metadata.api === "anthropic-messages" &&
117
- forcesToolUse(args.toolChoice)
118
- ) {
119
- anthropicToolChoiceRejected = true;
120
- continue;
121
- }
122
- compatible.push({ model, reasoningEffort });
123
- }
124
- }
125
-
126
- if (compatible.length > 0) return { candidates: compatible };
127
-
128
- if (anthropicToolChoiceRejected) {
129
- throw new RouterCoreError(
130
- "invalid_request",
131
- "Forced tool choice is not supported by any enabled model at the requested reasoning setting.",
132
- "unsupported_tool_choice",
133
- "tool_choice",
134
- );
135
- }
136
-
137
- if (metaToolChoiceRejected) {
138
- throw new RouterCoreError(
139
- "invalid_request",
140
- "Meta models currently support only omitted or 'auto' tool_choice.",
141
- "unsupported_tool_choice",
142
- "tool_choice",
143
- );
144
- }
145
-
146
- throw new RouterCoreError(
147
- "invalid_request",
148
- requested === undefined
149
- ? args.thinkingEnabled
150
- ? "Thinking is enabled, but no enabled non-off model/thinking-level pair is compatible with this request."
151
- : "No enabled model/thinking-level pair is compatible with this request."
152
- : `reasoning_effort '${requested}' is not supported by any enabled model for this router.`,
153
- "unsupported_reasoning_effort",
154
- requested === undefined ? undefined : "reasoning_effort",
155
- );
156
- }
157
-
158
- function forcesToolUse(choice: unknown): boolean {
159
- return choice === "required" ||
160
- choice === "any" ||
161
- (typeof choice === "object" && choice !== null);
162
- }
@@ -1,368 +0,0 @@
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
- }