@effect/ai-openrouter 4.0.0-beta.7 → 4.0.0-beta.71
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.
- package/dist/Generated.d.ts +171 -92
- package/dist/Generated.d.ts.map +1 -1
- package/dist/Generated.js +60 -31
- package/dist/Generated.js.map +1 -1
- package/dist/OpenRouterClient.d.ts +101 -17
- package/dist/OpenRouterClient.d.ts.map +1 -1
- package/dist/OpenRouterClient.js +62 -11
- package/dist/OpenRouterClient.js.map +1 -1
- package/dist/OpenRouterConfig.d.ts +121 -10
- package/dist/OpenRouterConfig.d.ts.map +1 -1
- package/dist/OpenRouterConfig.js +71 -7
- package/dist/OpenRouterConfig.js.map +1 -1
- package/dist/OpenRouterError.d.ts +166 -35
- package/dist/OpenRouterError.d.ts.map +1 -1
- package/dist/OpenRouterError.js +1 -1
- package/dist/OpenRouterLanguageModel.d.ts +368 -20
- package/dist/OpenRouterLanguageModel.d.ts.map +1 -1
- package/dist/OpenRouterLanguageModel.js +344 -241
- package/dist/OpenRouterLanguageModel.js.map +1 -1
- package/dist/index.d.ts +6 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -11
- package/dist/index.js.map +1 -1
- package/dist/internal/errors.js +4 -4
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/utilities.js +0 -1
- package/dist/internal/utilities.js.map +1 -1
- package/package.json +3 -3
- package/src/Generated.ts +123 -53
- package/src/OpenRouterClient.ts +102 -18
- package/src/OpenRouterConfig.ts +166 -13
- package/src/OpenRouterError.ts +168 -35
- package/src/OpenRouterLanguageModel.ts +644 -246
- package/src/index.ts +6 -11
- package/src/internal/errors.ts +6 -4
- package/src/internal/utilities.ts +0 -1
|
@@ -1,18 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The `OpenRouterLanguageModel` module provides the OpenRouter implementation
|
|
3
|
+
* of Effect AI's `LanguageModel` service. It translates provider-neutral
|
|
4
|
+
* prompts, tools, files, structured output requests, reasoning metadata,
|
|
5
|
+
* cache-control hints, and telemetry annotations into OpenRouter chat
|
|
6
|
+
* completion requests, then converts responses and streams back into Effect AI
|
|
7
|
+
* response parts.
|
|
8
|
+
*
|
|
9
|
+
* **Mental model**
|
|
10
|
+
*
|
|
11
|
+
* `OpenRouterClient` owns HTTP transport and authentication. This module owns
|
|
12
|
+
* protocol translation: message assembly, tool conversion, structured output
|
|
13
|
+
* codec selection, streaming chunk handling, OpenRouter metadata round-trips,
|
|
14
|
+
* and GenAI telemetry annotations. {@link model}, {@link layer}, and
|
|
15
|
+
* {@link make} all build the same OpenRouter-backed
|
|
16
|
+
* `LanguageModel.LanguageModel` service from a model id and optional request
|
|
17
|
+
* defaults.
|
|
18
|
+
*
|
|
19
|
+
* **Common tasks**
|
|
20
|
+
*
|
|
21
|
+
* - Create an OpenRouter model descriptor for `Effect.provide`: {@link model}
|
|
22
|
+
* - Provide `LanguageModel.LanguageModel` as a `Layer`: {@link layer}
|
|
23
|
+
* - Construct the service effectfully from an existing `OpenRouterClient`:
|
|
24
|
+
* {@link make}
|
|
25
|
+
* - Supply or scope OpenRouter request defaults: {@link Config},
|
|
26
|
+
* {@link withConfigOverride}
|
|
27
|
+
* - Preserve OpenRouter reasoning and file metadata across turns:
|
|
28
|
+
* {@link ReasoningDetails}, {@link FileAnnotation}
|
|
29
|
+
*
|
|
30
|
+
* **Gotchas**
|
|
31
|
+
*
|
|
32
|
+
* - OpenRouter routes to many underlying providers, so support for images,
|
|
33
|
+
* files, tools, structured outputs, caching, and reasoning metadata depends
|
|
34
|
+
* on the selected model and route.
|
|
35
|
+
* - Provider-specific prompt and response metadata lives under the `openrouter`
|
|
36
|
+
* option namespace so later requests can replay reasoning details and file
|
|
37
|
+
* annotations when the model supports them.
|
|
38
|
+
* - Provider-defined tools are not supported by this integration; requests that
|
|
39
|
+
* include them fail before reaching OpenRouter.
|
|
40
|
+
*
|
|
41
|
+
* @since 4.0.0
|
|
3
42
|
*/
|
|
4
43
|
/** @effect-diagnostics preferSchemaOverJson:skip-file */
|
|
5
44
|
import * as Arr from "effect/Array"
|
|
45
|
+
import * as Context from "effect/Context"
|
|
6
46
|
import * as DateTime from "effect/DateTime"
|
|
7
47
|
import * as Effect from "effect/Effect"
|
|
8
48
|
import * as Encoding from "effect/Encoding"
|
|
9
49
|
import { dual } from "effect/Function"
|
|
10
50
|
import * as Layer from "effect/Layer"
|
|
51
|
+
import * as Option from "effect/Option"
|
|
11
52
|
import * as Predicate from "effect/Predicate"
|
|
12
53
|
import * as Redactable from "effect/Redactable"
|
|
13
54
|
import type * as Schema from "effect/Schema"
|
|
14
55
|
import * as SchemaAST from "effect/SchemaAST"
|
|
15
|
-
import * as ServiceMap from "effect/ServiceMap"
|
|
16
56
|
import * as Stream from "effect/Stream"
|
|
17
57
|
import type { Span } from "effect/Tracer"
|
|
18
58
|
import type { DeepMutable, Mutable, Simplify } from "effect/Types"
|
|
@@ -39,10 +79,17 @@ import { type ChatStreamingResponseChunkData, OpenRouterClient } from "./OpenRou
|
|
|
39
79
|
/**
|
|
40
80
|
* Service definition for OpenRouter language model configuration.
|
|
41
81
|
*
|
|
42
|
-
*
|
|
82
|
+
* **When to use**
|
|
83
|
+
*
|
|
84
|
+
* Use to provide scoped OpenRouter chat completion defaults or per-operation
|
|
85
|
+
* overrides for an OpenRouter language model service.
|
|
86
|
+
*
|
|
87
|
+
* @see {@link withConfigOverride} for scoping language model request overrides
|
|
88
|
+
*
|
|
43
89
|
* @category services
|
|
90
|
+
* @since 4.0.0
|
|
44
91
|
*/
|
|
45
|
-
export class Config extends
|
|
92
|
+
export class Config extends Context.Service<
|
|
46
93
|
Config,
|
|
47
94
|
Simplify<
|
|
48
95
|
& Partial<
|
|
@@ -68,14 +115,20 @@ export class Config extends ServiceMap.Service<
|
|
|
68
115
|
// =============================================================================
|
|
69
116
|
|
|
70
117
|
/**
|
|
71
|
-
*
|
|
118
|
+
* OpenRouter assistant reasoning detail blocks preserved for multi-turn
|
|
119
|
+
* conversations.
|
|
120
|
+
*
|
|
72
121
|
* @category models
|
|
122
|
+
* @since 4.0.0
|
|
73
123
|
*/
|
|
74
124
|
export type ReasoningDetails = Exclude<typeof Generated.AssistantMessage.Encoded["reasoning_details"], undefined>
|
|
75
125
|
|
|
76
126
|
/**
|
|
77
|
-
*
|
|
127
|
+
* File annotations emitted on OpenRouter assistant messages and exposed in
|
|
128
|
+
* finish metadata.
|
|
129
|
+
*
|
|
78
130
|
* @category models
|
|
131
|
+
* @since 4.0.0
|
|
79
132
|
*/
|
|
80
133
|
export type FileAnnotation = Extract<
|
|
81
134
|
NonNullable<typeof Generated.AssistantMessage.fields.annotations.Type>[number],
|
|
@@ -83,7 +136,21 @@ export type FileAnnotation = Extract<
|
|
|
83
136
|
>
|
|
84
137
|
|
|
85
138
|
declare module "effect/unstable/ai/Prompt" {
|
|
139
|
+
/**
|
|
140
|
+
* OpenRouter-specific options for system messages.
|
|
141
|
+
*
|
|
142
|
+
* **Details**
|
|
143
|
+
*
|
|
144
|
+
* These options are used when translating system instructions into
|
|
145
|
+
* OpenRouter chat messages.
|
|
146
|
+
*
|
|
147
|
+
* @category request
|
|
148
|
+
* @since 4.0.0
|
|
149
|
+
*/
|
|
86
150
|
export interface SystemMessageOptions extends ProviderOptions {
|
|
151
|
+
/**
|
|
152
|
+
* Provider-specific options sent to OpenRouter for the system message.
|
|
153
|
+
*/
|
|
87
154
|
readonly openrouter?: {
|
|
88
155
|
/**
|
|
89
156
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -92,7 +159,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
92
159
|
} | null
|
|
93
160
|
}
|
|
94
161
|
|
|
162
|
+
/**
|
|
163
|
+
* OpenRouter-specific options for user messages.
|
|
164
|
+
*
|
|
165
|
+
* **Details**
|
|
166
|
+
*
|
|
167
|
+
* These options are used when translating user content into OpenRouter chat
|
|
168
|
+
* messages.
|
|
169
|
+
*
|
|
170
|
+
* @category request
|
|
171
|
+
* @since 4.0.0
|
|
172
|
+
*/
|
|
95
173
|
export interface UserMessageOptions extends ProviderOptions {
|
|
174
|
+
/**
|
|
175
|
+
* Provider-specific options sent to OpenRouter for the user message.
|
|
176
|
+
*/
|
|
96
177
|
readonly openrouter?: {
|
|
97
178
|
/**
|
|
98
179
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -101,7 +182,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
101
182
|
} | null
|
|
102
183
|
}
|
|
103
184
|
|
|
185
|
+
/**
|
|
186
|
+
* OpenRouter-specific options for assistant messages.
|
|
187
|
+
*
|
|
188
|
+
* **Details**
|
|
189
|
+
*
|
|
190
|
+
* Preserves reasoning metadata when assistant messages are replayed in later
|
|
191
|
+
* OpenRouter requests.
|
|
192
|
+
*
|
|
193
|
+
* @category request
|
|
194
|
+
* @since 4.0.0
|
|
195
|
+
*/
|
|
104
196
|
export interface AssistantMessageOptions extends ProviderOptions {
|
|
197
|
+
/**
|
|
198
|
+
* Provider-specific options sent to OpenRouter for the assistant message.
|
|
199
|
+
*/
|
|
105
200
|
readonly openrouter?: {
|
|
106
201
|
/**
|
|
107
202
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -114,7 +209,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
114
209
|
} | null
|
|
115
210
|
}
|
|
116
211
|
|
|
212
|
+
/**
|
|
213
|
+
* OpenRouter-specific options for tool messages.
|
|
214
|
+
*
|
|
215
|
+
* **Details**
|
|
216
|
+
*
|
|
217
|
+
* These options are used when converting tool results into OpenRouter chat
|
|
218
|
+
* messages.
|
|
219
|
+
*
|
|
220
|
+
* @category request
|
|
221
|
+
* @since 4.0.0
|
|
222
|
+
*/
|
|
117
223
|
export interface ToolMessageOptions extends ProviderOptions {
|
|
224
|
+
/**
|
|
225
|
+
* Provider-specific options sent to OpenRouter for the tool message.
|
|
226
|
+
*/
|
|
118
227
|
readonly openrouter?: {
|
|
119
228
|
/**
|
|
120
229
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -123,7 +232,20 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
123
232
|
} | null
|
|
124
233
|
}
|
|
125
234
|
|
|
235
|
+
/**
|
|
236
|
+
* OpenRouter-specific options for text prompt parts.
|
|
237
|
+
*
|
|
238
|
+
* **When to use**
|
|
239
|
+
*
|
|
240
|
+
* Use when you use these options to control how text content is sent to OpenRouter.
|
|
241
|
+
*
|
|
242
|
+
* @category request
|
|
243
|
+
* @since 4.0.0
|
|
244
|
+
*/
|
|
126
245
|
export interface TextPartOptions extends ProviderOptions {
|
|
246
|
+
/**
|
|
247
|
+
* Provider-specific options sent to OpenRouter for the text part.
|
|
248
|
+
*/
|
|
127
249
|
readonly openrouter?: {
|
|
128
250
|
/**
|
|
129
251
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -132,7 +254,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
132
254
|
} | null
|
|
133
255
|
}
|
|
134
256
|
|
|
257
|
+
/**
|
|
258
|
+
* OpenRouter-specific options for reasoning prompt parts.
|
|
259
|
+
*
|
|
260
|
+
* **Details**
|
|
261
|
+
*
|
|
262
|
+
* Preserves provider reasoning blocks so reasoning-aware conversations can
|
|
263
|
+
* continue across OpenRouter requests.
|
|
264
|
+
*
|
|
265
|
+
* @category request
|
|
266
|
+
* @since 4.0.0
|
|
267
|
+
*/
|
|
135
268
|
export interface ReasoningPartOptions extends ProviderOptions {
|
|
269
|
+
/**
|
|
270
|
+
* Provider-specific options sent to OpenRouter for the reasoning part.
|
|
271
|
+
*/
|
|
136
272
|
readonly openrouter?: {
|
|
137
273
|
/**
|
|
138
274
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -145,7 +281,20 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
145
281
|
} | null
|
|
146
282
|
}
|
|
147
283
|
|
|
284
|
+
/**
|
|
285
|
+
* OpenRouter-specific options for file prompt parts.
|
|
286
|
+
*
|
|
287
|
+
* **Details**
|
|
288
|
+
*
|
|
289
|
+
* Controls file naming and prompt caching for files sent to OpenRouter.
|
|
290
|
+
*
|
|
291
|
+
* @category request
|
|
292
|
+
* @since 4.0.0
|
|
293
|
+
*/
|
|
148
294
|
export interface FilePartOptions extends ProviderOptions {
|
|
295
|
+
/**
|
|
296
|
+
* Provider-specific options sent to OpenRouter for the file part.
|
|
297
|
+
*/
|
|
149
298
|
readonly openrouter?: {
|
|
150
299
|
/**
|
|
151
300
|
* The name to give to the file. Will be prioritized over the file name
|
|
@@ -159,7 +308,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
159
308
|
} | null
|
|
160
309
|
}
|
|
161
310
|
|
|
311
|
+
/**
|
|
312
|
+
* OpenRouter-specific options for tool call prompt parts.
|
|
313
|
+
*
|
|
314
|
+
* **Details**
|
|
315
|
+
*
|
|
316
|
+
* Preserves reasoning details associated with tool calls when a conversation
|
|
317
|
+
* is sent back to OpenRouter.
|
|
318
|
+
*
|
|
319
|
+
* @category request
|
|
320
|
+
* @since 4.0.0
|
|
321
|
+
*/
|
|
162
322
|
export interface ToolCallPartOptions extends ProviderOptions {
|
|
323
|
+
/**
|
|
324
|
+
* Provider-specific options sent to OpenRouter for the tool call part.
|
|
325
|
+
*/
|
|
163
326
|
readonly openrouter?: {
|
|
164
327
|
/**
|
|
165
328
|
* Reasoning details associated with the tool call part.
|
|
@@ -168,7 +331,20 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
168
331
|
} | null
|
|
169
332
|
}
|
|
170
333
|
|
|
334
|
+
/**
|
|
335
|
+
* OpenRouter-specific options for tool result prompt parts.
|
|
336
|
+
*
|
|
337
|
+
* **Details**
|
|
338
|
+
*
|
|
339
|
+
* Controls prompt caching for tool results sent to OpenRouter.
|
|
340
|
+
*
|
|
341
|
+
* @category request
|
|
342
|
+
* @since 4.0.0
|
|
343
|
+
*/
|
|
171
344
|
export interface ToolResultPartOptions extends ProviderOptions {
|
|
345
|
+
/**
|
|
346
|
+
* Provider-specific options sent to OpenRouter for the tool result part.
|
|
347
|
+
*/
|
|
172
348
|
readonly openrouter?: {
|
|
173
349
|
/**
|
|
174
350
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -179,43 +355,157 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
179
355
|
}
|
|
180
356
|
|
|
181
357
|
declare module "effect/unstable/ai/Response" {
|
|
358
|
+
/**
|
|
359
|
+
* OpenRouter metadata attached to completed reasoning response parts.
|
|
360
|
+
*
|
|
361
|
+
* **Details**
|
|
362
|
+
*
|
|
363
|
+
* Preserves provider reasoning details that can be sent back in later turns.
|
|
364
|
+
*
|
|
365
|
+
* @category response
|
|
366
|
+
* @since 4.0.0
|
|
367
|
+
*/
|
|
182
368
|
export interface ReasoningPartMetadata extends ProviderMetadata {
|
|
369
|
+
/**
|
|
370
|
+
* Provider-specific metadata returned for the reasoning part.
|
|
371
|
+
*/
|
|
183
372
|
readonly openrouter?: {
|
|
373
|
+
/**
|
|
374
|
+
* Reasoning details emitted by the underlying provider for this part.
|
|
375
|
+
*/
|
|
184
376
|
readonly reasoningDetails?: ReasoningDetails | null
|
|
185
377
|
} | null
|
|
186
378
|
}
|
|
187
379
|
|
|
380
|
+
/**
|
|
381
|
+
* OpenRouter metadata emitted when a streamed reasoning part starts.
|
|
382
|
+
*
|
|
383
|
+
* **Details**
|
|
384
|
+
*
|
|
385
|
+
* Carries the first reasoning detail chunk when OpenRouter exposes one.
|
|
386
|
+
*
|
|
387
|
+
* @category response
|
|
388
|
+
* @since 4.0.0
|
|
389
|
+
*/
|
|
188
390
|
export interface ReasoningStartPartMetadata extends ProviderMetadata {
|
|
391
|
+
/**
|
|
392
|
+
* Provider-specific metadata returned for the streamed reasoning start.
|
|
393
|
+
*/
|
|
189
394
|
readonly openrouter?: {
|
|
395
|
+
/**
|
|
396
|
+
* Reasoning details emitted by the underlying provider for this part.
|
|
397
|
+
*/
|
|
190
398
|
readonly reasoningDetails?: ReasoningDetails | null
|
|
191
399
|
} | null
|
|
192
400
|
}
|
|
193
401
|
|
|
402
|
+
/**
|
|
403
|
+
* OpenRouter metadata emitted for streamed reasoning deltas.
|
|
404
|
+
*
|
|
405
|
+
* **Details**
|
|
406
|
+
*
|
|
407
|
+
* Carries provider reasoning detail chunks as they arrive from OpenRouter.
|
|
408
|
+
*
|
|
409
|
+
* @category response
|
|
410
|
+
* @since 4.0.0
|
|
411
|
+
*/
|
|
194
412
|
export interface ReasoningDeltaPartMetadata extends ProviderMetadata {
|
|
413
|
+
/**
|
|
414
|
+
* Provider-specific metadata returned for the streamed reasoning delta.
|
|
415
|
+
*/
|
|
195
416
|
readonly openrouter?: {
|
|
417
|
+
/**
|
|
418
|
+
* Reasoning details emitted by the underlying provider for this delta.
|
|
419
|
+
*/
|
|
196
420
|
readonly reasoningDetails?: ReasoningDetails | null
|
|
197
421
|
} | null
|
|
198
422
|
}
|
|
199
423
|
|
|
424
|
+
/**
|
|
425
|
+
* OpenRouter metadata attached to tool-call response parts.
|
|
426
|
+
*
|
|
427
|
+
* **Details**
|
|
428
|
+
*
|
|
429
|
+
* Associates tool calls with provider reasoning details when the model emits
|
|
430
|
+
* reasoning and tool calls together.
|
|
431
|
+
*
|
|
432
|
+
* @category response
|
|
433
|
+
* @since 4.0.0
|
|
434
|
+
*/
|
|
200
435
|
export interface ToolCallPartMetadata extends ProviderMetadata {
|
|
436
|
+
/**
|
|
437
|
+
* Provider-specific metadata returned for the tool call.
|
|
438
|
+
*/
|
|
201
439
|
readonly openrouter?: {
|
|
440
|
+
/**
|
|
441
|
+
* Reasoning details associated with this tool call.
|
|
442
|
+
*/
|
|
202
443
|
readonly reasoningDetails?: ReasoningDetails | null
|
|
203
444
|
} | null
|
|
204
445
|
}
|
|
205
446
|
|
|
447
|
+
/**
|
|
448
|
+
* OpenRouter metadata attached to URL source citations.
|
|
449
|
+
*
|
|
450
|
+
* **Details**
|
|
451
|
+
*
|
|
452
|
+
* Includes citation text and offsets returned by providers that support URL
|
|
453
|
+
* annotations.
|
|
454
|
+
*
|
|
455
|
+
* @category response
|
|
456
|
+
* @since 4.0.0
|
|
457
|
+
*/
|
|
206
458
|
export interface UrlSourcePartMetadata extends ProviderMetadata {
|
|
459
|
+
/**
|
|
460
|
+
* Provider-specific citation metadata returned for the URL source.
|
|
461
|
+
*/
|
|
207
462
|
readonly openrouter?: {
|
|
463
|
+
/**
|
|
464
|
+
* The cited source content returned by the provider.
|
|
465
|
+
*/
|
|
208
466
|
readonly content?: string | null
|
|
467
|
+
/**
|
|
468
|
+
* The zero-based start index of the citation in the generated text.
|
|
469
|
+
*/
|
|
209
470
|
readonly startIndex?: number | null
|
|
471
|
+
/**
|
|
472
|
+
* The zero-based end index of the citation in the generated text.
|
|
473
|
+
*/
|
|
210
474
|
readonly endIndex?: number | null
|
|
211
475
|
} | null
|
|
212
476
|
}
|
|
213
477
|
|
|
478
|
+
/**
|
|
479
|
+
* OpenRouter metadata attached to finish response parts.
|
|
480
|
+
*
|
|
481
|
+
* **Details**
|
|
482
|
+
*
|
|
483
|
+
* Exposes provider response details that are not represented by the common
|
|
484
|
+
* Effect AI finish part fields.
|
|
485
|
+
*
|
|
486
|
+
* @category response
|
|
487
|
+
* @since 4.0.0
|
|
488
|
+
*/
|
|
214
489
|
export interface FinishPartMetadata extends ProviderMetadata {
|
|
490
|
+
/**
|
|
491
|
+
* Provider-specific metadata returned when the OpenRouter response finishes.
|
|
492
|
+
*/
|
|
215
493
|
readonly openrouter?: {
|
|
494
|
+
/**
|
|
495
|
+
* Provider fingerprint for the backend configuration that served the request.
|
|
496
|
+
*/
|
|
216
497
|
readonly systemFingerprint?: string | null
|
|
498
|
+
/**
|
|
499
|
+
* Raw token usage reported by OpenRouter.
|
|
500
|
+
*/
|
|
217
501
|
readonly usage?: typeof Generated.ChatGenerationTokenUsage.Encoded | null
|
|
502
|
+
/**
|
|
503
|
+
* File annotations returned by the provider.
|
|
504
|
+
*/
|
|
218
505
|
readonly annotations?: ReadonlyArray<FileAnnotation> | null
|
|
506
|
+
/**
|
|
507
|
+
* The OpenRouter provider that served the request, when reported.
|
|
508
|
+
*/
|
|
219
509
|
readonly provider?: string | null
|
|
220
510
|
} | null
|
|
221
511
|
}
|
|
@@ -226,20 +516,59 @@ declare module "effect/unstable/ai/Response" {
|
|
|
226
516
|
// =============================================================================
|
|
227
517
|
|
|
228
518
|
/**
|
|
229
|
-
*
|
|
519
|
+
* Creates an OpenRouter model descriptor that can be provided with
|
|
520
|
+
* `Effect.provide`.
|
|
521
|
+
*
|
|
522
|
+
* **When to use**
|
|
523
|
+
*
|
|
524
|
+
* Use when you want an OpenRouter language model value that carries provider
|
|
525
|
+
* and model metadata and can be supplied directly to an Effect program.
|
|
526
|
+
*
|
|
527
|
+
* **Details**
|
|
528
|
+
*
|
|
529
|
+
* The returned model requires `OpenRouterClient` and provides
|
|
530
|
+
* `LanguageModel.LanguageModel`.
|
|
531
|
+
*
|
|
532
|
+
* @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly
|
|
533
|
+
* @see {@link make} for constructing the language model service effectfully
|
|
534
|
+
* @see {@link withConfigOverride} for scoping OpenRouter request overrides
|
|
535
|
+
*
|
|
230
536
|
* @category constructors
|
|
537
|
+
* @since 4.0.0
|
|
231
538
|
*/
|
|
232
539
|
export const model = (
|
|
233
540
|
model: string,
|
|
234
541
|
config?: Omit<typeof Config.Service, "model">
|
|
235
542
|
): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenRouterClient> =>
|
|
236
|
-
AiModel.make("openai", layer({ model, config }))
|
|
543
|
+
AiModel.make("openai", model, layer({ model, config }))
|
|
237
544
|
|
|
238
545
|
/**
|
|
239
|
-
* Creates an OpenRouter
|
|
546
|
+
* Creates an OpenRouter `LanguageModel` service from a model identifier and
|
|
547
|
+
* optional request defaults.
|
|
548
|
+
*
|
|
549
|
+
* **When to use**
|
|
550
|
+
*
|
|
551
|
+
* Use when an Effect needs to construct a `LanguageModel.Service` value backed
|
|
552
|
+
* by `OpenRouterClient`.
|
|
553
|
+
*
|
|
554
|
+
* **Details**
|
|
555
|
+
*
|
|
556
|
+
* The returned effect requires `OpenRouterClient`. Request defaults from the
|
|
557
|
+
* `config` option are merged with any `Config` service in the context, with
|
|
558
|
+
* context values taking precedence. The service supports both `generateText`
|
|
559
|
+
* and `streamText`.
|
|
560
|
+
*
|
|
561
|
+
* **Gotchas**
|
|
562
|
+
*
|
|
563
|
+
* Provider-defined tools are not supported by this provider integration;
|
|
564
|
+
* requests that include them fail with an `InvalidUserInputError`.
|
|
565
|
+
*
|
|
566
|
+
* @see {@link layer} for providing the service as a `Layer`
|
|
567
|
+
* @see {@link model} for creating a model descriptor for `Effect.provide`
|
|
568
|
+
* @see {@link withConfigOverride} for scoping request defaults around operations
|
|
240
569
|
*
|
|
241
|
-
* @since 1.0.0
|
|
242
570
|
* @category constructors
|
|
571
|
+
* @since 4.0.0
|
|
243
572
|
*/
|
|
244
573
|
export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: {
|
|
245
574
|
readonly model: string
|
|
@@ -249,7 +578,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
|
|
|
249
578
|
const codecTransformer = getCodecTransformer(model)
|
|
250
579
|
|
|
251
580
|
const makeConfig = Effect.gen(function*() {
|
|
252
|
-
const services = yield* Effect.
|
|
581
|
+
const services = yield* Effect.context<never>()
|
|
253
582
|
return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
|
|
254
583
|
})
|
|
255
584
|
|
|
@@ -273,6 +602,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
|
|
|
273
602
|
)
|
|
274
603
|
|
|
275
604
|
return yield* LanguageModel.make({
|
|
605
|
+
codecTransformer: toCodecOpenAI,
|
|
276
606
|
generateText: Effect.fnUntraced(
|
|
277
607
|
function*(options) {
|
|
278
608
|
const config = yield* makeConfig
|
|
@@ -300,17 +630,23 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
|
|
|
300
630
|
})
|
|
301
631
|
)
|
|
302
632
|
)
|
|
303
|
-
})
|
|
304
|
-
LanguageModel.CurrentCodecTransformer,
|
|
305
|
-
codecTransformer
|
|
306
|
-
))
|
|
633
|
+
})
|
|
307
634
|
})
|
|
308
635
|
|
|
309
636
|
/**
|
|
310
637
|
* Creates a layer for the OpenRouter language model.
|
|
311
638
|
*
|
|
312
|
-
*
|
|
639
|
+
* **When to use**
|
|
640
|
+
*
|
|
641
|
+
* Use when composing application layers and you want OpenRouter to satisfy
|
|
642
|
+
* `LanguageModel.LanguageModel` while supplying `OpenRouterClient` from another
|
|
643
|
+
* layer.
|
|
644
|
+
*
|
|
645
|
+
* @see {@link make} for constructing the language model service effectfully
|
|
646
|
+
* @see {@link model} for creating a model descriptor for `Effect.provide`
|
|
647
|
+
*
|
|
313
648
|
* @category layers
|
|
649
|
+
* @since 4.0.0
|
|
314
650
|
*/
|
|
315
651
|
export const layer = (options: {
|
|
316
652
|
readonly model: string
|
|
@@ -321,37 +657,107 @@ export const layer = (options: {
|
|
|
321
657
|
/**
|
|
322
658
|
* Provides config overrides for OpenRouter language model operations.
|
|
323
659
|
*
|
|
324
|
-
*
|
|
660
|
+
* **When to use**
|
|
661
|
+
*
|
|
662
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
663
|
+
* the model's default configuration.
|
|
664
|
+
*
|
|
665
|
+
* **Details**
|
|
666
|
+
*
|
|
667
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
668
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
669
|
+
* config, and the helper supports both pipe form and
|
|
670
|
+
* `withConfigOverride(effect, overrides)`.
|
|
671
|
+
*
|
|
672
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
673
|
+
*
|
|
325
674
|
* @category configuration
|
|
675
|
+
* @since 4.0.0
|
|
326
676
|
*/
|
|
327
677
|
export const withConfigOverride: {
|
|
328
678
|
/**
|
|
329
679
|
* Provides config overrides for OpenRouter language model operations.
|
|
330
680
|
*
|
|
331
|
-
*
|
|
681
|
+
* **When to use**
|
|
682
|
+
*
|
|
683
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
684
|
+
* the model's default configuration.
|
|
685
|
+
*
|
|
686
|
+
* **Details**
|
|
687
|
+
*
|
|
688
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
689
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
690
|
+
* config, and the helper supports both pipe form and
|
|
691
|
+
* `withConfigOverride(effect, overrides)`.
|
|
692
|
+
*
|
|
693
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
694
|
+
*
|
|
332
695
|
* @category configuration
|
|
696
|
+
* @since 4.0.0
|
|
333
697
|
*/
|
|
334
698
|
(overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
|
|
335
699
|
/**
|
|
336
700
|
* Provides config overrides for OpenRouter language model operations.
|
|
337
701
|
*
|
|
338
|
-
*
|
|
702
|
+
* **When to use**
|
|
703
|
+
*
|
|
704
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
705
|
+
* the model's default configuration.
|
|
706
|
+
*
|
|
707
|
+
* **Details**
|
|
708
|
+
*
|
|
709
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
710
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
711
|
+
* config, and the helper supports both pipe form and
|
|
712
|
+
* `withConfigOverride(effect, overrides)`.
|
|
713
|
+
*
|
|
714
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
715
|
+
*
|
|
339
716
|
* @category configuration
|
|
717
|
+
* @since 4.0.0
|
|
340
718
|
*/
|
|
341
719
|
<A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service): Effect.Effect<A, E, Exclude<R, Config>>
|
|
342
720
|
} = dual<
|
|
343
721
|
/**
|
|
344
722
|
* Provides config overrides for OpenRouter language model operations.
|
|
345
723
|
*
|
|
346
|
-
*
|
|
724
|
+
* **When to use**
|
|
725
|
+
*
|
|
726
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
727
|
+
* the model's default configuration.
|
|
728
|
+
*
|
|
729
|
+
* **Details**
|
|
730
|
+
*
|
|
731
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
732
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
733
|
+
* config, and the helper supports both pipe form and
|
|
734
|
+
* `withConfigOverride(effect, overrides)`.
|
|
735
|
+
*
|
|
736
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
737
|
+
*
|
|
347
738
|
* @category configuration
|
|
739
|
+
* @since 4.0.0
|
|
348
740
|
*/
|
|
349
741
|
(overrides: typeof Config.Service) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>,
|
|
350
742
|
/**
|
|
351
743
|
* Provides config overrides for OpenRouter language model operations.
|
|
352
744
|
*
|
|
353
|
-
*
|
|
745
|
+
* **When to use**
|
|
746
|
+
*
|
|
747
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
748
|
+
* the model's default configuration.
|
|
749
|
+
*
|
|
750
|
+
* **Details**
|
|
751
|
+
*
|
|
752
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
753
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
754
|
+
* config, and the helper supports both pipe form and
|
|
755
|
+
* `withConfigOverride(effect, overrides)`.
|
|
756
|
+
*
|
|
757
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
758
|
+
*
|
|
354
759
|
* @category configuration
|
|
760
|
+
* @since 4.0.0
|
|
355
761
|
*/
|
|
356
762
|
<A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service) => Effect.Effect<A, E, Exclude<R, Config>>
|
|
357
763
|
>(2, (self, overrides) =>
|
|
@@ -588,7 +994,7 @@ const buildHttpRequestDetails = (
|
|
|
588
994
|
method: request.method,
|
|
589
995
|
url: request.url,
|
|
590
996
|
urlParams: Array.from(request.urlParams),
|
|
591
|
-
hash: request.hash,
|
|
997
|
+
hash: Option.getOrUndefined(request.hash),
|
|
592
998
|
headers: Redactable.redact(request.headers) as Record<string, string>
|
|
593
999
|
})
|
|
594
1000
|
|
|
@@ -885,283 +1291,275 @@ const makeStreamResponse = Effect.fnUntraced(
|
|
|
885
1291
|
}
|
|
886
1292
|
|
|
887
1293
|
const choice = event.choices[0]
|
|
888
|
-
if (Predicate.
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
reason: new AiError.InvalidOutputError({
|
|
893
|
-
description: "Received response with empty choices"
|
|
894
|
-
})
|
|
895
|
-
})
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
if (Predicate.isNotNull(choice.finish_reason)) {
|
|
899
|
-
finishReason = resolveFinishReason(choice.finish_reason)
|
|
900
|
-
}
|
|
1294
|
+
if (Predicate.isNotUndefined(choice)) {
|
|
1295
|
+
if (Predicate.isNotNullish(choice.finish_reason)) {
|
|
1296
|
+
finishReason = resolveFinishReason(choice.finish_reason)
|
|
1297
|
+
}
|
|
901
1298
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1299
|
+
const delta = choice.delta
|
|
1300
|
+
if (Predicate.isNullish(delta)) {
|
|
1301
|
+
return parts
|
|
1302
|
+
}
|
|
906
1303
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1304
|
+
const emitReasoning = Effect.fnUntraced(
|
|
1305
|
+
function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) {
|
|
1306
|
+
if (!reasoningStarted) {
|
|
1307
|
+
activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
1308
|
+
parts.push({
|
|
1309
|
+
type: "reasoning-start",
|
|
1310
|
+
id: activeReasoningId,
|
|
1311
|
+
metadata
|
|
1312
|
+
})
|
|
1313
|
+
reasoningStarted = true
|
|
1314
|
+
}
|
|
911
1315
|
parts.push({
|
|
912
|
-
type: "reasoning-
|
|
913
|
-
id: activeReasoningId
|
|
1316
|
+
type: "reasoning-delta",
|
|
1317
|
+
id: activeReasoningId!,
|
|
1318
|
+
delta,
|
|
914
1319
|
metadata
|
|
915
1320
|
})
|
|
916
|
-
reasoningStarted = true
|
|
917
1321
|
}
|
|
918
|
-
|
|
919
|
-
type: "reasoning-delta",
|
|
920
|
-
id: activeReasoningId!,
|
|
921
|
-
delta,
|
|
922
|
-
metadata
|
|
923
|
-
})
|
|
924
|
-
}
|
|
925
|
-
)
|
|
1322
|
+
)
|
|
926
1323
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1324
|
+
const reasoningDetails = delta.reasoning_details
|
|
1325
|
+
if (Predicate.isNotUndefined(reasoningDetails) && reasoningDetails.length > 0) {
|
|
1326
|
+
// Accumulate reasoning_details to preserve for multi-turn conversations
|
|
1327
|
+
// Merge consecutive reasoning.text items into a single entry
|
|
1328
|
+
for (const detail of reasoningDetails) {
|
|
1329
|
+
if (detail.type === "reasoning.text") {
|
|
1330
|
+
const lastDetail = accumulatedReasoningDetails[accumulatedReasoningDetails.length - 1]
|
|
1331
|
+
if (Predicate.isNotUndefined(lastDetail) && lastDetail.type === "reasoning.text") {
|
|
1332
|
+
// Merge with the previous text detail
|
|
1333
|
+
lastDetail.text = (lastDetail.text ?? "") + (detail.text ?? "")
|
|
1334
|
+
lastDetail.signature = lastDetail.signature ?? detail.signature ?? null
|
|
1335
|
+
lastDetail.format = lastDetail.format ?? detail.format ?? null
|
|
1336
|
+
} else {
|
|
1337
|
+
// Start a new text detail
|
|
1338
|
+
accumulatedReasoningDetails.push({ ...detail })
|
|
1339
|
+
}
|
|
939
1340
|
} else {
|
|
940
|
-
//
|
|
941
|
-
accumulatedReasoningDetails.push(
|
|
1341
|
+
// Non-text details (encrypted, summary) are pushed as-is
|
|
1342
|
+
accumulatedReasoningDetails.push(detail)
|
|
942
1343
|
}
|
|
943
|
-
} else {
|
|
944
|
-
// Non-text details (encrypted, summary) are pushed as-is
|
|
945
|
-
accumulatedReasoningDetails.push(detail)
|
|
946
1344
|
}
|
|
947
|
-
}
|
|
948
1345
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1346
|
+
// Emit reasoning_details in providerMetadata for each delta chunk
|
|
1347
|
+
// so users can accumulate them on their end before sending back
|
|
1348
|
+
const metadata: Response.ReasoningDeltaPart["metadata"] = {
|
|
1349
|
+
openrouter: {
|
|
1350
|
+
reasoningDetails
|
|
1351
|
+
}
|
|
954
1352
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1353
|
+
for (const detail of reasoningDetails) {
|
|
1354
|
+
switch (detail.type) {
|
|
1355
|
+
case "reasoning.text": {
|
|
1356
|
+
if (Predicate.isNotNullish(detail.text)) {
|
|
1357
|
+
yield* emitReasoning(detail.text, metadata)
|
|
1358
|
+
}
|
|
1359
|
+
break
|
|
961
1360
|
}
|
|
962
|
-
break
|
|
963
|
-
}
|
|
964
1361
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1362
|
+
case "reasoning.summary": {
|
|
1363
|
+
if (Predicate.isNotNullish(detail.summary)) {
|
|
1364
|
+
yield* emitReasoning(detail.summary, metadata)
|
|
1365
|
+
}
|
|
1366
|
+
break
|
|
968
1367
|
}
|
|
969
|
-
break
|
|
970
|
-
}
|
|
971
1368
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1369
|
+
case "reasoning.encrypted": {
|
|
1370
|
+
if (Predicate.isNotNullish(detail.data)) {
|
|
1371
|
+
yield* emitReasoning("[REDACTED]", metadata)
|
|
1372
|
+
}
|
|
1373
|
+
break
|
|
975
1374
|
}
|
|
976
|
-
break
|
|
977
1375
|
}
|
|
978
1376
|
}
|
|
1377
|
+
} else if (Predicate.isNotNullish(delta.reasoning)) {
|
|
1378
|
+
yield* emitReasoning(delta.reasoning)
|
|
979
1379
|
}
|
|
980
|
-
} else if (Predicate.isNotNullish(delta.reasoning)) {
|
|
981
|
-
yield* emitReasoning(delta.reasoning)
|
|
982
|
-
}
|
|
983
1380
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1381
|
+
const content = delta.content
|
|
1382
|
+
if (Predicate.isNotNullish(content)) {
|
|
1383
|
+
// If reasoning was previously active and now we're starting text content,
|
|
1384
|
+
// we should end the reasoning first to maintain proper order
|
|
1385
|
+
if (reasoningStarted && !textStarted) {
|
|
1386
|
+
parts.push({
|
|
1387
|
+
type: "reasoning-end",
|
|
1388
|
+
id: activeReasoningId!,
|
|
1389
|
+
// Include accumulated reasoning_details so the we can update the
|
|
1390
|
+
// reasoning part's provider metadata with the correct signature.
|
|
1391
|
+
// The signature typically arrives in the last reasoning delta,
|
|
1392
|
+
// but reasoning-start only carries the first delta's metadata.
|
|
1393
|
+
metadata: accumulatedReasoningDetails.length > 0
|
|
1394
|
+
? { openRouter: { reasoningDetails: accumulatedReasoningDetails } }
|
|
1395
|
+
: undefined
|
|
1396
|
+
})
|
|
1397
|
+
reasoningStarted = false
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (!textStarted) {
|
|
1401
|
+
activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
1402
|
+
parts.push({
|
|
1403
|
+
type: "text-start",
|
|
1404
|
+
id: activeTextId
|
|
1405
|
+
})
|
|
1406
|
+
textStarted = true
|
|
1407
|
+
}
|
|
1002
1408
|
|
|
1003
|
-
if (!textStarted) {
|
|
1004
|
-
activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
1005
1409
|
parts.push({
|
|
1006
|
-
type: "text-
|
|
1007
|
-
id: activeTextId
|
|
1410
|
+
type: "text-delta",
|
|
1411
|
+
id: activeTextId!,
|
|
1412
|
+
delta: content
|
|
1008
1413
|
})
|
|
1009
|
-
textStarted = true
|
|
1010
1414
|
}
|
|
1011
1415
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
...(Predicate.isNotUndefined(annotation.url_citation.start_index)
|
|
1035
|
-
? { startIndex: annotation.url_citation.start_index }
|
|
1036
|
-
: undefined),
|
|
1037
|
-
...(Predicate.isNotUndefined(annotation.url_citation.end_index)
|
|
1038
|
-
? { startIndex: annotation.url_citation.end_index }
|
|
1039
|
-
: undefined)
|
|
1416
|
+
const annotations = delta.annotations
|
|
1417
|
+
if (Predicate.isNotNullish(annotations)) {
|
|
1418
|
+
for (const annotation of annotations) {
|
|
1419
|
+
if (annotation.type === "url_citation") {
|
|
1420
|
+
parts.push({
|
|
1421
|
+
type: "source",
|
|
1422
|
+
sourceType: "url",
|
|
1423
|
+
id: annotation.url_citation.url,
|
|
1424
|
+
url: annotation.url_citation.url,
|
|
1425
|
+
title: annotation.url_citation.title ?? "",
|
|
1426
|
+
metadata: {
|
|
1427
|
+
openrouter: {
|
|
1428
|
+
...(Predicate.isNotUndefined(annotation.url_citation.content)
|
|
1429
|
+
? { content: annotation.url_citation.content }
|
|
1430
|
+
: undefined),
|
|
1431
|
+
...(Predicate.isNotUndefined(annotation.url_citation.start_index)
|
|
1432
|
+
? { startIndex: annotation.url_citation.start_index }
|
|
1433
|
+
: undefined),
|
|
1434
|
+
...(Predicate.isNotUndefined(annotation.url_citation.end_index)
|
|
1435
|
+
? { startIndex: annotation.url_citation.end_index }
|
|
1436
|
+
: undefined)
|
|
1437
|
+
}
|
|
1040
1438
|
}
|
|
1041
|
-
}
|
|
1042
|
-
})
|
|
1043
|
-
|
|
1044
|
-
|
|
1439
|
+
})
|
|
1440
|
+
} else if (annotation.type === "file") {
|
|
1441
|
+
accumulatedFileAnnotations.push(annotation)
|
|
1442
|
+
}
|
|
1045
1443
|
}
|
|
1046
1444
|
}
|
|
1047
|
-
}
|
|
1048
1445
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1446
|
+
const toolCalls = delta.tool_calls
|
|
1447
|
+
if (Predicate.isNotNullish(toolCalls)) {
|
|
1448
|
+
for (const toolCall of toolCalls) {
|
|
1449
|
+
const index = toolCall.index ?? toolCalls.length - 1
|
|
1450
|
+
let activeToolCall = activeToolCalls[index]
|
|
1451
|
+
|
|
1452
|
+
// Tool call start - OpenRouter returns all information except the
|
|
1453
|
+
// tool call parameters in the first chunk
|
|
1454
|
+
if (Predicate.isUndefined(activeToolCall)) {
|
|
1455
|
+
if (toolCall.type !== "function") {
|
|
1456
|
+
return yield* AiError.make({
|
|
1457
|
+
module: "OpenRouterLanguageModel",
|
|
1458
|
+
method: "makeStreamResponse",
|
|
1459
|
+
reason: new AiError.InvalidOutputError({
|
|
1460
|
+
description: "Received tool call delta that was not of type: 'function'"
|
|
1461
|
+
})
|
|
1064
1462
|
})
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1463
|
+
}
|
|
1067
1464
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1465
|
+
if (Predicate.isNullish(toolCall.id)) {
|
|
1466
|
+
return yield* AiError.make({
|
|
1467
|
+
module: "OpenRouterLanguageModel",
|
|
1468
|
+
method: "makeStreamResponse",
|
|
1469
|
+
reason: new AiError.InvalidOutputError({
|
|
1470
|
+
description: "Received tool call delta without a tool call identifier"
|
|
1471
|
+
})
|
|
1074
1472
|
})
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1473
|
+
}
|
|
1077
1474
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1475
|
+
if (Predicate.isNullish(toolCall.function?.name)) {
|
|
1476
|
+
return yield* AiError.make({
|
|
1477
|
+
module: "OpenRouterLanguageModel",
|
|
1478
|
+
method: "makeStreamResponse",
|
|
1479
|
+
reason: new AiError.InvalidOutputError({
|
|
1480
|
+
description: "Received tool call delta without a tool call name"
|
|
1481
|
+
})
|
|
1084
1482
|
})
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1483
|
+
}
|
|
1087
1484
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1485
|
+
activeToolCall = {
|
|
1486
|
+
id: toolCall.id,
|
|
1487
|
+
type: "function",
|
|
1488
|
+
name: toolCall.function.name,
|
|
1489
|
+
params: toolCall.function.arguments ?? ""
|
|
1490
|
+
}
|
|
1094
1491
|
|
|
1095
|
-
|
|
1492
|
+
activeToolCalls[index] = activeToolCall
|
|
1096
1493
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1494
|
+
parts.push({
|
|
1495
|
+
type: "tool-params-start",
|
|
1496
|
+
id: activeToolCall.id,
|
|
1497
|
+
name: activeToolCall.name
|
|
1498
|
+
})
|
|
1102
1499
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1500
|
+
// Emit a tool call delta part if parameters were also sent
|
|
1501
|
+
if (activeToolCall.params.length > 0) {
|
|
1502
|
+
parts.push({
|
|
1503
|
+
type: "tool-params-delta",
|
|
1504
|
+
id: activeToolCall.id,
|
|
1505
|
+
delta: activeToolCall.params
|
|
1506
|
+
})
|
|
1507
|
+
}
|
|
1508
|
+
} else {
|
|
1509
|
+
// If an active tool call was found, update and emit the delta for
|
|
1510
|
+
// the tool call's parameters
|
|
1511
|
+
activeToolCall.params += toolCall.function?.arguments ?? ""
|
|
1105
1512
|
parts.push({
|
|
1106
1513
|
type: "tool-params-delta",
|
|
1107
1514
|
id: activeToolCall.id,
|
|
1108
1515
|
delta: activeToolCall.params
|
|
1109
1516
|
})
|
|
1110
1517
|
}
|
|
1111
|
-
} else {
|
|
1112
|
-
// If an active tool call was found, update and emit the delta for
|
|
1113
|
-
// the tool call's parameters
|
|
1114
|
-
activeToolCall.params += toolCall.function?.arguments ?? ""
|
|
1115
|
-
parts.push({
|
|
1116
|
-
type: "tool-params-delta",
|
|
1117
|
-
id: activeToolCall.id,
|
|
1118
|
-
delta: activeToolCall.params
|
|
1119
|
-
})
|
|
1120
|
-
}
|
|
1121
1518
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1519
|
+
// Check if the tool call is complete
|
|
1520
|
+
// @effect-diagnostics-next-line tryCatchInEffectGen:off
|
|
1521
|
+
try {
|
|
1522
|
+
const params = Tool.unsafeSecureJsonParse(activeToolCall.params)
|
|
1126
1523
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1524
|
+
parts.push({
|
|
1525
|
+
type: "tool-params-end",
|
|
1526
|
+
id: activeToolCall.id
|
|
1527
|
+
})
|
|
1131
1528
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1529
|
+
parts.push({
|
|
1530
|
+
type: "tool-call",
|
|
1531
|
+
id: activeToolCall.id,
|
|
1532
|
+
name: activeToolCall.name,
|
|
1533
|
+
params,
|
|
1534
|
+
// Only attach reasoning_details to the first tool call to avoid
|
|
1535
|
+
// duplicating thinking blocks for parallel tool calls (Claude)
|
|
1536
|
+
metadata: reasoningDetailsAttachedToToolCall ? undefined : {
|
|
1537
|
+
openrouter: { reasoningDetails: accumulatedReasoningDetails }
|
|
1538
|
+
}
|
|
1539
|
+
})
|
|
1143
1540
|
|
|
1144
|
-
|
|
1541
|
+
reasoningDetailsAttachedToToolCall = true
|
|
1145
1542
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1543
|
+
// Increment the total tool calls emitted by the stream and
|
|
1544
|
+
// remove the active tool call
|
|
1545
|
+
totalToolCalls += 1
|
|
1546
|
+
delete activeToolCalls[toolCall.index]
|
|
1547
|
+
} catch {
|
|
1548
|
+
// Tool call incomplete, continue parsing
|
|
1549
|
+
continue
|
|
1550
|
+
}
|
|
1153
1551
|
}
|
|
1154
1552
|
}
|
|
1155
|
-
}
|
|
1156
1553
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1554
|
+
const images = delta.images
|
|
1555
|
+
if (Predicate.isNotNullish(images)) {
|
|
1556
|
+
for (const image of images) {
|
|
1557
|
+
parts.push({
|
|
1558
|
+
type: "file",
|
|
1559
|
+
mediaType: getMediaType(image.image_url.url, "image/jpeg"),
|
|
1560
|
+
data: getBase64FromDataUrl(image.image_url.url)
|
|
1561
|
+
})
|
|
1562
|
+
}
|
|
1165
1563
|
}
|
|
1166
1564
|
}
|
|
1167
1565
|
|