@openrouter/sdk 0.3.11 → 0.3.14

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 (70) hide show
  1. package/esm/index.d.ts +1 -0
  2. package/esm/index.js +2 -0
  3. package/esm/lib/anthropic-compat.test.js +3 -0
  4. package/esm/lib/chat-compat.test.js +3 -0
  5. package/esm/lib/config.d.ts +2 -2
  6. package/esm/lib/config.js +2 -2
  7. package/esm/lib/model-result.d.ts +8 -3
  8. package/esm/lib/model-result.js +57 -35
  9. package/esm/lib/tool-event-broadcaster.d.ts +44 -0
  10. package/esm/lib/tool-event-broadcaster.js +146 -0
  11. package/esm/lib/tool-executor.d.ts +21 -6
  12. package/esm/lib/tool-executor.js +57 -8
  13. package/esm/lib/tool-types.d.ts +18 -18
  14. package/esm/lib/tool.d.ts +16 -16
  15. package/esm/models/chatgenerationparams.d.ts +93 -12
  16. package/esm/models/chatgenerationparams.js +75 -6
  17. package/esm/models/chatgenerationtokenusage.d.ts +1 -0
  18. package/esm/models/chatgenerationtokenusage.js +2 -0
  19. package/esm/models/chatmessagetokenlogprob.d.ts +4 -4
  20. package/esm/models/chatmessagetokenlogprob.js +4 -5
  21. package/esm/models/index.d.ts +7 -0
  22. package/esm/models/index.js +7 -0
  23. package/esm/models/openairesponsesinputunion.d.ts +15 -5
  24. package/esm/models/openairesponsesinputunion.js +5 -5
  25. package/esm/models/openresponseseasyinputmessage.d.ts +41 -16
  26. package/esm/models/openresponseseasyinputmessage.js +38 -13
  27. package/esm/models/openresponsesinputmessageitem.d.ts +37 -12
  28. package/esm/models/openresponsesinputmessageitem.js +33 -9
  29. package/esm/models/openresponsesnonstreamingresponse.d.ts +5 -2
  30. package/esm/models/openresponsesnonstreamingresponse.js +8 -2
  31. package/esm/models/openresponsesreasoning.d.ts +1 -0
  32. package/esm/models/openresponsesreasoning.js +1 -0
  33. package/esm/models/openresponsesrequest.d.ts +61 -24
  34. package/esm/models/openresponsesrequest.js +39 -6
  35. package/esm/models/operations/getgeneration.d.ts +4 -0
  36. package/esm/models/operations/getgeneration.js +1 -0
  37. package/esm/models/percentilelatencycutoffs.d.ts +33 -0
  38. package/esm/models/percentilelatencycutoffs.js +16 -0
  39. package/esm/models/percentilestats.d.ts +28 -0
  40. package/esm/models/percentilestats.js +17 -0
  41. package/esm/models/percentilethroughputcutoffs.d.ts +33 -0
  42. package/esm/models/percentilethroughputcutoffs.js +16 -0
  43. package/esm/models/preferredmaxlatency.d.ts +12 -0
  44. package/esm/models/preferredmaxlatency.js +12 -0
  45. package/esm/models/preferredminthroughput.d.ts +12 -0
  46. package/esm/models/preferredminthroughput.js +12 -0
  47. package/esm/models/providername.d.ts +3 -2
  48. package/esm/models/providername.js +3 -2
  49. package/esm/models/providerpreferences.d.ts +8 -20
  50. package/esm/models/providerpreferences.js +6 -6
  51. package/esm/models/publicendpoint.d.ts +6 -0
  52. package/esm/models/publicendpoint.js +5 -0
  53. package/esm/models/responseinputimage.d.ts +11 -3
  54. package/esm/models/responseinputimage.js +9 -2
  55. package/esm/models/responseinputvideo.d.ts +20 -0
  56. package/esm/models/responseinputvideo.js +19 -0
  57. package/esm/models/responseoutputtext.d.ts +38 -0
  58. package/esm/models/responseoutputtext.js +50 -0
  59. package/esm/models/responsesoutputitemreasoning.d.ts +30 -1
  60. package/esm/models/responsesoutputitemreasoning.js +22 -0
  61. package/esm/models/responsesoutputmodality.d.ts +10 -0
  62. package/esm/models/responsesoutputmodality.js +12 -0
  63. package/esm/models/schema0.d.ts +3 -2
  64. package/esm/models/schema0.js +3 -2
  65. package/esm/models/schema3.d.ts +1 -0
  66. package/esm/models/schema3.js +1 -0
  67. package/jsr.json +1 -1
  68. package/package.json +6 -7
  69. package/vitest.config.ts +25 -16
  70. package/.zed/settings.json +0 -10
package/esm/index.d.ts CHANGED
@@ -17,5 +17,6 @@ export { extractUnsupportedContent, getUnsupportedContentSummary, hasUnsupported
17
17
  export { tool } from './lib/tool.js';
18
18
  export { hasExecuteFunction, isGeneratorTool, isRegularExecuteTool, isToolPreliminaryResultEvent, ToolType, } from './lib/tool-types.js';
19
19
  export { buildTurnContext, normalizeInputToArray } from './lib/turn-context.js';
20
+ export { ToolEventBroadcaster } from './lib/tool-event-broadcaster.js';
20
21
  export * from './sdk/sdk.js';
21
22
  //# sourceMappingURL=index.d.ts.map
package/esm/index.js CHANGED
@@ -21,5 +21,7 @@ export { tool } from './lib/tool.js';
21
21
  export { hasExecuteFunction, isGeneratorTool, isRegularExecuteTool, isToolPreliminaryResultEvent, ToolType, } from './lib/tool-types.js';
22
22
  // Turn context helpers
23
23
  export { buildTurnContext, normalizeInputToArray } from './lib/turn-context.js';
24
+ // Real-time tool event broadcasting
25
+ export { ToolEventBroadcaster } from './lib/tool-event-broadcaster.js';
24
26
  export * from './sdk/sdk.js';
25
27
  //# sourceMappingURL=index.js.map
@@ -9,12 +9,15 @@ function createMockResponse(overrides) {
9
9
  id: "resp_test",
10
10
  object: "response",
11
11
  createdAt: Date.now(),
12
+ completedAt: Date.now(),
12
13
  model: "openai/gpt-4",
13
14
  status: "completed",
14
15
  error: null,
15
16
  incompleteDetails: null,
16
17
  temperature: null,
17
18
  topP: null,
19
+ presencePenalty: null,
20
+ frequencyPenalty: null,
18
21
  metadata: null,
19
22
  tools: [],
20
23
  toolChoice: "auto",
@@ -9,12 +9,15 @@ function createMockResponse(overrides) {
9
9
  id: "resp_test",
10
10
  object: "response",
11
11
  createdAt: Date.now(),
12
+ completedAt: Date.now(),
12
13
  model: "openai/gpt-4",
13
14
  status: "completed",
14
15
  error: null,
15
16
  incompleteDetails: null,
16
17
  temperature: null,
17
18
  topP: null,
19
+ presencePenalty: null,
20
+ frequencyPenalty: null,
18
21
  metadata: null,
19
22
  tools: [],
20
23
  toolChoice: "auto",
@@ -47,8 +47,8 @@ export declare function serverURLFromOptions(options: SDKOptions): URL | null;
47
47
  export declare const SDK_METADATA: {
48
48
  readonly language: "typescript";
49
49
  readonly openapiDocVersion: "1.0.0";
50
- readonly sdkVersion: "0.3.11";
50
+ readonly sdkVersion: "0.3.14";
51
51
  readonly genVersion: "2.788.4";
52
- readonly userAgent: "speakeasy-sdk/typescript 0.3.11 2.788.4 1.0.0 @openrouter/sdk";
52
+ readonly userAgent: "speakeasy-sdk/typescript 0.3.14 2.788.4 1.0.0 @openrouter/sdk";
53
53
  };
54
54
  //# sourceMappingURL=config.d.ts.map
package/esm/lib/config.js CHANGED
@@ -26,8 +26,8 @@ export function serverURLFromOptions(options) {
26
26
  export const SDK_METADATA = {
27
27
  language: "typescript",
28
28
  openapiDocVersion: "1.0.0",
29
- sdkVersion: "0.3.11",
29
+ sdkVersion: "0.3.14",
30
30
  genVersion: "2.788.4",
31
- userAgent: "speakeasy-sdk/typescript 0.3.11 2.788.4 1.0.0 @openrouter/sdk",
31
+ userAgent: "speakeasy-sdk/typescript 0.3.14 2.788.4 1.0.0 @openrouter/sdk",
32
32
  };
33
33
  //# sourceMappingURL=config.js.map
@@ -37,10 +37,15 @@ export declare class ModelResult<TTools extends readonly Tool[]> {
37
37
  private initPromise;
38
38
  private toolExecutionPromise;
39
39
  private finalResponse;
40
- private preliminaryResults;
40
+ private toolEventBroadcaster;
41
41
  private allToolExecutionRounds;
42
42
  private resolvedRequest;
43
43
  constructor(options: GetResponseOptions<TTools>);
44
+ /**
45
+ * Get or create the tool event broadcaster (lazy initialization).
46
+ * Ensures only one broadcaster exists for the lifetime of this ModelResult.
47
+ */
48
+ private ensureBroadcaster;
44
49
  /**
45
50
  * Type guard to check if a value is a non-streaming response
46
51
  */
@@ -73,7 +78,7 @@ export declare class ModelResult<TTools extends readonly Tool[]> {
73
78
  /**
74
79
  * Stream all response events as they arrive.
75
80
  * Multiple consumers can iterate over this stream concurrently.
76
- * Includes preliminary tool result events after tool execution.
81
+ * Preliminary tool results are streamed in REAL-TIME as generator tools yield.
77
82
  */
78
83
  getFullResponsesStream(): AsyncIterableIterator<ResponseStreamEvent<InferToolEventsUnion<TTools>>>;
79
84
  /**
@@ -95,7 +100,7 @@ export declare class ModelResult<TTools extends readonly Tool[]> {
95
100
  getReasoningStream(): AsyncIterableIterator<string>;
96
101
  /**
97
102
  * Stream tool call argument deltas and preliminary results.
98
- * This filters the full event stream to yield:
103
+ * Preliminary results are streamed in REAL-TIME as generator tools yield.
99
104
  * - Tool call argument deltas as { type: "delta", content: string }
100
105
  * - Preliminary results as { type: "preliminary_result", toolCallId, result }
101
106
  */
@@ -1,3 +1,4 @@
1
+ import { ToolEventBroadcaster } from './tool-event-broadcaster.js';
1
2
  import { betaResponsesSend } from '../funcs/betaResponsesSend.js';
2
3
  import { hasAsyncFunctions, resolveAsyncFunctions, } from './async-params.js';
3
4
  import { ReusableReadableStream } from './reusable-stream.js';
@@ -51,12 +52,22 @@ export class ModelResult {
51
52
  this.initPromise = null;
52
53
  this.toolExecutionPromise = null;
53
54
  this.finalResponse = null;
54
- this.preliminaryResults = new Map();
55
+ this.toolEventBroadcaster = null;
55
56
  this.allToolExecutionRounds = [];
56
57
  // Track resolved request after async function resolution
57
58
  this.resolvedRequest = null;
58
59
  this.options = options;
59
60
  }
61
+ /**
62
+ * Get or create the tool event broadcaster (lazy initialization).
63
+ * Ensures only one broadcaster exists for the lifetime of this ModelResult.
64
+ */
65
+ ensureBroadcaster() {
66
+ if (!this.toolEventBroadcaster) {
67
+ this.toolEventBroadcaster = new ToolEventBroadcaster();
68
+ }
69
+ return this.toolEventBroadcaster;
70
+ }
60
71
  /**
61
72
  * Type guard to check if a value is a non-streaming response
62
73
  */
@@ -91,8 +102,7 @@ export class ModelResult {
91
102
  // Already resolved, extract non-function fields
92
103
  // Since request is CallModelInput, we need to filter out stopWhen
93
104
  // Note: tools are already in API format at this point (converted in callModel())
94
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
95
- const { stopWhen, ...rest } = this.options.request;
105
+ const { stopWhen: _, ...rest } = this.options.request;
96
106
  // Cast to ResolvedCallModelInput - we know it's resolved if hasAsyncFunctions returned false
97
107
  baseRequest = rest;
98
108
  }
@@ -215,11 +225,17 @@ export class ModelResult {
215
225
  if (!tool || !hasExecuteFunction(tool)) {
216
226
  continue;
217
227
  }
218
- const result = await executeTool(tool, toolCall, turnContext);
219
- // Store preliminary results
220
- if (result.preliminaryResults && result.preliminaryResults.length > 0) {
221
- this.preliminaryResults.set(toolCall.id, result.preliminaryResults);
222
- }
228
+ // Create callback for real-time preliminary results
229
+ const onPreliminaryResult = this.toolEventBroadcaster
230
+ ? (callId, resultValue) => {
231
+ this.toolEventBroadcaster?.push({
232
+ type: 'preliminary_result',
233
+ toolCallId: callId,
234
+ result: resultValue,
235
+ });
236
+ }
237
+ : undefined;
238
+ const result = await executeTool(tool, toolCall, turnContext, onPreliminaryResult);
223
239
  toolResults.push({
224
240
  type: 'function_call_output',
225
241
  id: `output_${toolCall.id}`,
@@ -335,7 +351,7 @@ export class ModelResult {
335
351
  /**
336
352
  * Stream all response events as they arrive.
337
353
  * Multiple consumers can iterate over this stream concurrently.
338
- * Includes preliminary tool result events after tool execution.
354
+ * Preliminary tool results are streamed in REAL-TIME as generator tools yield.
339
355
  */
340
356
  getFullResponsesStream() {
341
357
  return async function* () {
@@ -343,24 +359,29 @@ export class ModelResult {
343
359
  if (!this.reusableStream) {
344
360
  throw new Error('Stream not initialized');
345
361
  }
362
+ // Get or create broadcaster for real-time tool events (lazy init prevents race conditions)
363
+ const broadcaster = this.ensureBroadcaster();
364
+ const toolEventConsumer = broadcaster.createConsumer();
365
+ // Start tool execution in background (completes broadcaster when done)
366
+ const executionPromise = this.executeToolsIfNeeded().finally(() => {
367
+ broadcaster.complete();
368
+ });
346
369
  const consumer = this.reusableStream.createConsumer();
347
- // Yield original events directly
370
+ // Yield original API events
348
371
  for await (const event of consumer) {
349
372
  yield event;
350
373
  }
351
- // After stream completes, check if tools were executed and emit preliminary results
352
- await this.executeToolsIfNeeded();
353
- // Emit all preliminary results as new event types
354
- for (const [toolCallId, results] of this.preliminaryResults) {
355
- for (const result of results) {
356
- yield {
357
- type: 'tool.preliminary_result',
358
- toolCallId,
359
- result: result,
360
- timestamp: Date.now(),
361
- };
362
- }
374
+ // Yield tool preliminary results as they arrive (real-time!)
375
+ for await (const event of toolEventConsumer) {
376
+ yield {
377
+ type: 'tool.preliminary_result',
378
+ toolCallId: event.toolCallId,
379
+ result: event.result,
380
+ timestamp: Date.now(),
381
+ };
363
382
  }
383
+ // Ensure execution completed (handles errors)
384
+ await executionPromise;
364
385
  }.call(this);
365
386
  }
366
387
  /**
@@ -423,7 +444,7 @@ export class ModelResult {
423
444
  }
424
445
  /**
425
446
  * Stream tool call argument deltas and preliminary results.
426
- * This filters the full event stream to yield:
447
+ * Preliminary results are streamed in REAL-TIME as generator tools yield.
427
448
  * - Tool call argument deltas as { type: "delta", content: string }
428
449
  * - Preliminary results as { type: "preliminary_result", toolCallId, result }
429
450
  */
@@ -433,25 +454,26 @@ export class ModelResult {
433
454
  if (!this.reusableStream) {
434
455
  throw new Error('Stream not initialized');
435
456
  }
436
- // Yield tool deltas as structured events
457
+ // Get or create broadcaster for real-time tool events (lazy init prevents race conditions)
458
+ const broadcaster = this.ensureBroadcaster();
459
+ const toolEventConsumer = broadcaster.createConsumer();
460
+ // Start tool execution in background (completes broadcaster when done)
461
+ const executionPromise = this.executeToolsIfNeeded().finally(() => {
462
+ broadcaster.complete();
463
+ });
464
+ // Yield tool deltas from API stream
437
465
  for await (const delta of extractToolDeltas(this.reusableStream)) {
438
466
  yield {
439
467
  type: 'delta',
440
468
  content: delta,
441
469
  };
442
470
  }
443
- // After stream completes, check if tools were executed and emit preliminary results
444
- await this.executeToolsIfNeeded();
445
- // Emit all preliminary results
446
- for (const [toolCallId, results] of this.preliminaryResults) {
447
- for (const result of results) {
448
- yield {
449
- type: 'preliminary_result',
450
- toolCallId,
451
- result: result,
452
- };
453
- }
471
+ // Yield tool events as they arrive (real-time!)
472
+ for await (const event of toolEventConsumer) {
473
+ yield event;
454
474
  }
475
+ // Ensure execution completed (handles errors)
476
+ await executionPromise;
455
477
  }.call(this);
456
478
  }
457
479
  /**
@@ -0,0 +1,44 @@
1
+ /**
2
+ * A push-based event broadcaster that supports multiple concurrent consumers.
3
+ * Similar to ReusableReadableStream but for push-based events from tool execution.
4
+ *
5
+ * Each consumer gets their own position in the buffer and receives all events
6
+ * from their join point onward. This enables real-time streaming of generator
7
+ * tool preliminary results to multiple consumers simultaneously.
8
+ *
9
+ * @template T - The event type being broadcast
10
+ */
11
+ export declare class ToolEventBroadcaster<T> {
12
+ private buffer;
13
+ private consumers;
14
+ private nextConsumerId;
15
+ private isComplete;
16
+ private completionError;
17
+ /**
18
+ * Push a new event to all consumers.
19
+ * Events are buffered so late-joining consumers can catch up.
20
+ */
21
+ push(event: T): void;
22
+ /**
23
+ * Mark the broadcaster as complete - no more events will be pushed.
24
+ * Optionally pass an error to signal failure to all consumers.
25
+ * Cleans up buffer and consumers after completion.
26
+ */
27
+ complete(error?: Error): void;
28
+ /**
29
+ * Clean up resources after all consumers have finished.
30
+ * Called automatically after complete(), but can be called manually.
31
+ */
32
+ private cleanup;
33
+ /**
34
+ * Create a new consumer that can independently iterate over events.
35
+ * Consumers can join at any time and will receive events from position 0.
36
+ * Multiple consumers can be created and will all receive the same events.
37
+ */
38
+ createConsumer(): AsyncIterableIterator<T>;
39
+ /**
40
+ * Notify all waiting consumers that new data is available or stream completed
41
+ */
42
+ private notifyWaitingConsumers;
43
+ }
44
+ //# sourceMappingURL=tool-event-broadcaster.d.ts.map
@@ -0,0 +1,146 @@
1
+ /**
2
+ * A push-based event broadcaster that supports multiple concurrent consumers.
3
+ * Similar to ReusableReadableStream but for push-based events from tool execution.
4
+ *
5
+ * Each consumer gets their own position in the buffer and receives all events
6
+ * from their join point onward. This enables real-time streaming of generator
7
+ * tool preliminary results to multiple consumers simultaneously.
8
+ *
9
+ * @template T - The event type being broadcast
10
+ */
11
+ export class ToolEventBroadcaster {
12
+ constructor() {
13
+ this.buffer = [];
14
+ this.consumers = new Map();
15
+ this.nextConsumerId = 0;
16
+ this.isComplete = false;
17
+ this.completionError = null;
18
+ }
19
+ /**
20
+ * Push a new event to all consumers.
21
+ * Events are buffered so late-joining consumers can catch up.
22
+ */
23
+ push(event) {
24
+ if (this.isComplete) {
25
+ return;
26
+ }
27
+ this.buffer.push(event);
28
+ this.notifyWaitingConsumers();
29
+ }
30
+ /**
31
+ * Mark the broadcaster as complete - no more events will be pushed.
32
+ * Optionally pass an error to signal failure to all consumers.
33
+ * Cleans up buffer and consumers after completion.
34
+ */
35
+ complete(error) {
36
+ this.isComplete = true;
37
+ this.completionError = error ?? null;
38
+ this.notifyWaitingConsumers();
39
+ // Schedule cleanup after consumers have processed completion
40
+ queueMicrotask(() => this.cleanup());
41
+ }
42
+ /**
43
+ * Clean up resources after all consumers have finished.
44
+ * Called automatically after complete(), but can be called manually.
45
+ */
46
+ cleanup() {
47
+ // Only cleanup if complete and all consumers are done
48
+ if (this.isComplete && this.consumers.size === 0) {
49
+ this.buffer = [];
50
+ }
51
+ }
52
+ /**
53
+ * Create a new consumer that can independently iterate over events.
54
+ * Consumers can join at any time and will receive events from position 0.
55
+ * Multiple consumers can be created and will all receive the same events.
56
+ */
57
+ createConsumer() {
58
+ const consumerId = this.nextConsumerId++;
59
+ const state = {
60
+ position: 0,
61
+ waitingPromise: null,
62
+ cancelled: false,
63
+ };
64
+ this.consumers.set(consumerId, state);
65
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
66
+ const self = this;
67
+ return {
68
+ async next() {
69
+ const consumer = self.consumers.get(consumerId);
70
+ if (!consumer) {
71
+ return { done: true, value: undefined };
72
+ }
73
+ if (consumer.cancelled) {
74
+ return { done: true, value: undefined };
75
+ }
76
+ // Return buffered event if available
77
+ if (consumer.position < self.buffer.length) {
78
+ const value = self.buffer[consumer.position];
79
+ consumer.position++;
80
+ return { done: false, value };
81
+ }
82
+ // If complete and caught up, we're done
83
+ if (self.isComplete) {
84
+ self.consumers.delete(consumerId);
85
+ self.cleanup();
86
+ if (self.completionError) {
87
+ throw self.completionError;
88
+ }
89
+ return { done: true, value: undefined };
90
+ }
91
+ // Set up waiting promise FIRST to avoid race condition
92
+ const waitPromise = new Promise((resolve, reject) => {
93
+ consumer.waitingPromise = { resolve, reject };
94
+ // Immediately check if we should resolve after setting up promise
95
+ if (self.isComplete ||
96
+ self.completionError ||
97
+ consumer.position < self.buffer.length) {
98
+ resolve();
99
+ }
100
+ });
101
+ await waitPromise;
102
+ consumer.waitingPromise = null;
103
+ // Recursively try again after waking up
104
+ return this.next();
105
+ },
106
+ async return() {
107
+ const consumer = self.consumers.get(consumerId);
108
+ if (consumer) {
109
+ consumer.cancelled = true;
110
+ self.consumers.delete(consumerId);
111
+ self.cleanup();
112
+ }
113
+ return { done: true, value: undefined };
114
+ },
115
+ async throw(e) {
116
+ const consumer = self.consumers.get(consumerId);
117
+ if (consumer) {
118
+ consumer.cancelled = true;
119
+ self.consumers.delete(consumerId);
120
+ self.cleanup();
121
+ }
122
+ throw e;
123
+ },
124
+ [Symbol.asyncIterator]() {
125
+ return this;
126
+ },
127
+ };
128
+ }
129
+ /**
130
+ * Notify all waiting consumers that new data is available or stream completed
131
+ */
132
+ notifyWaitingConsumers() {
133
+ for (const consumer of this.consumers.values()) {
134
+ if (consumer.waitingPromise) {
135
+ if (this.completionError) {
136
+ consumer.waitingPromise.reject(this.completionError);
137
+ }
138
+ else {
139
+ consumer.waitingPromise.resolve();
140
+ }
141
+ consumer.waitingPromise = null;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ //# sourceMappingURL=tool-event-broadcaster.js.map
@@ -1,10 +1,25 @@
1
- import type { ZodType } from 'zod';
1
+ import type { $ZodType } from 'zod/v4/core';
2
2
  import type { APITool, Tool, ParsedToolCall, ToolExecutionResult, TurnContext } from './tool-types.js';
3
+ import * as z4 from 'zod/v4';
4
+ export declare const ZodError: z4.z.core.$constructor<z4.ZodError<unknown>, z4.z.core.$ZodIssue[]>;
3
5
  /**
4
- * Convert a Zod schema to JSON Schema using Zod v4's toJSONSchema function
5
- * Uses type assertion to bridge zod (user schemas) and zod/v4 (toJSONSchema)
6
+ * Recursively remove keys prefixed with ~ from an object.
7
+ * These are metadata properties (like ~standard from Standard Schema)
8
+ * that should not be sent to downstream providers.
9
+ * @see https://github.com/OpenRouterTeam/typescript-sdk/issues/131
10
+ *
11
+ * When given a Record<string, unknown>, returns Record<string, unknown>.
12
+ * When given unknown, returns unknown (preserves primitives, null, etc).
6
13
  */
7
- export declare function convertZodToJsonSchema(zodSchema: ZodType): Record<string, unknown>;
14
+ export declare function sanitizeJsonSchema(obj: Record<string, unknown>): Record<string, unknown>;
15
+ export declare function sanitizeJsonSchema(obj: unknown): unknown;
16
+ /**
17
+ * Convert a Zod schema to JSON Schema using Zod v4's toJSONSchema function.
18
+ * Accepts ZodType from the main zod package for user compatibility.
19
+ * The resulting schema is sanitized to remove metadata properties (like ~standard)
20
+ * that would cause 400 errors with downstream providers.
21
+ */
22
+ export declare function convertZodToJsonSchema(zodSchema: $ZodType): Record<string, unknown>;
8
23
  /**
9
24
  * Convert tools to OpenRouter API format
10
25
  * Accepts readonly arrays for better type compatibility
@@ -14,12 +29,12 @@ export declare function convertToolsToAPIFormat(tools: readonly Tool[]): APITool
14
29
  * Validate tool input against Zod schema
15
30
  * @throws ZodError if validation fails
16
31
  */
17
- export declare function validateToolInput<T>(schema: ZodType<T>, args: unknown): T;
32
+ export declare function validateToolInput<T>(schema: $ZodType<T>, args: unknown): T;
18
33
  /**
19
34
  * Validate tool output against Zod schema
20
35
  * @throws ZodError if validation fails
21
36
  */
22
- export declare function validateToolOutput<T>(schema: ZodType<T>, result: unknown): T;
37
+ export declare function validateToolOutput<T>(schema: $ZodType<T>, result: unknown): T;
23
38
  /**
24
39
  * Parse tool call arguments from JSON string
25
40
  */
@@ -1,15 +1,64 @@
1
- import { toJSONSchema, ZodError } from 'zod/v4';
1
+ import * as z4 from 'zod/v4';
2
2
  import { hasExecuteFunction, isGeneratorTool, isRegularExecuteTool } from './tool-types.js';
3
+ // Re-export ZodError for convenience
4
+ export const ZodError = z4.ZodError;
3
5
  /**
4
- * Convert a Zod schema to JSON Schema using Zod v4's toJSONSchema function
5
- * Uses type assertion to bridge zod (user schemas) and zod/v4 (toJSONSchema)
6
+ * Typeguard to check if a value is a non-null object (not an array).
7
+ */
8
+ function isNonNullObject(value) {
9
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ }
11
+ export function sanitizeJsonSchema(obj) {
12
+ if (obj === null || typeof obj !== 'object') {
13
+ return obj;
14
+ }
15
+ if (Array.isArray(obj)) {
16
+ return obj.map(sanitizeJsonSchema);
17
+ }
18
+ // At this point, obj is a non-null, non-array object
19
+ // Use typeguard to narrow the type for type-safe property access
20
+ if (!isNonNullObject(obj)) {
21
+ return obj;
22
+ }
23
+ const result = {};
24
+ for (const key of Object.keys(obj)) {
25
+ if (!key.startsWith('~')) {
26
+ result[key] = sanitizeJsonSchema(obj[key]);
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+ /**
32
+ * Typeguard to check if a value is a valid Zod schema compatible with zod/v4.
33
+ * Zod schemas have a _zod property that contains schema metadata.
34
+ */
35
+ function isZodSchema(value) {
36
+ if (typeof value !== 'object' || value === null) {
37
+ return false;
38
+ }
39
+ if (!('_zod' in value)) {
40
+ return false;
41
+ }
42
+ // After the 'in' check, TypeScript knows value has _zod property
43
+ return typeof value._zod === 'object';
44
+ }
45
+ /**
46
+ * Convert a Zod schema to JSON Schema using Zod v4's toJSONSchema function.
47
+ * Accepts ZodType from the main zod package for user compatibility.
48
+ * The resulting schema is sanitized to remove metadata properties (like ~standard)
49
+ * that would cause 400 errors with downstream providers.
6
50
  */
7
51
  export function convertZodToJsonSchema(zodSchema) {
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- const jsonSchema = toJSONSchema(zodSchema, {
52
+ if (!isZodSchema(zodSchema)) {
53
+ throw new Error('Invalid Zod schema provided');
54
+ }
55
+ // Use draft-7 as it's closest to OpenAPI 3.0's JSON Schema variant
56
+ const jsonSchema = z4.toJSONSchema(zodSchema, {
10
57
  target: 'draft-7',
11
58
  });
12
- return jsonSchema;
59
+ // jsonSchema is always a Record<string, unknown> from toJSONSchema
60
+ // The overloaded sanitizeJsonSchema preserves this type
61
+ return sanitizeJsonSchema(jsonSchema);
13
62
  }
14
63
  /**
15
64
  * Convert tools to OpenRouter API format
@@ -29,14 +78,14 @@ export function convertToolsToAPIFormat(tools) {
29
78
  * @throws ZodError if validation fails
30
79
  */
31
80
  export function validateToolInput(schema, args) {
32
- return schema.parse(args);
81
+ return z4.parse(schema, args);
33
82
  }
34
83
  /**
35
84
  * Validate tool output against Zod schema
36
85
  * @throws ZodError if validation fails
37
86
  */
38
87
  export function validateToolOutput(schema, result) {
39
- return schema.parse(result);
88
+ return z4.parse(schema, result);
40
89
  }
41
90
  /**
42
91
  * Parse tool call arguments from JSON string