@jaypie/llm 1.2.0-rc.0 → 1.2.0-rc.10

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 (49) hide show
  1. package/dist/cjs/Llm.d.ts +7 -0
  2. package/dist/cjs/index.cjs +834 -4
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/cjs/index.d.ts +2 -0
  5. package/dist/cjs/operate/OperateLoop.d.ts +0 -2
  6. package/dist/cjs/operate/StreamLoop.d.ts +41 -0
  7. package/dist/cjs/operate/adapters/AnthropicAdapter.d.ts +2 -0
  8. package/dist/cjs/operate/adapters/GeminiAdapter.d.ts +2 -0
  9. package/dist/cjs/operate/adapters/OpenAiAdapter.d.ts +2 -0
  10. package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +2 -0
  11. package/dist/cjs/operate/adapters/ProviderAdapter.interface.d.ts +9 -0
  12. package/dist/cjs/operate/index.d.ts +2 -0
  13. package/dist/cjs/providers/anthropic/AnthropicProvider.class.d.ts +4 -0
  14. package/dist/cjs/providers/anthropic/utils.d.ts +1 -1
  15. package/dist/cjs/providers/gemini/GeminiProvider.class.d.ts +4 -0
  16. package/dist/cjs/providers/gemini/utils.d.ts +1 -1
  17. package/dist/cjs/providers/openai/OpenAiProvider.class.d.ts +4 -0
  18. package/dist/cjs/providers/openai/utils.d.ts +1 -1
  19. package/dist/cjs/providers/openrouter/OpenRouterProvider.class.d.ts +4 -0
  20. package/dist/cjs/providers/openrouter/utils.d.ts +1 -1
  21. package/dist/cjs/tools/Toolkit.class.d.ts +1 -1
  22. package/dist/cjs/types/LlmProvider.interface.d.ts +2 -0
  23. package/dist/cjs/types/LlmStreamChunk.interface.d.ts +41 -0
  24. package/dist/cjs/util/logger.d.ts +2 -2
  25. package/dist/esm/Llm.d.ts +7 -0
  26. package/dist/esm/index.d.ts +2 -0
  27. package/dist/esm/index.js +835 -5
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/operate/OperateLoop.d.ts +0 -2
  30. package/dist/esm/operate/StreamLoop.d.ts +41 -0
  31. package/dist/esm/operate/adapters/AnthropicAdapter.d.ts +2 -0
  32. package/dist/esm/operate/adapters/GeminiAdapter.d.ts +2 -0
  33. package/dist/esm/operate/adapters/OpenAiAdapter.d.ts +2 -0
  34. package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +2 -0
  35. package/dist/esm/operate/adapters/ProviderAdapter.interface.d.ts +9 -0
  36. package/dist/esm/operate/index.d.ts +2 -0
  37. package/dist/esm/providers/anthropic/AnthropicProvider.class.d.ts +4 -0
  38. package/dist/esm/providers/anthropic/utils.d.ts +1 -1
  39. package/dist/esm/providers/gemini/GeminiProvider.class.d.ts +4 -0
  40. package/dist/esm/providers/gemini/utils.d.ts +1 -1
  41. package/dist/esm/providers/openai/OpenAiProvider.class.d.ts +4 -0
  42. package/dist/esm/providers/openai/utils.d.ts +1 -1
  43. package/dist/esm/providers/openrouter/OpenRouterProvider.class.d.ts +4 -0
  44. package/dist/esm/providers/openrouter/utils.d.ts +1 -1
  45. package/dist/esm/tools/Toolkit.class.d.ts +1 -1
  46. package/dist/esm/types/LlmProvider.interface.d.ts +2 -0
  47. package/dist/esm/types/LlmStreamChunk.interface.d.ts +41 -0
  48. package/dist/esm/util/logger.d.ts +2 -2
  49. package/package.json +6 -4
@@ -158,6 +158,14 @@ function determineModelProvider(input) {
158
158
  provider: DEFAULT.PROVIDER.NAME,
159
159
  };
160
160
  }
161
+ // Check for explicit openrouter: prefix
162
+ if (input.startsWith("openrouter:")) {
163
+ const model = input.slice("openrouter:".length);
164
+ return {
165
+ model,
166
+ provider: PROVIDER.OPENROUTER.NAME,
167
+ };
168
+ }
161
169
  // Check if input is a provider name
162
170
  if (input === PROVIDER.ANTHROPIC.NAME) {
163
171
  return {
@@ -219,6 +227,14 @@ function determineModelProvider(input) {
219
227
  };
220
228
  }
221
229
  }
230
+ // Assume OpenRouter for models containing "/" (e.g., "openai/gpt-4", "anthropic/claude-3-opus")
231
+ // This check must come before match words so that "openai/gpt-4" is not matched by "openai" keyword
232
+ if (input.includes("/")) {
233
+ return {
234
+ model: input,
235
+ provider: PROVIDER.OPENROUTER.NAME,
236
+ };
237
+ }
222
238
  // Check Anthropic match words
223
239
  const lowerInput = input.toLowerCase();
224
240
  for (const matchWord of PROVIDER.ANTHROPIC.MODEL_MATCH_WORDS) {
@@ -338,6 +354,19 @@ var LlmResponseStatus;
338
354
  LlmResponseStatus["InProgress"] = "in_progress";
339
355
  })(LlmResponseStatus || (LlmResponseStatus = {}));
340
356
 
357
+ //
358
+ //
359
+ // Types
360
+ //
361
+ exports.LlmStreamChunkType = void 0;
362
+ (function (LlmStreamChunkType) {
363
+ LlmStreamChunkType["Done"] = "done";
364
+ LlmStreamChunkType["Error"] = "error";
365
+ LlmStreamChunkType["Text"] = "text";
366
+ LlmStreamChunkType["ToolCall"] = "tool_call";
367
+ LlmStreamChunkType["ToolResult"] = "tool_result";
368
+ })(exports.LlmStreamChunkType || (exports.LlmStreamChunkType = {}));
369
+
341
370
  /**
342
371
  * Converts a string to a standardized LlmInputMessage
343
372
  * @param input - String to format
@@ -722,7 +751,13 @@ class AnthropicAdapter extends BaseProviderAdapter {
722
751
  //
723
752
  buildRequest(request) {
724
753
  // Convert messages to Anthropic format (remove 'type' property)
725
- const messages = request.messages.map((msg) => {
754
+ // Filter out system messages - Anthropic only accepts system as a top-level field
755
+ const messages = request.messages
756
+ .filter((msg) => {
757
+ const role = msg.role;
758
+ return role !== "system";
759
+ })
760
+ .map((msg) => {
726
761
  const anthropicMsg = structuredClone(msg);
727
762
  delete anthropicMsg.type;
728
763
  return anthropicMsg;
@@ -815,6 +850,90 @@ class AnthropicAdapter extends BaseProviderAdapter {
815
850
  const anthropic = client;
816
851
  return (await anthropic.messages.create(request));
817
852
  }
853
+ async *executeStreamRequest(client, request) {
854
+ const anthropic = client;
855
+ const streamRequest = {
856
+ ...request,
857
+ stream: true,
858
+ };
859
+ const stream = await anthropic.messages.create(streamRequest);
860
+ // Track current tool call being built
861
+ let currentToolCall = null;
862
+ // Track usage for final chunk
863
+ let inputTokens = 0;
864
+ let outputTokens = 0;
865
+ let model = streamRequest.model;
866
+ for await (const event of stream) {
867
+ if (event.type === "message_start") {
868
+ // Extract initial usage and model info
869
+ const message = event.message;
870
+ if (message.usage) {
871
+ inputTokens = message.usage.input_tokens;
872
+ }
873
+ model = message.model;
874
+ }
875
+ else if (event.type === "content_block_start") {
876
+ const contentBlock = event.content_block;
877
+ if (contentBlock.type === "tool_use") {
878
+ // Start building a tool call
879
+ currentToolCall = {
880
+ id: contentBlock.id,
881
+ name: contentBlock.name,
882
+ arguments: "",
883
+ };
884
+ }
885
+ }
886
+ else if (event.type === "content_block_delta") {
887
+ const delta = event.delta;
888
+ if (delta.type === "text_delta") {
889
+ yield {
890
+ type: exports.LlmStreamChunkType.Text,
891
+ content: delta.text,
892
+ };
893
+ }
894
+ else if (delta.type === "input_json_delta" && currentToolCall) {
895
+ // Accumulate tool call arguments
896
+ currentToolCall.arguments += delta.partial_json;
897
+ }
898
+ }
899
+ else if (event.type === "content_block_stop") {
900
+ // If we were building a tool call, emit it now
901
+ if (currentToolCall) {
902
+ yield {
903
+ type: exports.LlmStreamChunkType.ToolCall,
904
+ toolCall: {
905
+ id: currentToolCall.id,
906
+ name: currentToolCall.name,
907
+ arguments: currentToolCall.arguments,
908
+ },
909
+ };
910
+ currentToolCall = null;
911
+ }
912
+ }
913
+ else if (event.type === "message_delta") {
914
+ // Extract final usage
915
+ if (event.usage) {
916
+ outputTokens = event.usage.output_tokens;
917
+ }
918
+ }
919
+ else if (event.type === "message_stop") {
920
+ // Emit done chunk with usage
921
+ yield {
922
+ type: exports.LlmStreamChunkType.Done,
923
+ usage: [
924
+ {
925
+ input: inputTokens,
926
+ output: outputTokens,
927
+ reasoning: 0,
928
+ total: inputTokens + outputTokens,
929
+ provider: this.name,
930
+ model,
931
+ },
932
+ ],
933
+ };
934
+ }
935
+ }
936
+ }
818
937
  //
819
938
  // Response Parsing
820
939
  //
@@ -1147,6 +1266,77 @@ class GeminiAdapter extends BaseProviderAdapter {
1147
1266
  });
1148
1267
  return response;
1149
1268
  }
1269
+ async *executeStreamRequest(client, request) {
1270
+ const genAI = client;
1271
+ const geminiRequest = request;
1272
+ // Use generateContentStream for streaming
1273
+ const stream = await genAI.models.generateContentStream({
1274
+ model: geminiRequest.model,
1275
+ contents: geminiRequest.contents,
1276
+ config: geminiRequest.config,
1277
+ });
1278
+ // Track current function call being built
1279
+ let currentFunctionCall = null;
1280
+ // Track usage for final chunk
1281
+ let inputTokens = 0;
1282
+ let outputTokens = 0;
1283
+ let reasoningTokens = 0;
1284
+ const model = geminiRequest.model || this.defaultModel;
1285
+ for await (const chunk of stream) {
1286
+ // Extract text content from the chunk
1287
+ const candidate = chunk.candidates?.[0];
1288
+ if (candidate?.content?.parts) {
1289
+ for (const part of candidate.content.parts) {
1290
+ // Handle text content (excluding thought parts)
1291
+ if (part.text && !part.thought) {
1292
+ yield {
1293
+ type: exports.LlmStreamChunkType.Text,
1294
+ content: part.text,
1295
+ };
1296
+ }
1297
+ // Handle function calls
1298
+ if (part.functionCall) {
1299
+ const functionCall = part.functionCall;
1300
+ currentFunctionCall = {
1301
+ id: functionCall.id || this.generateCallId(),
1302
+ name: functionCall.name || "",
1303
+ arguments: functionCall.args || {},
1304
+ };
1305
+ // Emit the function call immediately
1306
+ yield {
1307
+ type: exports.LlmStreamChunkType.ToolCall,
1308
+ toolCall: {
1309
+ id: currentFunctionCall.id,
1310
+ name: currentFunctionCall.name,
1311
+ arguments: JSON.stringify(currentFunctionCall.arguments),
1312
+ },
1313
+ };
1314
+ currentFunctionCall = null;
1315
+ }
1316
+ }
1317
+ }
1318
+ // Extract usage metadata if present
1319
+ if (chunk.usageMetadata) {
1320
+ inputTokens = chunk.usageMetadata.promptTokenCount || 0;
1321
+ outputTokens = chunk.usageMetadata.candidatesTokenCount || 0;
1322
+ reasoningTokens = chunk.usageMetadata.thoughtsTokenCount || 0;
1323
+ }
1324
+ }
1325
+ // Emit done chunk with final usage
1326
+ yield {
1327
+ type: exports.LlmStreamChunkType.Done,
1328
+ usage: [
1329
+ {
1330
+ input: inputTokens,
1331
+ output: outputTokens,
1332
+ reasoning: reasoningTokens,
1333
+ total: inputTokens + outputTokens,
1334
+ provider: this.name,
1335
+ model,
1336
+ },
1337
+ ],
1338
+ };
1339
+ }
1150
1340
  //
1151
1341
  // Response Parsing
1152
1342
  //
@@ -1612,6 +1802,95 @@ class OpenAiAdapter extends BaseProviderAdapter {
1612
1802
  // @ts-expect-error OpenAI SDK types don't match our request format exactly
1613
1803
  return await openai.responses.create(request);
1614
1804
  }
1805
+ async *executeStreamRequest(client, request) {
1806
+ const openai = client;
1807
+ const baseRequest = request;
1808
+ const streamRequest = {
1809
+ ...baseRequest,
1810
+ stream: true,
1811
+ };
1812
+ const stream = await openai.responses.create(streamRequest);
1813
+ // Track current function call being built
1814
+ let currentFunctionCall = null;
1815
+ // Track usage for final chunk
1816
+ let inputTokens = 0;
1817
+ let outputTokens = 0;
1818
+ let reasoningTokens = 0;
1819
+ const model = baseRequest.model || this.defaultModel;
1820
+ // Cast to async iterable - when stream: true, this is always a Stream<ResponseStreamEvent>
1821
+ const asyncStream = stream;
1822
+ for await (const event of asyncStream) {
1823
+ const eventType = event.type;
1824
+ if (eventType === "response.output_text.delta") {
1825
+ // Text content delta
1826
+ const delta = event.delta;
1827
+ if (delta) {
1828
+ yield {
1829
+ type: exports.LlmStreamChunkType.Text,
1830
+ content: delta,
1831
+ };
1832
+ }
1833
+ }
1834
+ else if (eventType === "response.function_call_arguments.delta") {
1835
+ // Function call arguments delta - accumulate
1836
+ const delta = event.delta;
1837
+ if (delta && currentFunctionCall) {
1838
+ currentFunctionCall.arguments += delta;
1839
+ }
1840
+ }
1841
+ else if (eventType === "response.output_item.added") {
1842
+ // New output item - check if it's a function call
1843
+ const item = event.item;
1844
+ if (item?.type === "function_call") {
1845
+ currentFunctionCall = {
1846
+ id: item.id || "",
1847
+ callId: item.call_id || "",
1848
+ name: item.name || "",
1849
+ arguments: "",
1850
+ };
1851
+ }
1852
+ }
1853
+ else if (eventType === "response.output_item.done") {
1854
+ // Output item completed - emit function call if that's what we were building
1855
+ if (currentFunctionCall) {
1856
+ yield {
1857
+ type: exports.LlmStreamChunkType.ToolCall,
1858
+ toolCall: {
1859
+ id: currentFunctionCall.callId,
1860
+ name: currentFunctionCall.name,
1861
+ arguments: currentFunctionCall.arguments,
1862
+ },
1863
+ };
1864
+ currentFunctionCall = null;
1865
+ }
1866
+ }
1867
+ else if (eventType === "response.completed") {
1868
+ // Response completed - extract final usage
1869
+ const response = event.response;
1870
+ if (response?.usage) {
1871
+ inputTokens = response.usage.input_tokens || 0;
1872
+ outputTokens = response.usage.output_tokens || 0;
1873
+ reasoningTokens = response.usage.output_tokens_details?.reasoning_tokens || 0;
1874
+ }
1875
+ }
1876
+ else if (eventType === "response.done") {
1877
+ // Stream done - emit final chunk with usage
1878
+ yield {
1879
+ type: exports.LlmStreamChunkType.Done,
1880
+ usage: [
1881
+ {
1882
+ input: inputTokens,
1883
+ output: outputTokens,
1884
+ reasoning: reasoningTokens,
1885
+ total: inputTokens + outputTokens,
1886
+ provider: this.name,
1887
+ model,
1888
+ },
1889
+ ],
1890
+ };
1891
+ }
1892
+ }
1893
+ }
1615
1894
  //
1616
1895
  // Response Parsing
1617
1896
  //
@@ -1924,6 +2203,110 @@ class OpenRouterAdapter extends BaseProviderAdapter {
1924
2203
  });
1925
2204
  return response;
1926
2205
  }
2206
+ async *executeStreamRequest(client, request) {
2207
+ const openRouter = client;
2208
+ const openRouterRequest = request;
2209
+ // Use chat.send with stream: true for streaming responses
2210
+ const stream = await openRouter.chat.send({
2211
+ model: openRouterRequest.model,
2212
+ messages: openRouterRequest.messages,
2213
+ tools: openRouterRequest.tools,
2214
+ toolChoice: openRouterRequest.tool_choice,
2215
+ user: openRouterRequest.user,
2216
+ stream: true,
2217
+ });
2218
+ // Track current tool call being built
2219
+ let currentToolCall = null;
2220
+ // Track usage for final chunk
2221
+ let inputTokens = 0;
2222
+ let outputTokens = 0;
2223
+ const model = openRouterRequest.model || this.defaultModel;
2224
+ for await (const chunk of stream) {
2225
+ const typedChunk = chunk;
2226
+ const choices = typedChunk.choices;
2227
+ if (choices && choices.length > 0) {
2228
+ const delta = choices[0].delta;
2229
+ // Handle text content
2230
+ if (delta?.content) {
2231
+ yield {
2232
+ type: exports.LlmStreamChunkType.Text,
2233
+ content: delta.content,
2234
+ };
2235
+ }
2236
+ // Handle tool calls
2237
+ if (delta?.tool_calls && delta.tool_calls.length > 0) {
2238
+ for (const toolCallDelta of delta.tool_calls) {
2239
+ if (toolCallDelta.id) {
2240
+ // New tool call starting
2241
+ if (currentToolCall) {
2242
+ // Emit the previous tool call
2243
+ yield {
2244
+ type: exports.LlmStreamChunkType.ToolCall,
2245
+ toolCall: {
2246
+ id: currentToolCall.id,
2247
+ name: currentToolCall.name,
2248
+ arguments: currentToolCall.arguments,
2249
+ },
2250
+ };
2251
+ }
2252
+ currentToolCall = {
2253
+ id: toolCallDelta.id,
2254
+ name: toolCallDelta.function?.name || "",
2255
+ arguments: toolCallDelta.function?.arguments || "",
2256
+ };
2257
+ }
2258
+ else if (currentToolCall) {
2259
+ // Continuing existing tool call
2260
+ if (toolCallDelta.function?.name) {
2261
+ currentToolCall.name += toolCallDelta.function.name;
2262
+ }
2263
+ if (toolCallDelta.function?.arguments) {
2264
+ currentToolCall.arguments += toolCallDelta.function.arguments;
2265
+ }
2266
+ }
2267
+ }
2268
+ }
2269
+ // Check for finish reason
2270
+ if (choices[0].finish_reason) {
2271
+ // Emit any pending tool call
2272
+ if (currentToolCall) {
2273
+ yield {
2274
+ type: exports.LlmStreamChunkType.ToolCall,
2275
+ toolCall: {
2276
+ id: currentToolCall.id,
2277
+ name: currentToolCall.name,
2278
+ arguments: currentToolCall.arguments,
2279
+ },
2280
+ };
2281
+ currentToolCall = null;
2282
+ }
2283
+ }
2284
+ }
2285
+ // Extract usage if present (usually in the final chunk)
2286
+ if (typedChunk.usage) {
2287
+ inputTokens =
2288
+ typedChunk.usage.prompt_tokens || typedChunk.usage.promptTokens || 0;
2289
+ outputTokens =
2290
+ typedChunk.usage.completion_tokens ||
2291
+ typedChunk.usage.completionTokens ||
2292
+ 0;
2293
+ }
2294
+ }
2295
+ // Emit done chunk with final usage
2296
+ yield {
2297
+ type: exports.LlmStreamChunkType.Done,
2298
+ usage: [
2299
+ {
2300
+ input: inputTokens,
2301
+ output: outputTokens,
2302
+ reasoning: 0,
2303
+ total: inputTokens + outputTokens,
2304
+ provider: this.name,
2305
+ model,
2306
+ },
2307
+ ],
2308
+ };
2309
+ }
1927
2310
  //
1928
2311
  // Response Parsing
1929
2312
  //
@@ -2771,7 +3154,7 @@ class RetryExecutor {
2771
3154
  //
2772
3155
  // Constants
2773
3156
  //
2774
- const ERROR = {
3157
+ const ERROR$1 = {
2775
3158
  BAD_FUNCTION_CALL: "Bad Function Call",
2776
3159
  };
2777
3160
  //
@@ -2808,13 +3191,16 @@ class OperateLoop {
2808
3191
  this.client = config.client;
2809
3192
  this.hookRunnerInstance = config.hookRunner ?? hookRunner;
2810
3193
  this.inputProcessorInstance = config.inputProcessor ?? inputProcessor;
2811
- this.maxRetries = config.maxRetries ?? 6;
2812
3194
  this.retryPolicy = config.retryPolicy ?? defaultRetryPolicy;
2813
3195
  }
2814
3196
  /**
2815
3197
  * Execute the operate loop for multi-turn conversations with tool calling.
2816
3198
  */
2817
3199
  async execute(input, options = {}) {
3200
+ // Log what was passed to operate
3201
+ log$1.trace("[operate] Starting operate loop");
3202
+ log$1.var({ "operate.input": input });
3203
+ log$1.var({ "operate.options": options });
2818
3204
  // Initialize state
2819
3205
  const state = this.initializeState(input, options);
2820
3206
  const context = this.createContext(options);
@@ -2917,6 +3303,9 @@ class OperateLoop {
2917
3303
  });
2918
3304
  // Build provider-specific request
2919
3305
  const providerRequest = this.adapter.buildRequest(request);
3306
+ // Log what was passed to the model
3307
+ log$1.trace("[operate] Calling model");
3308
+ log$1.var({ "operate.request": providerRequest });
2920
3309
  // Execute beforeEachModelRequest hook
2921
3310
  await this.hookRunnerInstance.runBeforeModelRequest(context.hooks, {
2922
3311
  input: state.currentInput,
@@ -2932,6 +3321,9 @@ class OperateLoop {
2932
3321
  },
2933
3322
  hooks: context.hooks,
2934
3323
  });
3324
+ // Log what was returned from the model
3325
+ log$1.trace("[operate] Model response received");
3326
+ log$1.var({ "operate.response": response });
2935
3327
  // Parse response
2936
3328
  const parsed = this.adapter.parseResponse(response, options);
2937
3329
  // Track usage
@@ -3020,7 +3412,7 @@ class OperateLoop {
3020
3412
  state.responseBuilder.setError({
3021
3413
  detail,
3022
3414
  status: jaypieError.status,
3023
- title: ERROR.BAD_FUNCTION_CALL,
3415
+ title: ERROR$1.BAD_FUNCTION_CALL,
3024
3416
  });
3025
3417
  log$1.error(`Error executing function call ${toolCall.name}`);
3026
3418
  log$1.var({ error });
@@ -3151,6 +3543,325 @@ function createOperateLoop(config) {
3151
3543
  return new OperateLoop(config);
3152
3544
  }
3153
3545
 
3546
+ //
3547
+ //
3548
+ // Constants
3549
+ //
3550
+ const ERROR = {
3551
+ BAD_FUNCTION_CALL: "Bad Function Call",
3552
+ };
3553
+ //
3554
+ //
3555
+ // Main
3556
+ //
3557
+ /**
3558
+ * StreamLoop implements streaming multi-turn conversation loop.
3559
+ * It orchestrates provider adapters and tool calling while yielding
3560
+ * stream chunks as they become available.
3561
+ */
3562
+ class StreamLoop {
3563
+ constructor(config) {
3564
+ this.adapter = config.adapter;
3565
+ this.client = config.client;
3566
+ this.hookRunnerInstance = config.hookRunner ?? hookRunner;
3567
+ this.inputProcessorInstance = config.inputProcessor ?? inputProcessor;
3568
+ }
3569
+ /**
3570
+ * Execute the streaming loop for multi-turn conversations with tool calling.
3571
+ * Yields stream chunks as they become available.
3572
+ */
3573
+ async *execute(input, options = {}) {
3574
+ // Verify adapter supports streaming
3575
+ if (!this.adapter.executeStreamRequest) {
3576
+ throw new errors.BadGatewayError(`Provider ${this.adapter.name} does not support streaming`);
3577
+ }
3578
+ // Initialize state
3579
+ const state = this.initializeState(input, options);
3580
+ const context = this.createContext(options);
3581
+ // Build initial request
3582
+ let request = this.buildInitialRequest(state, options);
3583
+ // Multi-turn loop
3584
+ while (state.currentTurn < state.maxTurns) {
3585
+ state.currentTurn++;
3586
+ // Execute one streaming turn
3587
+ const { shouldContinue, toolCalls } = yield* this.executeOneStreamingTurn(request, state, context, options);
3588
+ if (!shouldContinue) {
3589
+ break;
3590
+ }
3591
+ // If we have tool calls, process them
3592
+ if (toolCalls && toolCalls.length > 0 && state.toolkit) {
3593
+ yield* this.processToolCalls(toolCalls, state, context, options);
3594
+ // Check if we've reached max turns
3595
+ if (state.currentTurn >= state.maxTurns) {
3596
+ const error = new errors.TooManyRequestsError();
3597
+ const detail = `Model requested function call but exceeded ${state.maxTurns} turns`;
3598
+ log$1.warn(detail);
3599
+ yield {
3600
+ type: exports.LlmStreamChunkType.Error,
3601
+ error: {
3602
+ detail,
3603
+ status: error.status,
3604
+ title: error.title,
3605
+ },
3606
+ };
3607
+ break;
3608
+ }
3609
+ // Rebuild request with updated history for next turn
3610
+ request = {
3611
+ format: state.formattedFormat,
3612
+ instructions: options.instructions,
3613
+ messages: state.currentInput,
3614
+ model: options.model ?? this.adapter.defaultModel,
3615
+ providerOptions: options.providerOptions,
3616
+ system: options.system,
3617
+ tools: state.formattedTools,
3618
+ user: options.user,
3619
+ };
3620
+ }
3621
+ else {
3622
+ break;
3623
+ }
3624
+ }
3625
+ // Emit final done chunk with accumulated usage
3626
+ yield {
3627
+ type: exports.LlmStreamChunkType.Done,
3628
+ usage: state.usageItems,
3629
+ };
3630
+ }
3631
+ //
3632
+ // Private Methods
3633
+ //
3634
+ initializeState(input, options) {
3635
+ // Process input with placeholders
3636
+ const processedInput = this.inputProcessorInstance.process(input, options);
3637
+ // Determine max turns
3638
+ const maxTurns = maxTurnsFromOptions(options);
3639
+ // Get toolkit
3640
+ let toolkit;
3641
+ if (options.tools) {
3642
+ if (options.tools instanceof Toolkit) {
3643
+ toolkit = options.tools;
3644
+ }
3645
+ else if (Array.isArray(options.tools) && options.tools.length > 0) {
3646
+ const explain = options.explain ?? false;
3647
+ toolkit = new Toolkit(options.tools, { explain });
3648
+ }
3649
+ }
3650
+ // Format output schema through adapter if provided
3651
+ let formattedFormat;
3652
+ if (options.format) {
3653
+ formattedFormat = this.adapter.formatOutputSchema(options.format);
3654
+ }
3655
+ // Format tools through adapter
3656
+ const formattedTools = toolkit
3657
+ ? this.adapter.formatTools(toolkit, formattedFormat)
3658
+ : undefined;
3659
+ return {
3660
+ currentInput: processedInput.history,
3661
+ currentTurn: 0,
3662
+ formattedFormat,
3663
+ formattedTools,
3664
+ maxTurns,
3665
+ toolkit,
3666
+ usageItems: [],
3667
+ };
3668
+ }
3669
+ createContext(options) {
3670
+ return {
3671
+ hooks: options.hooks ?? {},
3672
+ options,
3673
+ };
3674
+ }
3675
+ buildInitialRequest(state, options) {
3676
+ return {
3677
+ format: state.formattedFormat,
3678
+ instructions: options.instructions,
3679
+ messages: state.currentInput,
3680
+ model: options.model ?? this.adapter.defaultModel,
3681
+ providerOptions: options.providerOptions,
3682
+ system: options.system,
3683
+ tools: state.formattedTools,
3684
+ user: options.user,
3685
+ };
3686
+ }
3687
+ async *executeOneStreamingTurn(request, state, context, options) {
3688
+ // Build provider-specific request
3689
+ const providerRequest = this.adapter.buildRequest(request);
3690
+ // Execute beforeEachModelRequest hook
3691
+ await this.hookRunnerInstance.runBeforeModelRequest(context.hooks, {
3692
+ input: state.currentInput,
3693
+ options,
3694
+ providerRequest,
3695
+ });
3696
+ // Collect tool calls from the stream
3697
+ const collectedToolCalls = [];
3698
+ // Execute streaming request
3699
+ const streamGenerator = this.adapter.executeStreamRequest(this.client, providerRequest);
3700
+ for await (const chunk of streamGenerator) {
3701
+ // Pass through text chunks
3702
+ if (chunk.type === exports.LlmStreamChunkType.Text) {
3703
+ yield chunk;
3704
+ }
3705
+ // Collect tool calls
3706
+ if (chunk.type === exports.LlmStreamChunkType.ToolCall) {
3707
+ collectedToolCalls.push({
3708
+ callId: chunk.toolCall.id,
3709
+ name: chunk.toolCall.name,
3710
+ arguments: chunk.toolCall.arguments,
3711
+ raw: chunk.toolCall,
3712
+ });
3713
+ yield chunk;
3714
+ }
3715
+ // Track usage from done chunk (but don't yield it yet - we'll emit our own)
3716
+ if (chunk.type === exports.LlmStreamChunkType.Done && chunk.usage) {
3717
+ state.usageItems.push(...chunk.usage);
3718
+ }
3719
+ // Pass through error chunks
3720
+ if (chunk.type === exports.LlmStreamChunkType.Error) {
3721
+ yield chunk;
3722
+ }
3723
+ }
3724
+ // Execute afterEachModelResponse hook
3725
+ await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
3726
+ content: "",
3727
+ input: state.currentInput,
3728
+ options,
3729
+ providerRequest,
3730
+ providerResponse: null,
3731
+ usage: state.usageItems,
3732
+ });
3733
+ // If we have tool calls and a toolkit, continue the loop
3734
+ if (collectedToolCalls.length > 0 && state.toolkit && state.maxTurns > 1) {
3735
+ // Add tool calls to history
3736
+ for (const toolCall of collectedToolCalls) {
3737
+ state.currentInput.push({
3738
+ type: exports.LlmMessageType.FunctionCall,
3739
+ name: toolCall.name,
3740
+ arguments: toolCall.arguments,
3741
+ call_id: toolCall.callId,
3742
+ id: toolCall.callId,
3743
+ });
3744
+ }
3745
+ return { shouldContinue: true, toolCalls: collectedToolCalls };
3746
+ }
3747
+ return { shouldContinue: false };
3748
+ }
3749
+ async *processToolCalls(toolCalls, state, context, _options) {
3750
+ for (const toolCall of toolCalls) {
3751
+ try {
3752
+ // Execute beforeEachTool hook
3753
+ await this.hookRunnerInstance.runBeforeTool(context.hooks, {
3754
+ args: toolCall.arguments,
3755
+ toolName: toolCall.name,
3756
+ });
3757
+ // Call the tool
3758
+ log$1.trace(`[stream] Calling tool - ${toolCall.name}`);
3759
+ const result = await state.toolkit.call({
3760
+ arguments: toolCall.arguments,
3761
+ name: toolCall.name,
3762
+ });
3763
+ // Execute afterEachTool hook
3764
+ await this.hookRunnerInstance.runAfterTool(context.hooks, {
3765
+ args: toolCall.arguments,
3766
+ result,
3767
+ toolName: toolCall.name,
3768
+ });
3769
+ // Yield tool result chunk
3770
+ yield {
3771
+ type: exports.LlmStreamChunkType.ToolResult,
3772
+ toolResult: {
3773
+ id: toolCall.callId,
3774
+ name: toolCall.name,
3775
+ result,
3776
+ },
3777
+ };
3778
+ // Add tool result to history
3779
+ state.currentInput.push({
3780
+ type: exports.LlmMessageType.FunctionCallOutput,
3781
+ output: JSON.stringify(result),
3782
+ call_id: toolCall.callId,
3783
+ name: toolCall.name,
3784
+ });
3785
+ }
3786
+ catch (error) {
3787
+ // Execute onToolError hook
3788
+ await this.hookRunnerInstance.runOnToolError(context.hooks, {
3789
+ args: toolCall.arguments,
3790
+ error: error,
3791
+ toolName: toolCall.name,
3792
+ });
3793
+ // Yield error chunk
3794
+ const jaypieError = new errors.BadGatewayError();
3795
+ const detail = [
3796
+ `Error executing function call ${toolCall.name}.`,
3797
+ error.message,
3798
+ ].join("\n");
3799
+ yield {
3800
+ type: exports.LlmStreamChunkType.Error,
3801
+ error: {
3802
+ detail,
3803
+ status: jaypieError.status,
3804
+ title: ERROR.BAD_FUNCTION_CALL,
3805
+ },
3806
+ };
3807
+ log$1.error(`Error executing function call ${toolCall.name}`);
3808
+ log$1.var({ error });
3809
+ }
3810
+ }
3811
+ }
3812
+ /**
3813
+ * Convert Gemini contents format to internal history format.
3814
+ */
3815
+ convertGeminiContentsToHistory(contents) {
3816
+ const history = [];
3817
+ for (const content of contents) {
3818
+ if (!content.parts)
3819
+ continue;
3820
+ for (const part of content.parts) {
3821
+ if (part.text && typeof part.text === "string") {
3822
+ history.push({
3823
+ role: content.role === "model"
3824
+ ? exports.LlmMessageRole.Assistant
3825
+ : exports.LlmMessageRole.User,
3826
+ content: part.text,
3827
+ type: exports.LlmMessageType.Message,
3828
+ });
3829
+ }
3830
+ else if (part.functionCall) {
3831
+ const fc = part.functionCall;
3832
+ history.push({
3833
+ type: exports.LlmMessageType.FunctionCall,
3834
+ name: fc.name || "",
3835
+ arguments: JSON.stringify(fc.args || {}),
3836
+ call_id: fc.id || "",
3837
+ id: fc.id || "",
3838
+ });
3839
+ }
3840
+ else if (part.functionResponse) {
3841
+ const fr = part.functionResponse;
3842
+ history.push({
3843
+ type: exports.LlmMessageType.FunctionCallOutput,
3844
+ output: JSON.stringify(fr.response || {}),
3845
+ call_id: "",
3846
+ name: fr.name || "",
3847
+ });
3848
+ }
3849
+ }
3850
+ }
3851
+ return history;
3852
+ }
3853
+ }
3854
+ //
3855
+ //
3856
+ // Factory
3857
+ //
3858
+ /**
3859
+ * Create a StreamLoop instance with the specified configuration.
3860
+ */
3861
+ function createStreamLoop(config) {
3862
+ return new StreamLoop(config);
3863
+ }
3864
+
3154
3865
  // SDK loader with caching
3155
3866
  let cachedSdk$2 = null;
3156
3867
  async function loadSdk$2() {
@@ -3306,6 +4017,17 @@ class AnthropicProvider {
3306
4017
  });
3307
4018
  return this._operateLoop;
3308
4019
  }
4020
+ async getStreamLoop() {
4021
+ if (this._streamLoop) {
4022
+ return this._streamLoop;
4023
+ }
4024
+ const client = await this.getClient();
4025
+ this._streamLoop = createStreamLoop({
4026
+ adapter: anthropicAdapter,
4027
+ client,
4028
+ });
4029
+ return this._streamLoop;
4030
+ }
3309
4031
  // Main send method
3310
4032
  async send(message, options) {
3311
4033
  const client = await this.getClient();
@@ -3342,6 +4064,18 @@ class AnthropicProvider {
3342
4064
  }
3343
4065
  return response;
3344
4066
  }
4067
+ async *stream(input, options = {}) {
4068
+ const streamLoop = await this.getStreamLoop();
4069
+ const mergedOptions = { ...options, model: options.model ?? this.model };
4070
+ // Create a merged history including both the tracked history and any explicitly provided history
4071
+ if (this.conversationHistory.length > 0) {
4072
+ mergedOptions.history = options.history
4073
+ ? [...this.conversationHistory, ...options.history]
4074
+ : [...this.conversationHistory];
4075
+ }
4076
+ // Execute stream loop
4077
+ yield* streamLoop.execute(input, mergedOptions);
4078
+ }
3345
4079
  }
3346
4080
 
3347
4081
  // SDK loader with caching
@@ -3418,6 +4152,17 @@ class GeminiProvider {
3418
4152
  });
3419
4153
  return this._operateLoop;
3420
4154
  }
4155
+ async getStreamLoop() {
4156
+ if (this._streamLoop) {
4157
+ return this._streamLoop;
4158
+ }
4159
+ const client = await this.getClient();
4160
+ this._streamLoop = createStreamLoop({
4161
+ adapter: geminiAdapter,
4162
+ client,
4163
+ });
4164
+ return this._streamLoop;
4165
+ }
3421
4166
  async send(message, options) {
3422
4167
  const client = await this.getClient();
3423
4168
  const { messages } = prepareMessages$2(message, options);
@@ -3472,6 +4217,18 @@ class GeminiProvider {
3472
4217
  }
3473
4218
  return response;
3474
4219
  }
4220
+ async *stream(input, options = {}) {
4221
+ const streamLoop = await this.getStreamLoop();
4222
+ const mergedOptions = { ...options, model: options.model ?? this.model };
4223
+ // Create a merged history including both the tracked history and any explicitly provided history
4224
+ if (this.conversationHistory.length > 0) {
4225
+ mergedOptions.history = options.history
4226
+ ? [...this.conversationHistory, ...options.history]
4227
+ : [...this.conversationHistory];
4228
+ }
4229
+ // Execute stream loop
4230
+ yield* streamLoop.execute(input, mergedOptions);
4231
+ }
3475
4232
  }
3476
4233
 
3477
4234
  // Logger
@@ -3586,6 +4343,17 @@ class OpenAiProvider {
3586
4343
  });
3587
4344
  return this._operateLoop;
3588
4345
  }
4346
+ async getStreamLoop() {
4347
+ if (this._streamLoop) {
4348
+ return this._streamLoop;
4349
+ }
4350
+ const client = await this.getClient();
4351
+ this._streamLoop = createStreamLoop({
4352
+ adapter: openAiAdapter,
4353
+ client,
4354
+ });
4355
+ return this._streamLoop;
4356
+ }
3589
4357
  async send(message, options) {
3590
4358
  const client = await this.getClient();
3591
4359
  const messages = prepareMessages$1(message, options || {});
@@ -3620,6 +4388,18 @@ class OpenAiProvider {
3620
4388
  }
3621
4389
  return response;
3622
4390
  }
4391
+ async *stream(input, options = {}) {
4392
+ const streamLoop = await this.getStreamLoop();
4393
+ const mergedOptions = { ...options, model: options.model ?? this.model };
4394
+ // Create a merged history including both the tracked history and any explicitly provided history
4395
+ if (this.conversationHistory.length > 0) {
4396
+ mergedOptions.history = options.history
4397
+ ? [...this.conversationHistory, ...options.history]
4398
+ : [...this.conversationHistory];
4399
+ }
4400
+ // Execute stream loop
4401
+ yield* streamLoop.execute(input, mergedOptions);
4402
+ }
3623
4403
  }
3624
4404
 
3625
4405
  // SDK loader with caching
@@ -3710,6 +4490,17 @@ class OpenRouterProvider {
3710
4490
  });
3711
4491
  return this._operateLoop;
3712
4492
  }
4493
+ async getStreamLoop() {
4494
+ if (this._streamLoop) {
4495
+ return this._streamLoop;
4496
+ }
4497
+ const client = await this.getClient();
4498
+ this._streamLoop = createStreamLoop({
4499
+ adapter: openRouterAdapter,
4500
+ client,
4501
+ });
4502
+ return this._streamLoop;
4503
+ }
3713
4504
  async send(message, options) {
3714
4505
  const client = await this.getClient();
3715
4506
  const messages = prepareMessages(message, options);
@@ -3759,6 +4550,18 @@ class OpenRouterProvider {
3759
4550
  }
3760
4551
  return response;
3761
4552
  }
4553
+ async *stream(input, options = {}) {
4554
+ const streamLoop = await this.getStreamLoop();
4555
+ const mergedOptions = { ...options, model: options.model ?? this.model };
4556
+ // Create a merged history including both the tracked history and any explicitly provided history
4557
+ if (this.conversationHistory.length > 0) {
4558
+ mergedOptions.history = options.history
4559
+ ? [...this.conversationHistory, ...options.history]
4560
+ : [...this.conversationHistory];
4561
+ }
4562
+ // Execute stream loop
4563
+ yield* streamLoop.execute(input, mergedOptions);
4564
+ }
3762
4565
  }
3763
4566
 
3764
4567
  class Llm {
@@ -3826,6 +4629,12 @@ class Llm {
3826
4629
  }
3827
4630
  return this._llm.operate(input, options);
3828
4631
  }
4632
+ async *stream(input, options = {}) {
4633
+ if (!this._llm.stream) {
4634
+ throw new errors.NotImplementedError(`Provider ${this._provider} does not support stream method`);
4635
+ }
4636
+ yield* this._llm.stream(input, options);
4637
+ }
3829
4638
  static async send(message, options) {
3830
4639
  const { llm, apiKey, model, ...messageOptions } = options || {};
3831
4640
  const instance = new Llm(llm, { apiKey, model });
@@ -3852,6 +4661,27 @@ class Llm {
3852
4661
  const instance = new Llm(finalLlm, { apiKey, model: finalModel });
3853
4662
  return instance.operate(input, operateOptions);
3854
4663
  }
4664
+ static stream(input, options) {
4665
+ const { llm, apiKey, model, ...streamOptions } = options || {};
4666
+ let finalLlm = llm;
4667
+ let finalModel = model;
4668
+ if (!llm && model) {
4669
+ const determined = determineModelProvider(model);
4670
+ if (determined.provider) {
4671
+ finalLlm = determined.provider;
4672
+ }
4673
+ }
4674
+ else if (llm && model) {
4675
+ // When both llm and model are provided, check if they conflict
4676
+ const determined = determineModelProvider(model);
4677
+ if (determined.provider && determined.provider !== llm) {
4678
+ // Don't pass the conflicting model to the constructor
4679
+ finalModel = undefined;
4680
+ }
4681
+ }
4682
+ const instance = new Llm(finalLlm, { apiKey, model: finalModel });
4683
+ return instance.stream(input, streamOptions);
4684
+ }
3855
4685
  }
3856
4686
 
3857
4687
  const random = {