@effect/ai-openrouter 0.8.3 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/Generated.d.ts +19505 -0
  2. package/dist/Generated.d.ts.map +1 -0
  3. package/dist/Generated.js +5115 -0
  4. package/dist/Generated.js.map +1 -0
  5. package/dist/OpenRouterClient.d.ts +116 -0
  6. package/dist/OpenRouterClient.d.ts.map +1 -0
  7. package/dist/OpenRouterClient.js +120 -0
  8. package/dist/OpenRouterClient.js.map +1 -0
  9. package/dist/{dts/OpenRouterConfig.d.ts → OpenRouterConfig.d.ts} +9 -9
  10. package/dist/OpenRouterConfig.d.ts.map +1 -0
  11. package/dist/{esm/OpenRouterConfig.js → OpenRouterConfig.js} +8 -5
  12. package/dist/OpenRouterConfig.js.map +1 -0
  13. package/dist/OpenRouterError.d.ts +83 -0
  14. package/dist/OpenRouterError.d.ts.map +1 -0
  15. package/dist/OpenRouterError.js +10 -0
  16. package/dist/OpenRouterError.js.map +1 -0
  17. package/dist/OpenRouterLanguageModel.d.ts +285 -0
  18. package/dist/OpenRouterLanguageModel.d.ts.map +1 -0
  19. package/dist/OpenRouterLanguageModel.js +1210 -0
  20. package/dist/OpenRouterLanguageModel.js.map +1 -0
  21. package/dist/index.d.ts +29 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +30 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/internal/errors.d.ts +2 -0
  26. package/dist/internal/errors.d.ts.map +1 -0
  27. package/dist/internal/errors.js +347 -0
  28. package/dist/internal/errors.js.map +1 -0
  29. package/dist/{dts/internal → internal}/utilities.d.ts.map +1 -1
  30. package/dist/internal/utilities.js +77 -0
  31. package/dist/internal/utilities.js.map +1 -0
  32. package/package.json +45 -62
  33. package/src/Generated.ts +9312 -5435
  34. package/src/OpenRouterClient.ts +223 -304
  35. package/src/OpenRouterConfig.ts +14 -14
  36. package/src/OpenRouterError.ts +92 -0
  37. package/src/OpenRouterLanguageModel.ts +941 -570
  38. package/src/index.ts +20 -4
  39. package/src/internal/errors.ts +373 -0
  40. package/src/internal/utilities.ts +78 -11
  41. package/Generated/package.json +0 -6
  42. package/OpenRouterClient/package.json +0 -6
  43. package/OpenRouterConfig/package.json +0 -6
  44. package/OpenRouterLanguageModel/package.json +0 -6
  45. package/README.md +0 -5
  46. package/dist/cjs/Generated.js +0 -5813
  47. package/dist/cjs/Generated.js.map +0 -1
  48. package/dist/cjs/OpenRouterClient.js +0 -229
  49. package/dist/cjs/OpenRouterClient.js.map +0 -1
  50. package/dist/cjs/OpenRouterConfig.js +0 -30
  51. package/dist/cjs/OpenRouterConfig.js.map +0 -1
  52. package/dist/cjs/OpenRouterLanguageModel.js +0 -825
  53. package/dist/cjs/OpenRouterLanguageModel.js.map +0 -1
  54. package/dist/cjs/index.js +0 -16
  55. package/dist/cjs/index.js.map +0 -1
  56. package/dist/cjs/internal/utilities.js +0 -29
  57. package/dist/cjs/internal/utilities.js.map +0 -1
  58. package/dist/dts/Generated.d.ts +0 -11026
  59. package/dist/dts/Generated.d.ts.map +0 -1
  60. package/dist/dts/OpenRouterClient.d.ts +0 -407
  61. package/dist/dts/OpenRouterClient.d.ts.map +0 -1
  62. package/dist/dts/OpenRouterConfig.d.ts.map +0 -1
  63. package/dist/dts/OpenRouterLanguageModel.d.ts +0 -215
  64. package/dist/dts/OpenRouterLanguageModel.d.ts.map +0 -1
  65. package/dist/dts/index.d.ts +0 -17
  66. package/dist/dts/index.d.ts.map +0 -1
  67. package/dist/esm/Generated.js +0 -5457
  68. package/dist/esm/Generated.js.map +0 -1
  69. package/dist/esm/OpenRouterClient.js +0 -214
  70. package/dist/esm/OpenRouterClient.js.map +0 -1
  71. package/dist/esm/OpenRouterConfig.js.map +0 -1
  72. package/dist/esm/OpenRouterLanguageModel.js +0 -814
  73. package/dist/esm/OpenRouterLanguageModel.js.map +0 -1
  74. package/dist/esm/index.js +0 -17
  75. package/dist/esm/index.js.map +0 -1
  76. package/dist/esm/internal/utilities.js +0 -21
  77. package/dist/esm/internal/utilities.js.map +0 -1
  78. package/dist/esm/package.json +0 -4
  79. package/index/package.json +0 -6
  80. /package/dist/{dts/internal → internal}/utilities.d.ts +0 -0
@@ -1,814 +0,0 @@
1
- /**
2
- * @since 1.0.0
3
- */
4
- import * as AiError from "@effect/ai/AiError";
5
- import * as LanguageModel from "@effect/ai/LanguageModel";
6
- import * as AiModel from "@effect/ai/Model";
7
- import { addGenAIAnnotations } from "@effect/ai/Telemetry";
8
- import * as Tool from "@effect/ai/Tool";
9
- import * as Arr from "effect/Array";
10
- import * as Context from "effect/Context";
11
- import * as DateTime from "effect/DateTime";
12
- import * as Effect from "effect/Effect";
13
- import * as Encoding from "effect/Encoding";
14
- import { dual } from "effect/Function";
15
- import * as Layer from "effect/Layer";
16
- import * as Predicate from "effect/Predicate";
17
- import * as Stream from "effect/Stream";
18
- import * as InternalUtilities from "./internal/utilities.js";
19
- import { OpenRouterClient } from "./OpenRouterClient.js";
20
- // =============================================================================
21
- // Configuration
22
- // =============================================================================
23
- /**
24
- * @since 1.0.0
25
- * @category Context
26
- */
27
- export class Config extends /*#__PURE__*/Context.Tag("@effect/ai-openrouter/OpenRouterLanguageModel/Config")() {
28
- /**
29
- * @since 1.0.0
30
- */
31
- static getOrUndefined = /*#__PURE__*/Effect.map(/*#__PURE__*/Effect.context(), context => context.unsafeMap.get(Config.key));
32
- }
33
- // =============================================================================
34
- // OpenRouter Language Model
35
- // =============================================================================
36
- /**
37
- * @since 1.0.0
38
- * @category Ai Models
39
- */
40
- export const model = (model, config) => AiModel.make("openrouter", layer({
41
- model,
42
- config
43
- }));
44
- /**
45
- * @since 1.0.0
46
- * @category Constructors
47
- */
48
- export const make = /*#__PURE__*/Effect.fnUntraced(function* (options) {
49
- const client = yield* OpenRouterClient;
50
- const makeRequest = Effect.fnUntraced(function* (providerOptions) {
51
- const context = yield* Effect.context();
52
- const config = {
53
- model: options.model,
54
- ...options.config,
55
- ...context.unsafeMap.get(Config.key)
56
- };
57
- const messages = yield* prepareMessages(providerOptions);
58
- const {
59
- toolChoice,
60
- tools
61
- } = yield* prepareTools(providerOptions);
62
- const responseFormat = providerOptions.responseFormat;
63
- const request = {
64
- ...config,
65
- messages,
66
- tools,
67
- tool_choice: toolChoice,
68
- response_format: responseFormat.type === "text" ? undefined : {
69
- type: "json_schema",
70
- json_schema: {
71
- name: responseFormat.objectName,
72
- description: Tool.getDescriptionFromSchemaAst(responseFormat.schema.ast) ?? "Respond with a JSON object",
73
- schema: Tool.getJsonSchemaFromSchemaAst(responseFormat.schema.ast),
74
- strict: true
75
- }
76
- }
77
- };
78
- return request;
79
- });
80
- return yield* LanguageModel.make({
81
- generateText: Effect.fnUntraced(function* (options) {
82
- const request = yield* makeRequest(options);
83
- annotateRequest(options.span, request);
84
- const rawResponse = yield* client.createChatCompletion(request);
85
- annotateResponse(options.span, rawResponse);
86
- return yield* makeResponse(rawResponse);
87
- }),
88
- streamText: Effect.fnUntraced(function* (options) {
89
- const request = yield* makeRequest(options);
90
- annotateRequest(options.span, request);
91
- return client.createChatCompletionStream(request);
92
- }, (effect, options) => effect.pipe(Effect.flatMap(stream => makeStreamResponse(stream)), Stream.unwrap, Stream.map(response => {
93
- annotateStreamResponse(options.span, response);
94
- return response;
95
- })))
96
- });
97
- });
98
- /**
99
- * @since 1.0.0
100
- * @category Layers
101
- */
102
- export const layer = options => Layer.effect(LanguageModel.LanguageModel, make({
103
- model: options.model,
104
- config: options.config
105
- }));
106
- /**
107
- * @since 1.0.0
108
- * @category Configuration
109
- */
110
- export const withConfigOverride = /*#__PURE__*/dual(2, (self, overrides) => Effect.flatMap(Config.getOrUndefined, config => Effect.provideService(self, Config, {
111
- ...config,
112
- ...overrides
113
- })));
114
- // =============================================================================
115
- // Prompt Conversion
116
- // =============================================================================
117
- const prepareMessages = /*#__PURE__*/Effect.fnUntraced(function* (options) {
118
- const messages = [];
119
- for (const message of options.prompt.content) {
120
- switch (message.role) {
121
- case "system":
122
- {
123
- messages.push({
124
- role: "system",
125
- content: message.content,
126
- cache_control: getCacheControl(message)
127
- });
128
- break;
129
- }
130
- case "user":
131
- {
132
- if (message.content.length === 1 && message.content[0].type === "text") {
133
- const part = message.content[0];
134
- const cacheControl = getCacheControl(message) ?? getCacheControl(part);
135
- messages.push({
136
- role: "user",
137
- content: Predicate.isNotUndefined(cacheControl) ? [{
138
- type: "text",
139
- text: part.text,
140
- cache_control: cacheControl
141
- }] : part.text
142
- });
143
- } else {
144
- const content = [];
145
- const messageCacheControl = getCacheControl(message);
146
- for (const part of message.content) {
147
- const partCacheControl = getCacheControl(part);
148
- const cacheControl = partCacheControl ?? messageCacheControl;
149
- switch (part.type) {
150
- case "text":
151
- {
152
- content.push({
153
- type: "text",
154
- text: part.text,
155
- cache_control: cacheControl
156
- });
157
- break;
158
- }
159
- case "file":
160
- {
161
- if (part.mediaType.startsWith("image/")) {
162
- const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType;
163
- content.push({
164
- type: "image_url",
165
- image_url: {
166
- url: part.data instanceof URL ? part.data.toString() : part.data instanceof Uint8Array ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}` : part.data
167
- },
168
- cache_control: cacheControl
169
- });
170
- } else {
171
- const options = part.options.openrouter;
172
- const fileName = options?.fileName ?? part.fileName ?? "";
173
- content.push({
174
- type: "file",
175
- file: {
176
- filename: fileName,
177
- file_data: part.data instanceof URL ? part.data.toString() : part.data instanceof Uint8Array ? `data:${part.mediaType};base64,${Encoding.encodeBase64(part.data)}` : part.data
178
- },
179
- cache_control: part.data instanceof URL ? cacheControl : undefined
180
- });
181
- }
182
- break;
183
- }
184
- }
185
- }
186
- messages.push({
187
- role: "user",
188
- content
189
- });
190
- }
191
- break;
192
- }
193
- case "assistant":
194
- {
195
- let text = "";
196
- let reasoning = "";
197
- const reasoningDetails = [];
198
- const toolCalls = [];
199
- const cacheControl = getCacheControl(message);
200
- for (const part of message.content) {
201
- switch (part.type) {
202
- case "text":
203
- {
204
- text += part.text;
205
- break;
206
- }
207
- case "reasoning":
208
- {
209
- reasoning += part.text;
210
- reasoningDetails.push({
211
- type: "reasoning.text",
212
- text: part.text
213
- });
214
- break;
215
- }
216
- case "tool-call":
217
- {
218
- toolCalls.push({
219
- id: part.id,
220
- type: "function",
221
- function: {
222
- name: part.name,
223
- arguments: JSON.stringify(part.params)
224
- }
225
- });
226
- break;
227
- }
228
- }
229
- }
230
- messages.push({
231
- role: "assistant",
232
- content: text,
233
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
234
- reasoning: reasoning.length > 0 ? reasoning : undefined,
235
- reasoning_details: reasoningDetails.length > 0 ? reasoningDetails : undefined,
236
- cache_control: cacheControl
237
- });
238
- break;
239
- }
240
- case "tool":
241
- {
242
- const cacheControl = getCacheControl(message);
243
- for (const part of message.content) {
244
- messages.push({
245
- role: "tool",
246
- tool_call_id: part.id,
247
- content: JSON.stringify(part.result),
248
- cache_control: cacheControl
249
- });
250
- }
251
- break;
252
- }
253
- }
254
- }
255
- return messages;
256
- });
257
- // =============================================================================
258
- // Tool Conversion
259
- // =============================================================================
260
- const prepareTools = /*#__PURE__*/Effect.fnUntraced(function* (options) {
261
- if (options.tools.length === 0) {
262
- return {
263
- tools: undefined,
264
- toolChoice: undefined
265
- };
266
- }
267
- const hasProviderDefinedTools = options.tools.some(tool => Tool.isProviderDefined(tool));
268
- if (hasProviderDefinedTools) {
269
- return yield* new AiError.MalformedInput({
270
- module: "OpenRouterLanguageModel",
271
- method: "prepareTools",
272
- description: "Provider-defined tools are unsupported by the OpenRouter " + "provider integration at this time"
273
- });
274
- }
275
- let tools = [];
276
- let toolChoice = undefined;
277
- for (const tool of options.tools) {
278
- tools.push({
279
- type: "function",
280
- function: {
281
- name: tool.name,
282
- description: Tool.getDescription(tool),
283
- parameters: Tool.getJsonSchema(tool),
284
- strict: true
285
- }
286
- });
287
- }
288
- if (options.toolChoice === "none") {
289
- toolChoice = "none";
290
- } else if (options.toolChoice === "auto") {
291
- toolChoice = "auto";
292
- } else if (options.toolChoice === "required") {
293
- toolChoice = "required";
294
- } else if ("tool" in options.toolChoice) {
295
- toolChoice = {
296
- type: "function",
297
- function: {
298
- name: options.toolChoice.tool
299
- }
300
- };
301
- } else {
302
- const allowedTools = new Set(options.toolChoice.oneOf);
303
- tools = tools.filter(tool => allowedTools.has(tool.function.name));
304
- toolChoice = options.toolChoice.mode === "auto" ? "auto" : "required";
305
- }
306
- return {
307
- tools,
308
- toolChoice
309
- };
310
- });
311
- // =============================================================================
312
- // Response Conversion
313
- // =============================================================================
314
- const makeResponse = /*#__PURE__*/Effect.fnUntraced(function* (response) {
315
- const choice = response.choices[0];
316
- if (Predicate.isUndefined(choice)) {
317
- return yield* new AiError.MalformedOutput({
318
- module: "OpenRouterLanguageModel",
319
- method: "makeResponse",
320
- description: "Received response with no valid choices"
321
- });
322
- }
323
- const parts = [];
324
- const message = choice.message;
325
- const createdAt = new Date(response.created * 1000);
326
- parts.push({
327
- type: "response-metadata",
328
- id: response.id,
329
- modelId: response.model,
330
- timestamp: DateTime.formatIso(DateTime.unsafeFromDate(createdAt))
331
- });
332
- if (Predicate.isNotNullable(message.reasoning) && message.reasoning.length > 0) {
333
- parts.push({
334
- type: "reasoning",
335
- text: message.reasoning
336
- });
337
- }
338
- if (Predicate.isNotNullable(message.reasoning_details) && message.reasoning_details.length > 0) {
339
- for (const detail of message.reasoning_details) {
340
- switch (detail.type) {
341
- case "reasoning.summary":
342
- {
343
- if (Predicate.isNotUndefined(detail.summary) && detail.summary.length > 0) {
344
- parts.push({
345
- type: "reasoning",
346
- text: detail.summary
347
- });
348
- }
349
- break;
350
- }
351
- case "reasoning.encrypted":
352
- {
353
- if (Predicate.isNotUndefined(detail.data) && detail.data.length > 0) {
354
- parts.push({
355
- type: "reasoning",
356
- text: "",
357
- metadata: {
358
- openrouter: {
359
- type: "encrypted_reasoning",
360
- format: detail.format,
361
- redactedData: detail.data
362
- }
363
- }
364
- });
365
- }
366
- break;
367
- }
368
- case "reasoning.text":
369
- {
370
- if (Predicate.isNotUndefined(detail.text) && detail.text.length > 0) {
371
- parts.push({
372
- type: "reasoning",
373
- text: detail.text,
374
- metadata: {
375
- openrouter: {
376
- type: "reasoning",
377
- signature: detail.signature
378
- }
379
- }
380
- });
381
- }
382
- break;
383
- }
384
- }
385
- }
386
- }
387
- if (Predicate.isNotNullable(message.content) && message.content.length > 0) {
388
- parts.push({
389
- type: "text",
390
- text: message.content
391
- });
392
- }
393
- if (Predicate.isNotNullable(message.tool_calls)) {
394
- for (const toolCall of message.tool_calls) {
395
- const toolName = toolCall.function.name;
396
- const toolParams = toolCall.function.arguments;
397
- const params = yield* Effect.try({
398
- try: () => Tool.unsafeSecureJsonParse(toolParams),
399
- catch: cause => new AiError.MalformedOutput({
400
- module: "OpenRouterLanguageModel",
401
- method: "makeResponse",
402
- description: "Failed to securely parse tool call parameters " + `for tool '${toolName}':\nParameters: ${toolParams}`,
403
- cause
404
- })
405
- });
406
- parts.push({
407
- type: "tool-call",
408
- id: toolCall.id,
409
- name: toolName,
410
- params
411
- });
412
- }
413
- }
414
- if (Predicate.isNotNullable(message.annotations)) {
415
- for (const annotation of message.annotations) {
416
- if (annotation.type === "url_citation") {
417
- parts.push({
418
- type: "source",
419
- sourceType: "url",
420
- id: annotation.url_citation.url,
421
- url: annotation.url_citation.url,
422
- title: annotation.url_citation.title,
423
- metadata: {
424
- openrouter: {
425
- content: annotation.url_citation.content
426
- }
427
- }
428
- });
429
- }
430
- }
431
- }
432
- if (Predicate.isNotNullable(message.images)) {
433
- for (const image of message.images) {
434
- parts.push({
435
- type: "file",
436
- mediaType: getMediaType(image.image_url.url) ?? "image/jpeg",
437
- data: getBase64FromDataUrl(image.image_url.url)
438
- });
439
- }
440
- }
441
- parts.push({
442
- type: "finish",
443
- reason: InternalUtilities.resolveFinishReason(choice.finish_reason),
444
- usage: {
445
- inputTokens: response.usage?.prompt_tokens,
446
- outputTokens: response.usage?.completion_tokens,
447
- totalTokens: response.usage?.total_tokens,
448
- reasoningTokens: response.usage?.completion_tokens_details?.reasoning_tokens,
449
- cachedInputTokens: response.usage?.prompt_tokens_details?.cached_tokens
450
- },
451
- metadata: {
452
- openrouter: {
453
- provider: response.provider,
454
- usage: {
455
- cost: response.usage?.cost,
456
- promptTokensDetails: response.usage?.prompt_tokens_details,
457
- completionTokensDetails: response.usage?.completion_tokens_details,
458
- costDetails: response.usage?.cost_details
459
- }
460
- }
461
- }
462
- });
463
- return parts;
464
- });
465
- const makeStreamResponse = /*#__PURE__*/Effect.fnUntraced(function* (stream) {
466
- let idCounter = 0;
467
- let activeTextId = undefined;
468
- let activeReasoningId = undefined;
469
- let finishReason = "unknown";
470
- let responseMetadataEmitted = false;
471
- const activeToolCalls = {};
472
- return stream.pipe(Stream.mapEffect(Effect.fnUntraced(function* (event) {
473
- const parts = [];
474
- if ("error" in event) {
475
- parts.push({
476
- type: "error",
477
- error: event.error
478
- });
479
- return parts;
480
- }
481
- // Response Metadata
482
- if (Predicate.isNotUndefined(event.id) && !responseMetadataEmitted) {
483
- parts.push({
484
- type: "response-metadata",
485
- id: event.id,
486
- modelId: event.model,
487
- timestamp: DateTime.formatIso(yield* DateTime.now)
488
- });
489
- responseMetadataEmitted = true;
490
- }
491
- const choice = event.choices[0];
492
- if (Predicate.isUndefined(choice)) {
493
- return yield* new AiError.MalformedOutput({
494
- module: "OpenRouterLanguageModel",
495
- method: "makeResponse",
496
- description: "Received response with no valid choices"
497
- });
498
- }
499
- const delta = choice.delta;
500
- if (Predicate.isUndefined(delta)) {
501
- return parts;
502
- }
503
- // Reasoning Parts
504
- const emitReasoningPart = (delta, metadata = undefined) => {
505
- // End in-progress text part if present before starting reasoning
506
- if (Predicate.isNotUndefined(activeTextId)) {
507
- parts.push({
508
- type: "text-end",
509
- id: activeTextId
510
- });
511
- activeTextId = undefined;
512
- }
513
- // Start a new reasoning part if necessary
514
- if (Predicate.isUndefined(activeReasoningId)) {
515
- activeReasoningId = (idCounter++).toString();
516
- parts.push({
517
- type: "reasoning-start",
518
- id: activeReasoningId,
519
- metadata: {
520
- openrouter: metadata
521
- }
522
- });
523
- }
524
- // Emit the reasoning delta
525
- parts.push({
526
- type: "reasoning-delta",
527
- id: activeReasoningId,
528
- delta,
529
- metadata: {
530
- openrouter: metadata
531
- }
532
- });
533
- };
534
- if (Predicate.isNotNullable(delta.reasoning_details) && delta.reasoning_details.length > 0) {
535
- for (const detail of delta.reasoning_details) {
536
- switch (detail.type) {
537
- case "reasoning.summary":
538
- {
539
- if (Predicate.isNotUndefined(detail.summary) && detail.summary.length > 0) {
540
- emitReasoningPart(detail.summary);
541
- }
542
- break;
543
- }
544
- case "reasoning.encrypted":
545
- {
546
- if (Predicate.isNotUndefined(detail.data) && detail.data.length > 0) {
547
- emitReasoningPart("", {
548
- type: "encrypted_reasoning",
549
- format: detail.format,
550
- redactedData: detail.data
551
- });
552
- }
553
- break;
554
- }
555
- case "reasoning.text":
556
- {
557
- if (Predicate.isNotUndefined(detail.text) && detail.text.length > 0) {
558
- emitReasoningPart(detail.text, {
559
- type: "reasoning",
560
- signature: detail.signature
561
- });
562
- }
563
- break;
564
- }
565
- }
566
- }
567
- } else if (Predicate.isNotNullable(delta.reasoning) && delta.reasoning.length > 0) {
568
- emitReasoningPart(delta.reasoning);
569
- }
570
- // Text Parts
571
- if (Predicate.isNotNullable(delta.content) && delta.content.length > 0) {
572
- // End in-progress reasoning part if present before starting text
573
- if (Predicate.isNotUndefined(activeReasoningId)) {
574
- parts.push({
575
- type: "reasoning-end",
576
- id: activeReasoningId
577
- });
578
- activeReasoningId = undefined;
579
- }
580
- // Start a new text part if necessary
581
- if (Predicate.isUndefined(activeTextId)) {
582
- activeTextId = (idCounter++).toString();
583
- parts.push({
584
- type: "text-start",
585
- id: activeTextId
586
- });
587
- }
588
- // Emit the text delta
589
- parts.push({
590
- type: "text-delta",
591
- id: activeTextId,
592
- delta: delta.content
593
- });
594
- }
595
- // Source Parts
596
- if (Predicate.isNotNullable(delta.annotations)) {
597
- for (const annotation of delta.annotations) {
598
- if (annotation.type === "url_citation") {
599
- parts.push({
600
- type: "source",
601
- sourceType: "url",
602
- id: annotation.url_citation.url,
603
- url: annotation.url_citation.url,
604
- title: annotation.url_citation.title,
605
- metadata: {
606
- openrouter: {
607
- content: annotation.url_citation.content
608
- }
609
- }
610
- });
611
- }
612
- }
613
- }
614
- // Tool Call Parts
615
- if (Predicate.isNotNullable(delta.tool_calls) && delta.tool_calls.length > 0) {
616
- for (const toolCall of delta.tool_calls) {
617
- // Get the active tool call, if present
618
- let activeToolCall = activeToolCalls[toolCall.index];
619
- // If no active tool call was found, start a new active tool call
620
- if (Predicate.isUndefined(activeToolCall)) {
621
- // The tool call id and function name always come back with the
622
- // first tool call delta
623
- activeToolCall = {
624
- index: toolCall.index,
625
- id: toolCall.id,
626
- name: toolCall.function.name,
627
- params: toolCall.function.arguments ?? ""
628
- };
629
- activeToolCalls[toolCall.index] = activeToolCall;
630
- parts.push({
631
- type: "tool-params-start",
632
- id: activeToolCall.id,
633
- name: activeToolCall.name
634
- });
635
- // Emit a tool call delta part if parameters were also sent
636
- if (activeToolCall.params.length > 0) {
637
- parts.push({
638
- type: "tool-params-delta",
639
- id: activeToolCall.id,
640
- delta: activeToolCall.params
641
- });
642
- }
643
- } else {
644
- // If an active tool call was found, update and emit the delta for
645
- // the tool call's parameters
646
- activeToolCall.params += toolCall.function.arguments;
647
- parts.push({
648
- type: "tool-params-delta",
649
- id: activeToolCall.id,
650
- delta: activeToolCall.params
651
- });
652
- }
653
- // Check if the tool call is complete
654
- try {
655
- const params = Tool.unsafeSecureJsonParse(activeToolCall.params);
656
- parts.push({
657
- type: "tool-params-end",
658
- id: activeToolCall.id
659
- });
660
- parts.push({
661
- type: "tool-call",
662
- id: activeToolCall.id,
663
- name: activeToolCall.name,
664
- params
665
- });
666
- delete activeToolCalls[toolCall.index];
667
- } catch {
668
- // Tool call incomplete, continue parsing
669
- continue;
670
- }
671
- }
672
- }
673
- // File Parts
674
- if (Predicate.isNotNullable(delta.images)) {
675
- for (const image of delta.images) {
676
- parts.push({
677
- type: "file",
678
- mediaType: getMediaType(image.image_url.url) ?? "image/jpeg",
679
- data: getBase64FromDataUrl(image.image_url.url)
680
- });
681
- }
682
- }
683
- // Finish Parts
684
- if (Predicate.isNotNullable(choice.finish_reason)) {
685
- finishReason = InternalUtilities.resolveFinishReason(choice.finish_reason);
686
- }
687
- // Usage is only emitted by the last part of the stream, so we need to
688
- // handle flushing any remaining text / reasoning / tool calls
689
- if (Predicate.isNotUndefined(event.usage)) {
690
- // Complete any remaining tool calls if the finish reason is tool-calls
691
- if (finishReason === "tool-calls") {
692
- for (const toolCall of Object.values(activeToolCalls)) {
693
- // Coerce invalid tool call parameters to an empty object
694
- const params = yield* Effect.try(() => Tool.unsafeSecureJsonParse(toolCall.params)).pipe(Effect.catchAll(() => Effect.succeed({})));
695
- parts.push({
696
- type: "tool-params-end",
697
- id: toolCall.id
698
- });
699
- parts.push({
700
- type: "tool-call",
701
- id: toolCall.id,
702
- name: toolCall.name,
703
- params
704
- });
705
- delete activeToolCalls[toolCall.index];
706
- }
707
- }
708
- // Flush remaining reasoning parts
709
- if (Predicate.isNotUndefined(activeReasoningId)) {
710
- parts.push({
711
- type: "reasoning-end",
712
- id: activeReasoningId
713
- });
714
- activeReasoningId = undefined;
715
- }
716
- // Flush remaining text parts
717
- if (Predicate.isNotUndefined(activeTextId)) {
718
- parts.push({
719
- type: "text-end",
720
- id: activeTextId
721
- });
722
- activeTextId = undefined;
723
- }
724
- parts.push({
725
- type: "finish",
726
- reason: finishReason,
727
- usage: {
728
- inputTokens: event.usage?.prompt_tokens,
729
- outputTokens: event.usage?.completion_tokens,
730
- totalTokens: event.usage?.total_tokens,
731
- reasoningTokens: event.usage?.completion_tokens_details?.reasoning_tokens,
732
- cachedInputTokens: event.usage?.prompt_tokens_details?.cached_tokens
733
- },
734
- metadata: {
735
- openrouter: {
736
- provider: event.provider,
737
- usage: {
738
- cost: event.usage?.cost,
739
- promptTokensDetails: event.usage?.prompt_tokens_details,
740
- completionTokensDetails: event.usage?.completion_tokens_details,
741
- costDetails: event.usage?.cost_details
742
- }
743
- }
744
- }
745
- });
746
- }
747
- return parts;
748
- })), Stream.flattenIterables);
749
- });
750
- // =============================================================================
751
- // Telemetry
752
- // =============================================================================
753
- const annotateRequest = (span, request) => {
754
- addGenAIAnnotations(span, {
755
- system: "openrouter",
756
- operation: {
757
- name: "chat"
758
- },
759
- request: {
760
- model: request.model,
761
- temperature: request.temperature,
762
- topP: request.top_p,
763
- maxTokens: request.max_tokens,
764
- stopSequences: Arr.ensure(request.stop).filter(Predicate.isNotNullable)
765
- }
766
- });
767
- };
768
- const annotateResponse = (span, response) => {
769
- addGenAIAnnotations(span, {
770
- response: {
771
- id: response.id,
772
- model: response.model,
773
- finishReasons: response.choices.map(choice => choice.finish_reason).filter(Predicate.isNotNullable)
774
- },
775
- usage: {
776
- inputTokens: response.usage?.prompt_tokens,
777
- outputTokens: response.usage?.completion_tokens
778
- }
779
- });
780
- };
781
- const annotateStreamResponse = (span, part) => {
782
- if (part.type === "response-metadata") {
783
- addGenAIAnnotations(span, {
784
- response: {
785
- id: part.id,
786
- model: part.modelId
787
- }
788
- });
789
- }
790
- if (part.type === "finish") {
791
- addGenAIAnnotations(span, {
792
- response: {
793
- finishReasons: [part.reason]
794
- },
795
- usage: {
796
- inputTokens: part.usage.inputTokens,
797
- outputTokens: part.usage.outputTokens
798
- }
799
- });
800
- }
801
- };
802
- // =============================================================================
803
- // Utilities
804
- // =============================================================================
805
- const getCacheControl = part => part.options.openrouter?.cacheControl;
806
- const getMediaType = dataUrl => {
807
- const match = dataUrl.match(/^data:([^;]+)/);
808
- return match ? match[1] : undefined;
809
- };
810
- const getBase64FromDataUrl = dataUrl => {
811
- const match = dataUrl.match(/^data:[^;]*;base64,(.+)$/);
812
- return match ? match[1] : dataUrl;
813
- };
814
- //# sourceMappingURL=OpenRouterLanguageModel.js.map