@openrouter/sdk 0.1.17 → 0.1.23

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 (80) hide show
  1. package/README.md +0 -8
  2. package/esm/funcs/callModel.d.ts +77 -0
  3. package/esm/funcs/callModel.js +100 -0
  4. package/esm/lib/config.d.ts +10 -2
  5. package/esm/lib/config.js +2 -2
  6. package/esm/lib/env.d.ts +13 -0
  7. package/esm/lib/env.js +16 -0
  8. package/esm/lib/response-wrapper.d.ts +116 -0
  9. package/esm/lib/response-wrapper.js +459 -0
  10. package/esm/lib/reusable-stream.d.ts +39 -0
  11. package/esm/lib/reusable-stream.js +173 -0
  12. package/esm/lib/sdks.js +2 -2
  13. package/esm/lib/stream-transformers.d.ts +47 -0
  14. package/esm/lib/stream-transformers.js +280 -0
  15. package/esm/lib/tool-executor.d.ts +53 -0
  16. package/esm/lib/tool-executor.js +181 -0
  17. package/esm/lib/tool-orchestrator.d.ts +50 -0
  18. package/esm/lib/tool-orchestrator.js +132 -0
  19. package/esm/lib/tool-types.d.ts +199 -0
  20. package/esm/lib/tool-types.js +32 -0
  21. package/esm/sdk/sdk.d.ts +10 -0
  22. package/esm/sdk/sdk.js +9 -0
  23. package/jsr.json +1 -1
  24. package/package.json +2 -16
  25. package/vitest.config.ts +4 -0
  26. package/REACT_QUERY.md +0 -296
  27. package/esm/react-query/_context.d.ts +0 -8
  28. package/esm/react-query/_context.js +0 -14
  29. package/esm/react-query/_types.d.ts +0 -27
  30. package/esm/react-query/_types.js +0 -5
  31. package/esm/react-query/analyticsGetUserActivity.d.ts +0 -36
  32. package/esm/react-query/analyticsGetUserActivity.js +0 -77
  33. package/esm/react-query/apiKeysCreate.d.ts +0 -20
  34. package/esm/react-query/apiKeysCreate.js +0 -39
  35. package/esm/react-query/apiKeysDelete.d.ts +0 -20
  36. package/esm/react-query/apiKeysDelete.js +0 -39
  37. package/esm/react-query/apiKeysGet.d.ts +0 -24
  38. package/esm/react-query/apiKeysGet.js +0 -66
  39. package/esm/react-query/apiKeysGetCurrentKeyMetadata.d.ts +0 -29
  40. package/esm/react-query/apiKeysGetCurrentKeyMetadata.js +0 -66
  41. package/esm/react-query/apiKeysList.d.ts +0 -37
  42. package/esm/react-query/apiKeysList.js +0 -69
  43. package/esm/react-query/apiKeysUpdate.d.ts +0 -20
  44. package/esm/react-query/apiKeysUpdate.js +0 -39
  45. package/esm/react-query/betaResponsesSend.d.ts +0 -24
  46. package/esm/react-query/betaResponsesSend.js +0 -42
  47. package/esm/react-query/chatSend.d.ts +0 -24
  48. package/esm/react-query/chatSend.js +0 -42
  49. package/esm/react-query/completionsGenerate.d.ts +0 -23
  50. package/esm/react-query/completionsGenerate.js +0 -42
  51. package/esm/react-query/creditsCreateCoinbaseCharge.d.ts +0 -25
  52. package/esm/react-query/creditsCreateCoinbaseCharge.js +0 -42
  53. package/esm/react-query/creditsGetCredits.d.ts +0 -29
  54. package/esm/react-query/creditsGetCredits.js +0 -66
  55. package/esm/react-query/embeddingsGenerate.d.ts +0 -23
  56. package/esm/react-query/embeddingsGenerate.js +0 -42
  57. package/esm/react-query/embeddingsListModels.d.ts +0 -29
  58. package/esm/react-query/embeddingsListModels.js +0 -66
  59. package/esm/react-query/endpointsList.d.ts +0 -24
  60. package/esm/react-query/endpointsList.js +0 -66
  61. package/esm/react-query/endpointsListZdrEndpoints.d.ts +0 -23
  62. package/esm/react-query/endpointsListZdrEndpoints.js +0 -60
  63. package/esm/react-query/generationsGetGeneration.d.ts +0 -30
  64. package/esm/react-query/generationsGetGeneration.js +0 -71
  65. package/esm/react-query/index.d.ts +0 -27
  66. package/esm/react-query/index.js +0 -30
  67. package/esm/react-query/modelsCount.d.ts +0 -23
  68. package/esm/react-query/modelsCount.js +0 -60
  69. package/esm/react-query/modelsList.d.ts +0 -38
  70. package/esm/react-query/modelsList.js +0 -69
  71. package/esm/react-query/modelsListForUser.d.ts +0 -24
  72. package/esm/react-query/modelsListForUser.js +0 -60
  73. package/esm/react-query/oAuthCreateAuthCode.d.ts +0 -23
  74. package/esm/react-query/oAuthCreateAuthCode.js +0 -42
  75. package/esm/react-query/oAuthExchangeAuthCodeForAPIKey.d.ts +0 -23
  76. package/esm/react-query/oAuthExchangeAuthCodeForAPIKey.js +0 -42
  77. package/esm/react-query/parametersGetParameters.d.ts +0 -38
  78. package/esm/react-query/parametersGetParameters.js +0 -80
  79. package/esm/react-query/providersList.d.ts +0 -23
  80. package/esm/react-query/providersList.js +0 -60
@@ -0,0 +1,459 @@
1
+ import { betaResponsesSend } from "../funcs/betaResponsesSend.js";
2
+ import { ReusableReadableStream } from "./reusable-stream.js";
3
+ import { extractTextDeltas, extractReasoningDeltas, extractToolDeltas, buildMessageStream, consumeStreamForCompletion, extractMessageFromResponse, extractTextFromResponse, extractToolCallsFromResponse, buildToolCallStream, } from "./stream-transformers.js";
4
+ import { hasExecuteFunction, } from "./tool-types.js";
5
+ import { executeTool, } from "./tool-executor.js";
6
+ /**
7
+ * A wrapper around a streaming response that provides multiple consumption patterns.
8
+ *
9
+ * Allows consuming the response in multiple ways:
10
+ * - `await response.getMessage()` - Get the completed message
11
+ * - `await response.getText()` - Get just the text
12
+ * - `for await (const delta of response.getTextStream())` - Stream text deltas
13
+ * - `for await (const msg of response.getNewMessagesStream())` - Stream incremental message updates
14
+ * - `for await (const event of response.getFullResponsesStream())` - Stream all response events
15
+ *
16
+ * All consumption patterns can be used concurrently thanks to the underlying
17
+ * ReusableReadableStream implementation.
18
+ */
19
+ export class ResponseWrapper {
20
+ constructor(options) {
21
+ this.reusableStream = null;
22
+ this.streamPromise = null;
23
+ this.messagePromise = null;
24
+ this.textPromise = null;
25
+ this.initPromise = null;
26
+ this.toolExecutionPromise = null;
27
+ this.finalResponse = null;
28
+ this.preliminaryResults = new Map();
29
+ this.allToolExecutionRounds = [];
30
+ this.options = options;
31
+ }
32
+ /**
33
+ * Initialize the stream if not already started
34
+ * This is idempotent - multiple calls will return the same promise
35
+ */
36
+ initStream() {
37
+ if (this.initPromise) {
38
+ return this.initPromise;
39
+ }
40
+ this.initPromise = (async () => {
41
+ // Force stream mode
42
+ const request = { ...this.options.request, stream: true };
43
+ // Create the stream promise
44
+ this.streamPromise = betaResponsesSend(this.options.client, request, this.options.options).then((result) => {
45
+ if (!result.ok) {
46
+ throw result.error;
47
+ }
48
+ return result.value;
49
+ });
50
+ // Wait for the stream and create the reusable stream
51
+ const eventStream = await this.streamPromise;
52
+ this.reusableStream = new ReusableReadableStream(eventStream);
53
+ })();
54
+ return this.initPromise;
55
+ }
56
+ /**
57
+ * Execute tools automatically if they are provided and have execute functions
58
+ * This is idempotent - multiple calls will return the same promise
59
+ */
60
+ async executeToolsIfNeeded() {
61
+ if (this.toolExecutionPromise) {
62
+ return this.toolExecutionPromise;
63
+ }
64
+ this.toolExecutionPromise = (async () => {
65
+ await this.initStream();
66
+ if (!this.reusableStream) {
67
+ throw new Error("Stream not initialized");
68
+ }
69
+ // Get the initial response
70
+ const initialResponse = await consumeStreamForCompletion(this.reusableStream);
71
+ // Check if we have tools and if auto-execution is enabled
72
+ const shouldAutoExecute = this.options.tools &&
73
+ this.options.tools.length > 0 &&
74
+ initialResponse.output.some((item) => "type" in item && item.type === "function_call");
75
+ if (!shouldAutoExecute) {
76
+ // No tools to execute, use initial response
77
+ this.finalResponse = initialResponse;
78
+ return;
79
+ }
80
+ // Extract tool calls
81
+ const toolCalls = extractToolCallsFromResponse(initialResponse);
82
+ // Check if any have execute functions
83
+ const executableTools = toolCalls.filter((toolCall) => {
84
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
85
+ return tool && hasExecuteFunction(tool);
86
+ });
87
+ if (executableTools.length === 0) {
88
+ // No executable tools, use initial response
89
+ this.finalResponse = initialResponse;
90
+ return;
91
+ }
92
+ // Get maxToolRounds configuration
93
+ const maxToolRounds = this.options.maxToolRounds ?? 5;
94
+ let currentResponse = initialResponse;
95
+ let currentRound = 0;
96
+ let currentInput = this.options.request.input || [];
97
+ while (true) {
98
+ const currentToolCalls = extractToolCallsFromResponse(currentResponse);
99
+ if (currentToolCalls.length === 0) {
100
+ break;
101
+ }
102
+ const hasExecutable = currentToolCalls.some((toolCall) => {
103
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
104
+ return tool && hasExecuteFunction(tool);
105
+ });
106
+ if (!hasExecutable) {
107
+ break;
108
+ }
109
+ // Check if we should continue based on maxToolRounds
110
+ if (typeof maxToolRounds === "number") {
111
+ if (currentRound >= maxToolRounds) {
112
+ break;
113
+ }
114
+ }
115
+ else if (typeof maxToolRounds === "function") {
116
+ // Function signature: (context: TurnContext) => boolean
117
+ const turnContext = {
118
+ numberOfTurns: currentRound + 1,
119
+ messageHistory: currentInput,
120
+ ...(this.options.request.model && { model: this.options.request.model }),
121
+ ...(this.options.request.models && { models: this.options.request.models }),
122
+ };
123
+ const shouldContinue = maxToolRounds(turnContext);
124
+ if (!shouldContinue) {
125
+ break;
126
+ }
127
+ }
128
+ // Store execution round info
129
+ this.allToolExecutionRounds.push({
130
+ round: currentRound,
131
+ toolCalls: currentToolCalls,
132
+ response: currentResponse,
133
+ });
134
+ // Build turn context for tool execution
135
+ const turnContext = {
136
+ numberOfTurns: currentRound + 1, // 1-indexed
137
+ messageHistory: currentInput,
138
+ ...(this.options.request.model && { model: this.options.request.model }),
139
+ ...(this.options.request.models && { models: this.options.request.models }),
140
+ };
141
+ // Execute all tool calls
142
+ const toolResults = [];
143
+ for (const toolCall of currentToolCalls) {
144
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
145
+ if (!tool || !hasExecuteFunction(tool)) {
146
+ continue;
147
+ }
148
+ const result = await executeTool(tool, toolCall, turnContext);
149
+ // Store preliminary results
150
+ if (result.preliminaryResults && result.preliminaryResults.length > 0) {
151
+ this.preliminaryResults.set(toolCall.id, result.preliminaryResults);
152
+ }
153
+ toolResults.push({
154
+ type: "function_call_output",
155
+ id: `output_${toolCall.id}`,
156
+ callId: toolCall.id,
157
+ output: result.error
158
+ ? JSON.stringify({ error: result.error.message })
159
+ : JSON.stringify(result.result),
160
+ });
161
+ }
162
+ // Build new input with tool results
163
+ // For the Responses API, we need to include the tool results in the input
164
+ const newInput = [
165
+ ...(Array.isArray(currentResponse.output) ? currentResponse.output : [currentResponse.output]),
166
+ ...toolResults,
167
+ ];
168
+ // Update current input for next iteration
169
+ currentInput = newInput;
170
+ // Make new request with tool results
171
+ const newRequest = {
172
+ ...this.options.request,
173
+ input: newInput,
174
+ stream: false,
175
+ };
176
+ const newResult = await betaResponsesSend(this.options.client, newRequest, this.options.options);
177
+ if (!newResult.ok) {
178
+ throw newResult.error;
179
+ }
180
+ // Handle the result - it might be a stream or a response
181
+ const value = newResult.value;
182
+ if (value && typeof value === "object" && "toReadableStream" in value) {
183
+ // It's a stream, consume it
184
+ const stream = new ReusableReadableStream(value);
185
+ currentResponse = await consumeStreamForCompletion(stream);
186
+ }
187
+ else {
188
+ currentResponse = value;
189
+ }
190
+ currentRound++;
191
+ }
192
+ this.finalResponse = currentResponse;
193
+ })();
194
+ return this.toolExecutionPromise;
195
+ }
196
+ /**
197
+ * Get the completed message from the response.
198
+ * This will consume the stream until completion, execute any tools, and extract the first message.
199
+ * Returns an AssistantMessage in chat format.
200
+ */
201
+ getMessage() {
202
+ if (this.messagePromise) {
203
+ return this.messagePromise;
204
+ }
205
+ this.messagePromise = (async () => {
206
+ await this.executeToolsIfNeeded();
207
+ if (!this.finalResponse) {
208
+ throw new Error("Response not available");
209
+ }
210
+ return extractMessageFromResponse(this.finalResponse);
211
+ })();
212
+ return this.messagePromise;
213
+ }
214
+ /**
215
+ * Get just the text content from the response.
216
+ * This will consume the stream until completion, execute any tools, and extract the text.
217
+ */
218
+ getText() {
219
+ if (this.textPromise) {
220
+ return this.textPromise;
221
+ }
222
+ this.textPromise = (async () => {
223
+ await this.executeToolsIfNeeded();
224
+ if (!this.finalResponse) {
225
+ throw new Error("Response not available");
226
+ }
227
+ return extractTextFromResponse(this.finalResponse);
228
+ })();
229
+ return this.textPromise;
230
+ }
231
+ /**
232
+ * Stream all response events as they arrive.
233
+ * Multiple consumers can iterate over this stream concurrently.
234
+ * Includes preliminary tool result events after tool execution.
235
+ */
236
+ getFullResponsesStream() {
237
+ return (async function* () {
238
+ await this.initStream();
239
+ if (!this.reusableStream) {
240
+ throw new Error("Stream not initialized");
241
+ }
242
+ const consumer = this.reusableStream.createConsumer();
243
+ // Yield original events directly
244
+ for await (const event of consumer) {
245
+ yield event;
246
+ }
247
+ // After stream completes, check if tools were executed and emit preliminary results
248
+ await this.executeToolsIfNeeded();
249
+ // Emit all preliminary results as new event types
250
+ for (const [toolCallId, results] of this.preliminaryResults) {
251
+ for (const result of results) {
252
+ yield {
253
+ type: "tool.preliminary_result",
254
+ toolCallId,
255
+ result,
256
+ timestamp: Date.now(),
257
+ };
258
+ }
259
+ }
260
+ }.call(this));
261
+ }
262
+ /**
263
+ * Stream only text deltas as they arrive.
264
+ * This filters the full event stream to only yield text content.
265
+ */
266
+ getTextStream() {
267
+ return (async function* () {
268
+ await this.initStream();
269
+ if (!this.reusableStream) {
270
+ throw new Error("Stream not initialized");
271
+ }
272
+ yield* extractTextDeltas(this.reusableStream);
273
+ }.call(this));
274
+ }
275
+ /**
276
+ * Stream incremental message updates as content is added.
277
+ * Each iteration yields an updated version of the message with new content.
278
+ * Also yields ToolResponseMessages after tool execution completes.
279
+ * Returns AssistantMessage or ToolResponseMessage in chat format.
280
+ */
281
+ getNewMessagesStream() {
282
+ return (async function* () {
283
+ await this.initStream();
284
+ if (!this.reusableStream) {
285
+ throw new Error("Stream not initialized");
286
+ }
287
+ // First yield assistant messages from the stream
288
+ yield* buildMessageStream(this.reusableStream);
289
+ // Execute tools if needed
290
+ await this.executeToolsIfNeeded();
291
+ // Yield tool response messages for each executed tool
292
+ for (const round of this.allToolExecutionRounds) {
293
+ for (const toolCall of round.toolCalls) {
294
+ // Find the tool to check if it was executed
295
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
296
+ if (!tool || !hasExecuteFunction(tool)) {
297
+ continue;
298
+ }
299
+ // Get the result from preliminary results or construct from the response
300
+ const prelimResults = this.preliminaryResults.get(toolCall.id);
301
+ const result = prelimResults && prelimResults.length > 0
302
+ ? prelimResults[prelimResults.length - 1] // Last result is the final output
303
+ : undefined;
304
+ // Yield tool response message
305
+ yield {
306
+ role: "tool",
307
+ content: result !== undefined ? JSON.stringify(result) : "",
308
+ toolCallId: toolCall.id,
309
+ };
310
+ }
311
+ }
312
+ // If tools were executed, yield the final assistant message (if there is one)
313
+ if (this.finalResponse && this.allToolExecutionRounds.length > 0) {
314
+ // Check if the final response contains a message
315
+ const hasMessage = this.finalResponse.output.some((item) => "type" in item && item.type === "message");
316
+ if (hasMessage) {
317
+ yield extractMessageFromResponse(this.finalResponse);
318
+ }
319
+ }
320
+ }.call(this));
321
+ }
322
+ /**
323
+ * Stream only reasoning deltas as they arrive.
324
+ * This filters the full event stream to only yield reasoning content.
325
+ */
326
+ getReasoningStream() {
327
+ return (async function* () {
328
+ await this.initStream();
329
+ if (!this.reusableStream) {
330
+ throw new Error("Stream not initialized");
331
+ }
332
+ yield* extractReasoningDeltas(this.reusableStream);
333
+ }.call(this));
334
+ }
335
+ /**
336
+ * Stream tool call argument deltas and preliminary results.
337
+ * This filters the full event stream to yield:
338
+ * - Tool call argument deltas as { type: "delta", content: string }
339
+ * - Preliminary results as { type: "preliminary_result", toolCallId, result }
340
+ */
341
+ getToolStream() {
342
+ return (async function* () {
343
+ await this.initStream();
344
+ if (!this.reusableStream) {
345
+ throw new Error("Stream not initialized");
346
+ }
347
+ // Yield tool deltas as structured events
348
+ for await (const delta of extractToolDeltas(this.reusableStream)) {
349
+ yield { type: "delta", content: delta };
350
+ }
351
+ // After stream completes, check if tools were executed and emit preliminary results
352
+ await this.executeToolsIfNeeded();
353
+ // Emit all preliminary results
354
+ for (const [toolCallId, results] of this.preliminaryResults) {
355
+ for (const result of results) {
356
+ yield {
357
+ type: "preliminary_result",
358
+ toolCallId,
359
+ result,
360
+ };
361
+ }
362
+ }
363
+ }.call(this));
364
+ }
365
+ /**
366
+ * Stream events in chat format (compatibility layer).
367
+ * Note: This transforms responses API events into a chat-like format.
368
+ * Includes preliminary tool result events after tool execution.
369
+ *
370
+ * @remarks
371
+ * This is a compatibility method that attempts to transform the responses API
372
+ * stream into a format similar to the chat API. Due to differences in the APIs,
373
+ * this may not be a perfect mapping.
374
+ */
375
+ getFullChatStream() {
376
+ return (async function* () {
377
+ await this.initStream();
378
+ if (!this.reusableStream) {
379
+ throw new Error("Stream not initialized");
380
+ }
381
+ const consumer = this.reusableStream.createConsumer();
382
+ for await (const event of consumer) {
383
+ if (!("type" in event))
384
+ continue;
385
+ // Transform responses events to chat-like format
386
+ // This is a simplified transformation - you may need to adjust based on your needs
387
+ if (event.type === "response.output_text.delta") {
388
+ const deltaEvent = event;
389
+ yield {
390
+ type: "content.delta",
391
+ delta: deltaEvent.delta,
392
+ };
393
+ }
394
+ else if (event.type === "response.completed") {
395
+ const completedEvent = event;
396
+ yield {
397
+ type: "message.complete",
398
+ response: completedEvent.response,
399
+ };
400
+ }
401
+ else {
402
+ // Pass through other events
403
+ yield {
404
+ type: event.type,
405
+ event,
406
+ };
407
+ }
408
+ }
409
+ // After stream completes, check if tools were executed and emit preliminary results
410
+ await this.executeToolsIfNeeded();
411
+ // Emit all preliminary results
412
+ for (const [toolCallId, results] of this.preliminaryResults) {
413
+ for (const result of results) {
414
+ yield {
415
+ type: "tool.preliminary_result",
416
+ toolCallId,
417
+ result,
418
+ };
419
+ }
420
+ }
421
+ }.call(this));
422
+ }
423
+ /**
424
+ * Get all tool calls from the completed response (before auto-execution).
425
+ * Note: If tools have execute functions, they will be automatically executed
426
+ * and this will return the tool calls from the initial response.
427
+ * Returns structured tool calls with parsed arguments.
428
+ */
429
+ async getToolCalls() {
430
+ await this.initStream();
431
+ if (!this.reusableStream) {
432
+ throw new Error("Stream not initialized");
433
+ }
434
+ const completedResponse = await consumeStreamForCompletion(this.reusableStream);
435
+ return extractToolCallsFromResponse(completedResponse);
436
+ }
437
+ /**
438
+ * Stream structured tool call objects as they're completed.
439
+ * Each iteration yields a complete tool call with parsed arguments.
440
+ */
441
+ getToolCallsStream() {
442
+ return (async function* () {
443
+ await this.initStream();
444
+ if (!this.reusableStream) {
445
+ throw new Error("Stream not initialized");
446
+ }
447
+ yield* buildToolCallStream(this.reusableStream);
448
+ }.call(this));
449
+ }
450
+ /**
451
+ * Cancel the underlying stream and all consumers
452
+ */
453
+ async cancel() {
454
+ if (this.reusableStream) {
455
+ await this.reusableStream.cancel();
456
+ }
457
+ }
458
+ }
459
+ //# sourceMappingURL=response-wrapper.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A reusable readable stream that allows multiple consumers to read from the same source stream
3
+ * concurrently while it's actively streaming, without forcing consumers to wait for full buffering.
4
+ *
5
+ * Key features:
6
+ * - Multiple concurrent consumers with independent read positions
7
+ * - New consumers can attach while streaming is active
8
+ * - Efficient memory management with automatic cleanup
9
+ * - Each consumer can read at their own pace
10
+ */
11
+ export declare class ReusableReadableStream<T> {
12
+ private sourceStream;
13
+ private buffer;
14
+ private consumers;
15
+ private nextConsumerId;
16
+ private sourceReader;
17
+ private sourceComplete;
18
+ private sourceError;
19
+ private pumpStarted;
20
+ constructor(sourceStream: ReadableStream<T>);
21
+ /**
22
+ * Create a new consumer that can independently iterate over the stream.
23
+ * Multiple consumers can be created and will all receive the same data.
24
+ */
25
+ createConsumer(): AsyncIterableIterator<T>;
26
+ /**
27
+ * Start pumping data from the source stream into the buffer
28
+ */
29
+ private startPump;
30
+ /**
31
+ * Notify all waiting consumers that new data is available
32
+ */
33
+ private notifyAllConsumers;
34
+ /**
35
+ * Cancel the source stream and all consumers
36
+ */
37
+ cancel(): Promise<void>;
38
+ }
39
+ //# sourceMappingURL=reusable-stream.d.ts.map
@@ -0,0 +1,173 @@
1
+ /**
2
+ * A reusable readable stream that allows multiple consumers to read from the same source stream
3
+ * concurrently while it's actively streaming, without forcing consumers to wait for full buffering.
4
+ *
5
+ * Key features:
6
+ * - Multiple concurrent consumers with independent read positions
7
+ * - New consumers can attach while streaming is active
8
+ * - Efficient memory management with automatic cleanup
9
+ * - Each consumer can read at their own pace
10
+ */
11
+ export class ReusableReadableStream {
12
+ constructor(sourceStream) {
13
+ this.sourceStream = sourceStream;
14
+ this.buffer = [];
15
+ this.consumers = new Map();
16
+ this.nextConsumerId = 0;
17
+ this.sourceReader = null;
18
+ this.sourceComplete = false;
19
+ this.sourceError = null;
20
+ this.pumpStarted = false;
21
+ }
22
+ /**
23
+ * Create a new consumer that can independently iterate over the stream.
24
+ * Multiple consumers can be created and will all receive the same data.
25
+ */
26
+ createConsumer() {
27
+ const consumerId = this.nextConsumerId++;
28
+ const state = {
29
+ position: 0,
30
+ waitingPromise: null,
31
+ cancelled: false,
32
+ };
33
+ this.consumers.set(consumerId, state);
34
+ // Start pumping the source stream if not already started
35
+ if (!this.pumpStarted) {
36
+ this.startPump();
37
+ }
38
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
39
+ const self = this;
40
+ return {
41
+ async next() {
42
+ const consumer = self.consumers.get(consumerId);
43
+ if (!consumer) {
44
+ return { done: true, value: undefined };
45
+ }
46
+ if (consumer.cancelled) {
47
+ return { done: true, value: undefined };
48
+ }
49
+ // If we have buffered data at this position, return it
50
+ if (consumer.position < self.buffer.length) {
51
+ const value = self.buffer[consumer.position];
52
+ consumer.position++;
53
+ // Note: We don't clean up buffer to allow sequential/reusable access
54
+ return { done: false, value };
55
+ }
56
+ // If source is complete and we've read everything, we're done
57
+ if (self.sourceComplete) {
58
+ self.consumers.delete(consumerId);
59
+ return { done: true, value: undefined };
60
+ }
61
+ // If source had an error, propagate it
62
+ if (self.sourceError) {
63
+ self.consumers.delete(consumerId);
64
+ throw self.sourceError;
65
+ }
66
+ // Wait for more data - but check conditions after setting up the promise
67
+ // to avoid race condition where source completes between check and wait
68
+ const waitPromise = new Promise((resolve, reject) => {
69
+ consumer.waitingPromise = { resolve, reject };
70
+ });
71
+ // Double-check conditions after setting up promise to handle race
72
+ if (self.sourceComplete || self.sourceError || consumer.position < self.buffer.length) {
73
+ // Resolve immediately if conditions changed
74
+ if (consumer.waitingPromise) {
75
+ consumer.waitingPromise.resolve();
76
+ consumer.waitingPromise = null;
77
+ }
78
+ }
79
+ await waitPromise;
80
+ // Recursively try again after waking up
81
+ return this.next();
82
+ },
83
+ async return() {
84
+ const consumer = self.consumers.get(consumerId);
85
+ if (consumer) {
86
+ consumer.cancelled = true;
87
+ self.consumers.delete(consumerId);
88
+ }
89
+ return { done: true, value: undefined };
90
+ },
91
+ async throw(e) {
92
+ const consumer = self.consumers.get(consumerId);
93
+ if (consumer) {
94
+ consumer.cancelled = true;
95
+ self.consumers.delete(consumerId);
96
+ }
97
+ throw e;
98
+ },
99
+ [Symbol.asyncIterator]() {
100
+ return this;
101
+ },
102
+ };
103
+ }
104
+ /**
105
+ * Start pumping data from the source stream into the buffer
106
+ */
107
+ startPump() {
108
+ if (this.pumpStarted)
109
+ return;
110
+ this.pumpStarted = true;
111
+ this.sourceReader = this.sourceStream.getReader();
112
+ void (async () => {
113
+ try {
114
+ while (true) {
115
+ const result = await this.sourceReader.read();
116
+ if (result.done) {
117
+ this.sourceComplete = true;
118
+ this.notifyAllConsumers();
119
+ break;
120
+ }
121
+ // Add to buffer
122
+ this.buffer.push(result.value);
123
+ // Notify waiting consumers
124
+ this.notifyAllConsumers();
125
+ }
126
+ }
127
+ catch (error) {
128
+ this.sourceError = error instanceof Error ? error : new Error(String(error));
129
+ this.notifyAllConsumers();
130
+ }
131
+ finally {
132
+ if (this.sourceReader) {
133
+ this.sourceReader.releaseLock();
134
+ }
135
+ }
136
+ })();
137
+ }
138
+ /**
139
+ * Notify all waiting consumers that new data is available
140
+ */
141
+ notifyAllConsumers() {
142
+ for (const consumer of this.consumers.values()) {
143
+ if (consumer.waitingPromise) {
144
+ if (this.sourceError) {
145
+ consumer.waitingPromise.reject(this.sourceError);
146
+ }
147
+ else {
148
+ consumer.waitingPromise.resolve();
149
+ }
150
+ consumer.waitingPromise = null;
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Cancel the source stream and all consumers
156
+ */
157
+ async cancel() {
158
+ // Cancel all consumers
159
+ for (const consumer of this.consumers.values()) {
160
+ consumer.cancelled = true;
161
+ if (consumer.waitingPromise) {
162
+ consumer.waitingPromise.resolve();
163
+ }
164
+ }
165
+ this.consumers.clear();
166
+ // Cancel the source stream
167
+ if (this.sourceReader) {
168
+ await this.sourceReader.cancel();
169
+ this.sourceReader.releaseLock();
170
+ }
171
+ }
172
+ }
173
+ //# sourceMappingURL=reusable-stream.js.map
package/esm/lib/sdks.js CHANGED
@@ -19,7 +19,7 @@ import { ERR, OK } from "../types/fp.js";
19
19
  import { stringToBase64 } from "./base64.js";
20
20
  import { SDK_METADATA, serverURLFromOptions } from "./config.js";
21
21
  import { encodeForm } from "./encodings.js";
22
- import { env } from "./env.js";
22
+ import { env, fillGlobals } from "./env.js";
23
23
  import { HTTPClient, isAbortError, isConnectionError, isTimeoutError, matchContentType, matchStatusCode, } from "./http.js";
24
24
  import { retry } from "./retries.js";
25
25
  const gt = typeof globalThis === "undefined" ? null : globalThis;
@@ -54,7 +54,7 @@ export class ClientSDK {
54
54
  }
55
55
  this._baseURL = url;
56
56
  __classPrivateFieldSet(this, _ClientSDK_httpClient, options.httpClient || defaultHttpClient, "f");
57
- this._options = { ...options, hooks: __classPrivateFieldGet(this, _ClientSDK_hooks, "f") };
57
+ this._options = { ...fillGlobals(options), hooks: __classPrivateFieldGet(this, _ClientSDK_hooks, "f") };
58
58
  __classPrivateFieldSet(this, _ClientSDK_logger, this._options.debugLogger, "f");
59
59
  if (!__classPrivateFieldGet(this, _ClientSDK_logger, "f") && env().OPENROUTER_DEBUG) {
60
60
  __classPrivateFieldSet(this, _ClientSDK_logger, console, "f");