@effect/ai-openrouter 4.0.0-beta.1 → 4.0.0-beta.100
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 +1234 -1155
- package/dist/Generated.d.ts.map +1 -1
- package/dist/Generated.js +62 -33
- package/dist/Generated.js.map +1 -1
- package/dist/OpenRouterClient.d.ts +86 -20
- package/dist/OpenRouterClient.d.ts.map +1 -1
- package/dist/OpenRouterClient.js +62 -12
- package/dist/OpenRouterClient.js.map +1 -1
- package/dist/OpenRouterConfig.d.ts +95 -10
- package/dist/OpenRouterConfig.d.ts.map +1 -1
- package/dist/OpenRouterConfig.js +45 -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 +398 -49
- package/dist/OpenRouterLanguageModel.d.ts.map +1 -1
- package/dist/OpenRouterLanguageModel.js +374 -247
- 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 +5 -6
- package/src/Generated.ts +128 -58
- package/src/OpenRouterClient.ts +87 -21
- package/src/OpenRouterConfig.ts +140 -13
- package/src/OpenRouterError.ts +168 -35
- package/src/OpenRouterLanguageModel.ts +685 -254
- package/src/index.ts +6 -11
- package/src/internal/errors.ts +6 -4
- package/src/internal/utilities.ts +0 -1
|
@@ -1,18 +1,26 @@
|
|
|
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 provider options into OpenRouter chat completion
|
|
6
|
+
* requests, records GenAI telemetry around those calls, and converts normal or
|
|
7
|
+
* streaming results back into Effect AI response content and metadata.
|
|
8
|
+
*
|
|
9
|
+
* @since 4.0.0
|
|
3
10
|
*/
|
|
4
11
|
/** @effect-diagnostics preferSchemaOverJson:skip-file */
|
|
5
12
|
import * as Arr from "effect/Array"
|
|
13
|
+
import * as Context from "effect/Context"
|
|
6
14
|
import * as DateTime from "effect/DateTime"
|
|
7
15
|
import * as Effect from "effect/Effect"
|
|
8
|
-
import * as
|
|
16
|
+
import * as Encoding from "effect/Encoding"
|
|
9
17
|
import { dual } from "effect/Function"
|
|
10
18
|
import * as Layer from "effect/Layer"
|
|
19
|
+
import * as Option from "effect/Option"
|
|
11
20
|
import * as Predicate from "effect/Predicate"
|
|
12
21
|
import * as Redactable from "effect/Redactable"
|
|
13
22
|
import type * as Schema from "effect/Schema"
|
|
14
23
|
import * as SchemaAST from "effect/SchemaAST"
|
|
15
|
-
import * as ServiceMap from "effect/ServiceMap"
|
|
16
24
|
import * as Stream from "effect/Stream"
|
|
17
25
|
import type { Span } from "effect/Tracer"
|
|
18
26
|
import type { DeepMutable, Mutable, Simplify } from "effect/Types"
|
|
@@ -37,12 +45,19 @@ import { type ChatStreamingResponseChunkData, OpenRouterClient } from "./OpenRou
|
|
|
37
45
|
// =============================================================================
|
|
38
46
|
|
|
39
47
|
/**
|
|
40
|
-
*
|
|
48
|
+
* Context service for OpenRouter language model configuration.
|
|
49
|
+
*
|
|
50
|
+
* **When to use**
|
|
51
|
+
*
|
|
52
|
+
* Use to provide scoped OpenRouter chat completion defaults or per-operation
|
|
53
|
+
* overrides for an OpenRouter language model service.
|
|
54
|
+
*
|
|
55
|
+
* @see {@link withConfigOverride} for scoping language model request overrides
|
|
41
56
|
*
|
|
42
|
-
* @since 1.0.0
|
|
43
57
|
* @category services
|
|
58
|
+
* @since 4.0.0
|
|
44
59
|
*/
|
|
45
|
-
export class Config extends
|
|
60
|
+
export class Config extends Context.Service<
|
|
46
61
|
Config,
|
|
47
62
|
Simplify<
|
|
48
63
|
& Partial<
|
|
@@ -68,14 +83,20 @@ export class Config extends ServiceMap.Service<
|
|
|
68
83
|
// =============================================================================
|
|
69
84
|
|
|
70
85
|
/**
|
|
71
|
-
*
|
|
86
|
+
* OpenRouter assistant reasoning detail blocks preserved for multi-turn
|
|
87
|
+
* conversations.
|
|
88
|
+
*
|
|
72
89
|
* @category models
|
|
90
|
+
* @since 4.0.0
|
|
73
91
|
*/
|
|
74
92
|
export type ReasoningDetails = Exclude<typeof Generated.AssistantMessage.Encoded["reasoning_details"], undefined>
|
|
75
93
|
|
|
76
94
|
/**
|
|
77
|
-
*
|
|
95
|
+
* File annotations emitted on OpenRouter assistant messages and exposed in
|
|
96
|
+
* finish metadata.
|
|
97
|
+
*
|
|
78
98
|
* @category models
|
|
99
|
+
* @since 4.0.0
|
|
79
100
|
*/
|
|
80
101
|
export type FileAnnotation = Extract<
|
|
81
102
|
NonNullable<typeof Generated.AssistantMessage.fields.annotations.Type>[number],
|
|
@@ -83,7 +104,21 @@ export type FileAnnotation = Extract<
|
|
|
83
104
|
>
|
|
84
105
|
|
|
85
106
|
declare module "effect/unstable/ai/Prompt" {
|
|
107
|
+
/**
|
|
108
|
+
* OpenRouter-specific options for system messages.
|
|
109
|
+
*
|
|
110
|
+
* **Details**
|
|
111
|
+
*
|
|
112
|
+
* These options are used when translating system instructions into
|
|
113
|
+
* OpenRouter chat messages.
|
|
114
|
+
*
|
|
115
|
+
* @category request
|
|
116
|
+
* @since 4.0.0
|
|
117
|
+
*/
|
|
86
118
|
export interface SystemMessageOptions extends ProviderOptions {
|
|
119
|
+
/**
|
|
120
|
+
* Provider-specific options sent to OpenRouter for the system message.
|
|
121
|
+
*/
|
|
87
122
|
readonly openrouter?: {
|
|
88
123
|
/**
|
|
89
124
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -92,7 +127,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
92
127
|
} | null
|
|
93
128
|
}
|
|
94
129
|
|
|
130
|
+
/**
|
|
131
|
+
* OpenRouter-specific options for user messages.
|
|
132
|
+
*
|
|
133
|
+
* **Details**
|
|
134
|
+
*
|
|
135
|
+
* These options are used when translating user content into OpenRouter chat
|
|
136
|
+
* messages.
|
|
137
|
+
*
|
|
138
|
+
* @category request
|
|
139
|
+
* @since 4.0.0
|
|
140
|
+
*/
|
|
95
141
|
export interface UserMessageOptions extends ProviderOptions {
|
|
142
|
+
/**
|
|
143
|
+
* Provider-specific options sent to OpenRouter for the user message.
|
|
144
|
+
*/
|
|
96
145
|
readonly openrouter?: {
|
|
97
146
|
/**
|
|
98
147
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -101,7 +150,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
101
150
|
} | null
|
|
102
151
|
}
|
|
103
152
|
|
|
153
|
+
/**
|
|
154
|
+
* OpenRouter-specific options for assistant messages.
|
|
155
|
+
*
|
|
156
|
+
* **Details**
|
|
157
|
+
*
|
|
158
|
+
* Preserves reasoning metadata when assistant messages are replayed in later
|
|
159
|
+
* OpenRouter requests.
|
|
160
|
+
*
|
|
161
|
+
* @category request
|
|
162
|
+
* @since 4.0.0
|
|
163
|
+
*/
|
|
104
164
|
export interface AssistantMessageOptions extends ProviderOptions {
|
|
165
|
+
/**
|
|
166
|
+
* Provider-specific options sent to OpenRouter for the assistant message.
|
|
167
|
+
*/
|
|
105
168
|
readonly openrouter?: {
|
|
106
169
|
/**
|
|
107
170
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -114,7 +177,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
114
177
|
} | null
|
|
115
178
|
}
|
|
116
179
|
|
|
180
|
+
/**
|
|
181
|
+
* OpenRouter-specific options for tool messages.
|
|
182
|
+
*
|
|
183
|
+
* **Details**
|
|
184
|
+
*
|
|
185
|
+
* These options are used when converting tool results into OpenRouter chat
|
|
186
|
+
* messages.
|
|
187
|
+
*
|
|
188
|
+
* @category request
|
|
189
|
+
* @since 4.0.0
|
|
190
|
+
*/
|
|
117
191
|
export interface ToolMessageOptions extends ProviderOptions {
|
|
192
|
+
/**
|
|
193
|
+
* Provider-specific options sent to OpenRouter for the tool message.
|
|
194
|
+
*/
|
|
118
195
|
readonly openrouter?: {
|
|
119
196
|
/**
|
|
120
197
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -123,7 +200,20 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
123
200
|
} | null
|
|
124
201
|
}
|
|
125
202
|
|
|
203
|
+
/**
|
|
204
|
+
* OpenRouter-specific options for text prompt parts.
|
|
205
|
+
*
|
|
206
|
+
* **When to use**
|
|
207
|
+
*
|
|
208
|
+
* Use when you use these options to control how text content is sent to OpenRouter.
|
|
209
|
+
*
|
|
210
|
+
* @category request
|
|
211
|
+
* @since 4.0.0
|
|
212
|
+
*/
|
|
126
213
|
export interface TextPartOptions extends ProviderOptions {
|
|
214
|
+
/**
|
|
215
|
+
* Provider-specific options sent to OpenRouter for the text part.
|
|
216
|
+
*/
|
|
127
217
|
readonly openrouter?: {
|
|
128
218
|
/**
|
|
129
219
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -132,7 +222,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
132
222
|
} | null
|
|
133
223
|
}
|
|
134
224
|
|
|
225
|
+
/**
|
|
226
|
+
* OpenRouter-specific options for reasoning prompt parts.
|
|
227
|
+
*
|
|
228
|
+
* **Details**
|
|
229
|
+
*
|
|
230
|
+
* Preserves provider reasoning blocks so reasoning-aware conversations can
|
|
231
|
+
* continue across OpenRouter requests.
|
|
232
|
+
*
|
|
233
|
+
* @category request
|
|
234
|
+
* @since 4.0.0
|
|
235
|
+
*/
|
|
135
236
|
export interface ReasoningPartOptions extends ProviderOptions {
|
|
237
|
+
/**
|
|
238
|
+
* Provider-specific options sent to OpenRouter for the reasoning part.
|
|
239
|
+
*/
|
|
136
240
|
readonly openrouter?: {
|
|
137
241
|
/**
|
|
138
242
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -145,7 +249,20 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
145
249
|
} | null
|
|
146
250
|
}
|
|
147
251
|
|
|
252
|
+
/**
|
|
253
|
+
* OpenRouter-specific options for file prompt parts.
|
|
254
|
+
*
|
|
255
|
+
* **Details**
|
|
256
|
+
*
|
|
257
|
+
* Controls file naming and prompt caching for files sent to OpenRouter.
|
|
258
|
+
*
|
|
259
|
+
* @category request
|
|
260
|
+
* @since 4.0.0
|
|
261
|
+
*/
|
|
148
262
|
export interface FilePartOptions extends ProviderOptions {
|
|
263
|
+
/**
|
|
264
|
+
* Provider-specific options sent to OpenRouter for the file part.
|
|
265
|
+
*/
|
|
149
266
|
readonly openrouter?: {
|
|
150
267
|
/**
|
|
151
268
|
* The name to give to the file. Will be prioritized over the file name
|
|
@@ -159,7 +276,21 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
159
276
|
} | null
|
|
160
277
|
}
|
|
161
278
|
|
|
279
|
+
/**
|
|
280
|
+
* OpenRouter-specific options for tool call prompt parts.
|
|
281
|
+
*
|
|
282
|
+
* **Details**
|
|
283
|
+
*
|
|
284
|
+
* Preserves reasoning details associated with tool calls when a conversation
|
|
285
|
+
* is sent back to OpenRouter.
|
|
286
|
+
*
|
|
287
|
+
* @category request
|
|
288
|
+
* @since 4.0.0
|
|
289
|
+
*/
|
|
162
290
|
export interface ToolCallPartOptions extends ProviderOptions {
|
|
291
|
+
/**
|
|
292
|
+
* Provider-specific options sent to OpenRouter for the tool call part.
|
|
293
|
+
*/
|
|
163
294
|
readonly openrouter?: {
|
|
164
295
|
/**
|
|
165
296
|
* Reasoning details associated with the tool call part.
|
|
@@ -168,7 +299,20 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
168
299
|
} | null
|
|
169
300
|
}
|
|
170
301
|
|
|
302
|
+
/**
|
|
303
|
+
* OpenRouter-specific options for tool result prompt parts.
|
|
304
|
+
*
|
|
305
|
+
* **Details**
|
|
306
|
+
*
|
|
307
|
+
* Controls prompt caching for tool results sent to OpenRouter.
|
|
308
|
+
*
|
|
309
|
+
* @category request
|
|
310
|
+
* @since 4.0.0
|
|
311
|
+
*/
|
|
171
312
|
export interface ToolResultPartOptions extends ProviderOptions {
|
|
313
|
+
/**
|
|
314
|
+
* Provider-specific options sent to OpenRouter for the tool result part.
|
|
315
|
+
*/
|
|
172
316
|
readonly openrouter?: {
|
|
173
317
|
/**
|
|
174
318
|
* A breakpoint which marks the end of reusable content eligible for caching.
|
|
@@ -179,43 +323,157 @@ declare module "effect/unstable/ai/Prompt" {
|
|
|
179
323
|
}
|
|
180
324
|
|
|
181
325
|
declare module "effect/unstable/ai/Response" {
|
|
326
|
+
/**
|
|
327
|
+
* OpenRouter metadata attached to completed reasoning response parts.
|
|
328
|
+
*
|
|
329
|
+
* **Details**
|
|
330
|
+
*
|
|
331
|
+
* Preserves provider reasoning details that can be sent back in later turns.
|
|
332
|
+
*
|
|
333
|
+
* @category response
|
|
334
|
+
* @since 4.0.0
|
|
335
|
+
*/
|
|
182
336
|
export interface ReasoningPartMetadata extends ProviderMetadata {
|
|
337
|
+
/**
|
|
338
|
+
* Provider-specific metadata returned for the reasoning part.
|
|
339
|
+
*/
|
|
183
340
|
readonly openrouter?: {
|
|
341
|
+
/**
|
|
342
|
+
* Reasoning details emitted by the underlying provider for this part.
|
|
343
|
+
*/
|
|
184
344
|
readonly reasoningDetails?: ReasoningDetails | null
|
|
185
345
|
} | null
|
|
186
346
|
}
|
|
187
347
|
|
|
348
|
+
/**
|
|
349
|
+
* OpenRouter metadata emitted when a streamed reasoning part starts.
|
|
350
|
+
*
|
|
351
|
+
* **Details**
|
|
352
|
+
*
|
|
353
|
+
* Carries the first reasoning detail chunk when OpenRouter exposes one.
|
|
354
|
+
*
|
|
355
|
+
* @category response
|
|
356
|
+
* @since 4.0.0
|
|
357
|
+
*/
|
|
188
358
|
export interface ReasoningStartPartMetadata extends ProviderMetadata {
|
|
359
|
+
/**
|
|
360
|
+
* Provider-specific metadata returned for the streamed reasoning start.
|
|
361
|
+
*/
|
|
189
362
|
readonly openrouter?: {
|
|
363
|
+
/**
|
|
364
|
+
* Reasoning details emitted by the underlying provider for this part.
|
|
365
|
+
*/
|
|
190
366
|
readonly reasoningDetails?: ReasoningDetails | null
|
|
191
367
|
} | null
|
|
192
368
|
}
|
|
193
369
|
|
|
370
|
+
/**
|
|
371
|
+
* OpenRouter metadata emitted for streamed reasoning deltas.
|
|
372
|
+
*
|
|
373
|
+
* **Details**
|
|
374
|
+
*
|
|
375
|
+
* Carries provider reasoning detail chunks as they arrive from OpenRouter.
|
|
376
|
+
*
|
|
377
|
+
* @category response
|
|
378
|
+
* @since 4.0.0
|
|
379
|
+
*/
|
|
194
380
|
export interface ReasoningDeltaPartMetadata extends ProviderMetadata {
|
|
381
|
+
/**
|
|
382
|
+
* Provider-specific metadata returned for the streamed reasoning delta.
|
|
383
|
+
*/
|
|
195
384
|
readonly openrouter?: {
|
|
385
|
+
/**
|
|
386
|
+
* Reasoning details emitted by the underlying provider for this delta.
|
|
387
|
+
*/
|
|
196
388
|
readonly reasoningDetails?: ReasoningDetails | null
|
|
197
389
|
} | null
|
|
198
390
|
}
|
|
199
391
|
|
|
392
|
+
/**
|
|
393
|
+
* OpenRouter metadata attached to tool-call response parts.
|
|
394
|
+
*
|
|
395
|
+
* **Details**
|
|
396
|
+
*
|
|
397
|
+
* Associates tool calls with provider reasoning details when the model emits
|
|
398
|
+
* reasoning and tool calls together.
|
|
399
|
+
*
|
|
400
|
+
* @category response
|
|
401
|
+
* @since 4.0.0
|
|
402
|
+
*/
|
|
200
403
|
export interface ToolCallPartMetadata extends ProviderMetadata {
|
|
404
|
+
/**
|
|
405
|
+
* Provider-specific metadata returned for the tool call.
|
|
406
|
+
*/
|
|
201
407
|
readonly openrouter?: {
|
|
408
|
+
/**
|
|
409
|
+
* Reasoning details associated with this tool call.
|
|
410
|
+
*/
|
|
202
411
|
readonly reasoningDetails?: ReasoningDetails | null
|
|
203
412
|
} | null
|
|
204
413
|
}
|
|
205
414
|
|
|
415
|
+
/**
|
|
416
|
+
* OpenRouter metadata attached to URL source citations.
|
|
417
|
+
*
|
|
418
|
+
* **Details**
|
|
419
|
+
*
|
|
420
|
+
* Includes citation text and offsets returned by providers that support URL
|
|
421
|
+
* annotations.
|
|
422
|
+
*
|
|
423
|
+
* @category response
|
|
424
|
+
* @since 4.0.0
|
|
425
|
+
*/
|
|
206
426
|
export interface UrlSourcePartMetadata extends ProviderMetadata {
|
|
427
|
+
/**
|
|
428
|
+
* Provider-specific citation metadata returned for the URL source.
|
|
429
|
+
*/
|
|
207
430
|
readonly openrouter?: {
|
|
431
|
+
/**
|
|
432
|
+
* The cited source content returned by the provider.
|
|
433
|
+
*/
|
|
208
434
|
readonly content?: string | null
|
|
435
|
+
/**
|
|
436
|
+
* The zero-based start index of the citation in the generated text.
|
|
437
|
+
*/
|
|
209
438
|
readonly startIndex?: number | null
|
|
439
|
+
/**
|
|
440
|
+
* The zero-based end index of the citation in the generated text.
|
|
441
|
+
*/
|
|
210
442
|
readonly endIndex?: number | null
|
|
211
443
|
} | null
|
|
212
444
|
}
|
|
213
445
|
|
|
446
|
+
/**
|
|
447
|
+
* OpenRouter metadata attached to finish response parts.
|
|
448
|
+
*
|
|
449
|
+
* **Details**
|
|
450
|
+
*
|
|
451
|
+
* Exposes provider response details that are not represented by the common
|
|
452
|
+
* Effect AI finish part fields.
|
|
453
|
+
*
|
|
454
|
+
* @category response
|
|
455
|
+
* @since 4.0.0
|
|
456
|
+
*/
|
|
214
457
|
export interface FinishPartMetadata extends ProviderMetadata {
|
|
458
|
+
/**
|
|
459
|
+
* Provider-specific metadata returned when the OpenRouter response finishes.
|
|
460
|
+
*/
|
|
215
461
|
readonly openrouter?: {
|
|
462
|
+
/**
|
|
463
|
+
* Provider fingerprint for the backend configuration that served the request.
|
|
464
|
+
*/
|
|
216
465
|
readonly systemFingerprint?: string | null
|
|
466
|
+
/**
|
|
467
|
+
* Raw token usage reported by OpenRouter.
|
|
468
|
+
*/
|
|
217
469
|
readonly usage?: typeof Generated.ChatGenerationTokenUsage.Encoded | null
|
|
470
|
+
/**
|
|
471
|
+
* File annotations returned by the provider.
|
|
472
|
+
*/
|
|
218
473
|
readonly annotations?: ReadonlyArray<FileAnnotation> | null
|
|
474
|
+
/**
|
|
475
|
+
* The OpenRouter provider that served the request, when reported.
|
|
476
|
+
*/
|
|
219
477
|
readonly provider?: string | null
|
|
220
478
|
} | null
|
|
221
479
|
}
|
|
@@ -226,20 +484,59 @@ declare module "effect/unstable/ai/Response" {
|
|
|
226
484
|
// =============================================================================
|
|
227
485
|
|
|
228
486
|
/**
|
|
229
|
-
*
|
|
487
|
+
* Creates an OpenRouter model descriptor that can be provided with
|
|
488
|
+
* `Effect.provide`.
|
|
489
|
+
*
|
|
490
|
+
* **When to use**
|
|
491
|
+
*
|
|
492
|
+
* Use when you want an OpenRouter language model value that carries provider
|
|
493
|
+
* and model metadata and can be supplied directly to an Effect program.
|
|
494
|
+
*
|
|
495
|
+
* **Details**
|
|
496
|
+
*
|
|
497
|
+
* The returned model requires `OpenRouterClient` and provides
|
|
498
|
+
* `LanguageModel.LanguageModel`.
|
|
499
|
+
*
|
|
500
|
+
* @see {@link layer} for creating a `LanguageModel.LanguageModel` layer directly
|
|
501
|
+
* @see {@link make} for constructing the language model service effectfully
|
|
502
|
+
* @see {@link withConfigOverride} for scoping OpenRouter request overrides
|
|
503
|
+
*
|
|
230
504
|
* @category constructors
|
|
505
|
+
* @since 4.0.0
|
|
231
506
|
*/
|
|
232
507
|
export const model = (
|
|
233
508
|
model: string,
|
|
234
509
|
config?: Omit<typeof Config.Service, "model">
|
|
235
510
|
): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenRouterClient> =>
|
|
236
|
-
AiModel.make("openai", layer({ model, config }))
|
|
511
|
+
AiModel.make("openai", model, layer({ model, config }))
|
|
237
512
|
|
|
238
513
|
/**
|
|
239
|
-
* Creates an OpenRouter
|
|
514
|
+
* Creates an OpenRouter `LanguageModel` service from a model identifier and
|
|
515
|
+
* optional request defaults.
|
|
516
|
+
*
|
|
517
|
+
* **When to use**
|
|
518
|
+
*
|
|
519
|
+
* Use when you need to construct a `LanguageModel.Service` value backed by
|
|
520
|
+
* `OpenRouterClient` inside an Effect.
|
|
521
|
+
*
|
|
522
|
+
* **Details**
|
|
523
|
+
*
|
|
524
|
+
* The returned effect requires `OpenRouterClient`. Request defaults from the
|
|
525
|
+
* `config` option are merged with any `Config` service in the context, with
|
|
526
|
+
* context values taking precedence. The service supports both `generateText`
|
|
527
|
+
* and `streamText`.
|
|
528
|
+
*
|
|
529
|
+
* **Gotchas**
|
|
530
|
+
*
|
|
531
|
+
* Provider-defined tools are not supported by this provider integration;
|
|
532
|
+
* requests that include them fail with an `InvalidUserInputError`.
|
|
533
|
+
*
|
|
534
|
+
* @see {@link layer} for providing the service as a `Layer`
|
|
535
|
+
* @see {@link model} for creating a model descriptor for `Effect.provide`
|
|
536
|
+
* @see {@link withConfigOverride} for scoping request defaults around operations
|
|
240
537
|
*
|
|
241
|
-
* @since 1.0.0
|
|
242
538
|
* @category constructors
|
|
539
|
+
* @since 4.0.0
|
|
243
540
|
*/
|
|
244
541
|
export const make = Effect.fnUntraced(function*({ model, config: providerConfig }: {
|
|
245
542
|
readonly model: string
|
|
@@ -249,7 +546,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
|
|
|
249
546
|
const codecTransformer = getCodecTransformer(model)
|
|
250
547
|
|
|
251
548
|
const makeConfig = Effect.gen(function*() {
|
|
252
|
-
const services = yield* Effect.
|
|
549
|
+
const services = yield* Effect.context<never>()
|
|
253
550
|
return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
|
|
254
551
|
})
|
|
255
552
|
|
|
@@ -273,6 +570,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
|
|
|
273
570
|
)
|
|
274
571
|
|
|
275
572
|
return yield* LanguageModel.make({
|
|
573
|
+
codecTransformer: toCodecOpenAI,
|
|
276
574
|
generateText: Effect.fnUntraced(
|
|
277
575
|
function*(options) {
|
|
278
576
|
const config = yield* makeConfig
|
|
@@ -300,17 +598,23 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
|
|
|
300
598
|
})
|
|
301
599
|
)
|
|
302
600
|
)
|
|
303
|
-
})
|
|
304
|
-
LanguageModel.CurrentCodecTransformer,
|
|
305
|
-
codecTransformer
|
|
306
|
-
))
|
|
601
|
+
})
|
|
307
602
|
})
|
|
308
603
|
|
|
309
604
|
/**
|
|
310
605
|
* Creates a layer for the OpenRouter language model.
|
|
311
606
|
*
|
|
312
|
-
*
|
|
607
|
+
* **When to use**
|
|
608
|
+
*
|
|
609
|
+
* Use when composing application layers and you want OpenRouter to satisfy
|
|
610
|
+
* `LanguageModel.LanguageModel` while supplying `OpenRouterClient` from another
|
|
611
|
+
* layer.
|
|
612
|
+
*
|
|
613
|
+
* @see {@link make} for constructing the language model service effectfully
|
|
614
|
+
* @see {@link model} for creating a model descriptor for `Effect.provide`
|
|
615
|
+
*
|
|
313
616
|
* @category layers
|
|
617
|
+
* @since 4.0.0
|
|
314
618
|
*/
|
|
315
619
|
export const layer = (options: {
|
|
316
620
|
readonly model: string
|
|
@@ -321,37 +625,107 @@ export const layer = (options: {
|
|
|
321
625
|
/**
|
|
322
626
|
* Provides config overrides for OpenRouter language model operations.
|
|
323
627
|
*
|
|
324
|
-
*
|
|
628
|
+
* **When to use**
|
|
629
|
+
*
|
|
630
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
631
|
+
* the model's default configuration.
|
|
632
|
+
*
|
|
633
|
+
* **Details**
|
|
634
|
+
*
|
|
635
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
636
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
637
|
+
* config, and the helper supports both pipe form and
|
|
638
|
+
* `withConfigOverride(effect, overrides)`.
|
|
639
|
+
*
|
|
640
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
641
|
+
*
|
|
325
642
|
* @category configuration
|
|
643
|
+
* @since 4.0.0
|
|
326
644
|
*/
|
|
327
645
|
export const withConfigOverride: {
|
|
328
646
|
/**
|
|
329
647
|
* Provides config overrides for OpenRouter language model operations.
|
|
330
648
|
*
|
|
331
|
-
*
|
|
649
|
+
* **When to use**
|
|
650
|
+
*
|
|
651
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
652
|
+
* the model's default configuration.
|
|
653
|
+
*
|
|
654
|
+
* **Details**
|
|
655
|
+
*
|
|
656
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
657
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
658
|
+
* config, and the helper supports both pipe form and
|
|
659
|
+
* `withConfigOverride(effect, overrides)`.
|
|
660
|
+
*
|
|
661
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
662
|
+
*
|
|
332
663
|
* @category configuration
|
|
664
|
+
* @since 4.0.0
|
|
333
665
|
*/
|
|
334
666
|
(overrides: typeof Config.Service): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>
|
|
335
667
|
/**
|
|
336
668
|
* Provides config overrides for OpenRouter language model operations.
|
|
337
669
|
*
|
|
338
|
-
*
|
|
670
|
+
* **When to use**
|
|
671
|
+
*
|
|
672
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
673
|
+
* the model's default configuration.
|
|
674
|
+
*
|
|
675
|
+
* **Details**
|
|
676
|
+
*
|
|
677
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
678
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
679
|
+
* config, and the helper supports both pipe form and
|
|
680
|
+
* `withConfigOverride(effect, overrides)`.
|
|
681
|
+
*
|
|
682
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
683
|
+
*
|
|
339
684
|
* @category configuration
|
|
685
|
+
* @since 4.0.0
|
|
340
686
|
*/
|
|
341
687
|
<A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service): Effect.Effect<A, E, Exclude<R, Config>>
|
|
342
688
|
} = dual<
|
|
343
689
|
/**
|
|
344
690
|
* Provides config overrides for OpenRouter language model operations.
|
|
345
691
|
*
|
|
346
|
-
*
|
|
692
|
+
* **When to use**
|
|
693
|
+
*
|
|
694
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
695
|
+
* the model's default configuration.
|
|
696
|
+
*
|
|
697
|
+
* **Details**
|
|
698
|
+
*
|
|
699
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
700
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
701
|
+
* config, and the helper supports both pipe form and
|
|
702
|
+
* `withConfigOverride(effect, overrides)`.
|
|
703
|
+
*
|
|
704
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
705
|
+
*
|
|
347
706
|
* @category configuration
|
|
707
|
+
* @since 4.0.0
|
|
348
708
|
*/
|
|
349
709
|
(overrides: typeof Config.Service) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Config>>,
|
|
350
710
|
/**
|
|
351
711
|
* Provides config overrides for OpenRouter language model operations.
|
|
352
712
|
*
|
|
353
|
-
*
|
|
713
|
+
* **When to use**
|
|
714
|
+
*
|
|
715
|
+
* Use to apply OpenRouter request configuration to one effect without changing
|
|
716
|
+
* the model's default configuration.
|
|
717
|
+
*
|
|
718
|
+
* **Details**
|
|
719
|
+
*
|
|
720
|
+
* The overrides are merged with any existing `Config` service for the duration
|
|
721
|
+
* of the supplied effect. Fields in `overrides` take precedence over existing
|
|
722
|
+
* config, and the helper supports both pipe form and
|
|
723
|
+
* `withConfigOverride(effect, overrides)`.
|
|
724
|
+
*
|
|
725
|
+
* @see {@link Config} for available OpenRouter request configuration fields
|
|
726
|
+
*
|
|
354
727
|
* @category configuration
|
|
728
|
+
* @since 4.0.0
|
|
355
729
|
*/
|
|
356
730
|
<A, E, R>(self: Effect.Effect<A, E, R>, overrides: typeof Config.Service) => Effect.Effect<A, E, Exclude<R, Config>>
|
|
357
731
|
>(2, (self, overrides) =>
|
|
@@ -451,7 +825,7 @@ const prepareMessages = Effect.fnUntraced(
|
|
|
451
825
|
url: part.data instanceof URL
|
|
452
826
|
? part.data.toString()
|
|
453
827
|
: part.data instanceof Uint8Array
|
|
454
|
-
? `data:${mediaType};base64,${
|
|
828
|
+
? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}`
|
|
455
829
|
: part.data
|
|
456
830
|
},
|
|
457
831
|
...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined)
|
|
@@ -460,6 +834,45 @@ const prepareMessages = Effect.fnUntraced(
|
|
|
460
834
|
break
|
|
461
835
|
}
|
|
462
836
|
|
|
837
|
+
if (part.mediaType.startsWith("audio/")) {
|
|
838
|
+
const format = audioFormats[part.mediaType.toLowerCase()]
|
|
839
|
+
|
|
840
|
+
if (Predicate.isUndefined(format)) {
|
|
841
|
+
return yield* AiError.make({
|
|
842
|
+
module: "OpenRouterLanguageModel",
|
|
843
|
+
method: "prepareMessages",
|
|
844
|
+
reason: new AiError.InvalidUserInputError({
|
|
845
|
+
description: `Detected unsupported media type for audio file: '${part.mediaType}' ` +
|
|
846
|
+
`- OpenRouter supports ${supportedAudioFormats} audio`
|
|
847
|
+
})
|
|
848
|
+
})
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (part.data instanceof URL) {
|
|
852
|
+
return yield* AiError.make({
|
|
853
|
+
module: "OpenRouterLanguageModel",
|
|
854
|
+
method: "prepareMessages",
|
|
855
|
+
reason: new AiError.InvalidUserInputError({
|
|
856
|
+
description: "Detected URL data for audio file - OpenRouter requires " +
|
|
857
|
+
"audio to be provided as base64-encoded data"
|
|
858
|
+
})
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
content.push({
|
|
863
|
+
type: "input_audio",
|
|
864
|
+
input_audio: {
|
|
865
|
+
data: part.data instanceof Uint8Array
|
|
866
|
+
? Encoding.encodeBase64(part.data)
|
|
867
|
+
: getBase64FromDataUrl(part.data),
|
|
868
|
+
format
|
|
869
|
+
},
|
|
870
|
+
...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined)
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
break
|
|
874
|
+
}
|
|
875
|
+
|
|
463
876
|
const options = part.options.openrouter
|
|
464
877
|
const fileName = options?.fileName ?? part.fileName ?? ""
|
|
465
878
|
|
|
@@ -470,7 +883,7 @@ const prepareMessages = Effect.fnUntraced(
|
|
|
470
883
|
file_data: part.data instanceof URL
|
|
471
884
|
? part.data.toString()
|
|
472
885
|
: part.data instanceof Uint8Array
|
|
473
|
-
? `data:${part.mediaType};base64,${
|
|
886
|
+
? `data:${part.mediaType};base64,${Encoding.encodeBase64(part.data)}`
|
|
474
887
|
: part.data
|
|
475
888
|
},
|
|
476
889
|
...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined)
|
|
@@ -588,7 +1001,7 @@ const buildHttpRequestDetails = (
|
|
|
588
1001
|
method: request.method,
|
|
589
1002
|
url: request.url,
|
|
590
1003
|
urlParams: Array.from(request.urlParams),
|
|
591
|
-
hash: request.hash,
|
|
1004
|
+
hash: Option.getOrUndefined(request.hash),
|
|
592
1005
|
headers: Redactable.redact(request.headers) as Record<string, string>
|
|
593
1006
|
})
|
|
594
1007
|
|
|
@@ -830,12 +1243,12 @@ const makeStreamResponse = Effect.fnUntraced(
|
|
|
830
1243
|
let activeTextId: string | undefined = undefined
|
|
831
1244
|
|
|
832
1245
|
let totalToolCalls = 0
|
|
833
|
-
const activeToolCalls:
|
|
1246
|
+
const activeToolCalls: Record<number, {
|
|
834
1247
|
readonly id: string
|
|
835
1248
|
readonly type: "function"
|
|
836
1249
|
readonly name: string
|
|
837
1250
|
params: string
|
|
838
|
-
}> =
|
|
1251
|
+
}> = {}
|
|
839
1252
|
|
|
840
1253
|
// Track reasoning details to preserve for multi-turn conversations
|
|
841
1254
|
const accumulatedReasoningDetails: DeepMutable<ReasoningDetails> = []
|
|
@@ -885,283 +1298,275 @@ const makeStreamResponse = Effect.fnUntraced(
|
|
|
885
1298
|
}
|
|
886
1299
|
|
|
887
1300
|
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
|
-
}
|
|
1301
|
+
if (Predicate.isNotUndefined(choice)) {
|
|
1302
|
+
if (Predicate.isNotNullish(choice.finish_reason)) {
|
|
1303
|
+
finishReason = resolveFinishReason(choice.finish_reason)
|
|
1304
|
+
}
|
|
901
1305
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1306
|
+
const delta = choice.delta
|
|
1307
|
+
if (Predicate.isNullish(delta)) {
|
|
1308
|
+
return parts
|
|
1309
|
+
}
|
|
906
1310
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1311
|
+
const emitReasoning = Effect.fnUntraced(
|
|
1312
|
+
function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) {
|
|
1313
|
+
if (!reasoningStarted) {
|
|
1314
|
+
activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
1315
|
+
parts.push({
|
|
1316
|
+
type: "reasoning-start",
|
|
1317
|
+
id: activeReasoningId,
|
|
1318
|
+
metadata
|
|
1319
|
+
})
|
|
1320
|
+
reasoningStarted = true
|
|
1321
|
+
}
|
|
911
1322
|
parts.push({
|
|
912
|
-
type: "reasoning-
|
|
913
|
-
id: activeReasoningId
|
|
1323
|
+
type: "reasoning-delta",
|
|
1324
|
+
id: activeReasoningId!,
|
|
1325
|
+
delta,
|
|
914
1326
|
metadata
|
|
915
1327
|
})
|
|
916
|
-
reasoningStarted = true
|
|
917
1328
|
}
|
|
918
|
-
|
|
919
|
-
type: "reasoning-delta",
|
|
920
|
-
id: activeReasoningId!,
|
|
921
|
-
delta,
|
|
922
|
-
metadata
|
|
923
|
-
})
|
|
924
|
-
}
|
|
925
|
-
)
|
|
1329
|
+
)
|
|
926
1330
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1331
|
+
const reasoningDetails = delta.reasoning_details
|
|
1332
|
+
if (Predicate.isNotUndefined(reasoningDetails) && reasoningDetails.length > 0) {
|
|
1333
|
+
// Accumulate reasoning_details to preserve for multi-turn conversations
|
|
1334
|
+
// Merge consecutive reasoning.text items into a single entry
|
|
1335
|
+
for (const detail of reasoningDetails) {
|
|
1336
|
+
if (detail.type === "reasoning.text") {
|
|
1337
|
+
const lastDetail = accumulatedReasoningDetails[accumulatedReasoningDetails.length - 1]
|
|
1338
|
+
if (Predicate.isNotUndefined(lastDetail) && lastDetail.type === "reasoning.text") {
|
|
1339
|
+
// Merge with the previous text detail
|
|
1340
|
+
lastDetail.text = (lastDetail.text ?? "") + (detail.text ?? "")
|
|
1341
|
+
lastDetail.signature = lastDetail.signature ?? detail.signature ?? null
|
|
1342
|
+
lastDetail.format = lastDetail.format ?? detail.format ?? null
|
|
1343
|
+
} else {
|
|
1344
|
+
// Start a new text detail
|
|
1345
|
+
accumulatedReasoningDetails.push({ ...detail })
|
|
1346
|
+
}
|
|
939
1347
|
} else {
|
|
940
|
-
//
|
|
941
|
-
accumulatedReasoningDetails.push(
|
|
1348
|
+
// Non-text details (encrypted, summary) are pushed as-is
|
|
1349
|
+
accumulatedReasoningDetails.push(detail)
|
|
942
1350
|
}
|
|
943
|
-
} else {
|
|
944
|
-
// Non-text details (encrypted, summary) are pushed as-is
|
|
945
|
-
accumulatedReasoningDetails.push(detail)
|
|
946
1351
|
}
|
|
947
|
-
}
|
|
948
1352
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1353
|
+
// Emit reasoning_details in providerMetadata for each delta chunk
|
|
1354
|
+
// so users can accumulate them on their end before sending back
|
|
1355
|
+
const metadata: Response.ReasoningDeltaPart["metadata"] = {
|
|
1356
|
+
openrouter: {
|
|
1357
|
+
reasoningDetails
|
|
1358
|
+
}
|
|
954
1359
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1360
|
+
for (const detail of reasoningDetails) {
|
|
1361
|
+
switch (detail.type) {
|
|
1362
|
+
case "reasoning.text": {
|
|
1363
|
+
if (Predicate.isNotNullish(detail.text)) {
|
|
1364
|
+
yield* emitReasoning(detail.text, metadata)
|
|
1365
|
+
}
|
|
1366
|
+
break
|
|
961
1367
|
}
|
|
962
|
-
break
|
|
963
|
-
}
|
|
964
1368
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1369
|
+
case "reasoning.summary": {
|
|
1370
|
+
if (Predicate.isNotNullish(detail.summary)) {
|
|
1371
|
+
yield* emitReasoning(detail.summary, metadata)
|
|
1372
|
+
}
|
|
1373
|
+
break
|
|
968
1374
|
}
|
|
969
|
-
break
|
|
970
|
-
}
|
|
971
1375
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1376
|
+
case "reasoning.encrypted": {
|
|
1377
|
+
if (Predicate.isNotNullish(detail.data)) {
|
|
1378
|
+
yield* emitReasoning("[REDACTED]", metadata)
|
|
1379
|
+
}
|
|
1380
|
+
break
|
|
975
1381
|
}
|
|
976
|
-
break
|
|
977
1382
|
}
|
|
978
1383
|
}
|
|
1384
|
+
} else if (Predicate.isNotNullish(delta.reasoning)) {
|
|
1385
|
+
yield* emitReasoning(delta.reasoning)
|
|
979
1386
|
}
|
|
980
|
-
} else if (Predicate.isNotNullish(delta.reasoning)) {
|
|
981
|
-
yield* emitReasoning(delta.reasoning)
|
|
982
|
-
}
|
|
983
1387
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1388
|
+
const content = delta.content
|
|
1389
|
+
if (Predicate.isNotNullish(content)) {
|
|
1390
|
+
// If reasoning was previously active and now we're starting text content,
|
|
1391
|
+
// we should end the reasoning first to maintain proper order
|
|
1392
|
+
if (reasoningStarted && !textStarted) {
|
|
1393
|
+
parts.push({
|
|
1394
|
+
type: "reasoning-end",
|
|
1395
|
+
id: activeReasoningId!,
|
|
1396
|
+
// Include accumulated reasoning_details so the we can update the
|
|
1397
|
+
// reasoning part's provider metadata with the correct signature.
|
|
1398
|
+
// The signature typically arrives in the last reasoning delta,
|
|
1399
|
+
// but reasoning-start only carries the first delta's metadata.
|
|
1400
|
+
metadata: accumulatedReasoningDetails.length > 0
|
|
1401
|
+
? { openRouter: { reasoningDetails: accumulatedReasoningDetails } }
|
|
1402
|
+
: undefined
|
|
1403
|
+
})
|
|
1404
|
+
reasoningStarted = false
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (!textStarted) {
|
|
1408
|
+
activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
1409
|
+
parts.push({
|
|
1410
|
+
type: "text-start",
|
|
1411
|
+
id: activeTextId
|
|
1412
|
+
})
|
|
1413
|
+
textStarted = true
|
|
1414
|
+
}
|
|
1002
1415
|
|
|
1003
|
-
if (!textStarted) {
|
|
1004
|
-
activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
1005
1416
|
parts.push({
|
|
1006
|
-
type: "text-
|
|
1007
|
-
id: activeTextId
|
|
1417
|
+
type: "text-delta",
|
|
1418
|
+
id: activeTextId!,
|
|
1419
|
+
delta: content
|
|
1008
1420
|
})
|
|
1009
|
-
textStarted = true
|
|
1010
1421
|
}
|
|
1011
1422
|
|
|
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)
|
|
1423
|
+
const annotations = delta.annotations
|
|
1424
|
+
if (Predicate.isNotNullish(annotations)) {
|
|
1425
|
+
for (const annotation of annotations) {
|
|
1426
|
+
if (annotation.type === "url_citation") {
|
|
1427
|
+
parts.push({
|
|
1428
|
+
type: "source",
|
|
1429
|
+
sourceType: "url",
|
|
1430
|
+
id: annotation.url_citation.url,
|
|
1431
|
+
url: annotation.url_citation.url,
|
|
1432
|
+
title: annotation.url_citation.title ?? "",
|
|
1433
|
+
metadata: {
|
|
1434
|
+
openrouter: {
|
|
1435
|
+
...(Predicate.isNotUndefined(annotation.url_citation.content)
|
|
1436
|
+
? { content: annotation.url_citation.content }
|
|
1437
|
+
: undefined),
|
|
1438
|
+
...(Predicate.isNotUndefined(annotation.url_citation.start_index)
|
|
1439
|
+
? { startIndex: annotation.url_citation.start_index }
|
|
1440
|
+
: undefined),
|
|
1441
|
+
...(Predicate.isNotUndefined(annotation.url_citation.end_index)
|
|
1442
|
+
? { startIndex: annotation.url_citation.end_index }
|
|
1443
|
+
: undefined)
|
|
1444
|
+
}
|
|
1040
1445
|
}
|
|
1041
|
-
}
|
|
1042
|
-
})
|
|
1043
|
-
|
|
1044
|
-
|
|
1446
|
+
})
|
|
1447
|
+
} else if (annotation.type === "file") {
|
|
1448
|
+
accumulatedFileAnnotations.push(annotation)
|
|
1449
|
+
}
|
|
1045
1450
|
}
|
|
1046
1451
|
}
|
|
1047
|
-
}
|
|
1048
1452
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1453
|
+
const toolCalls = delta.tool_calls
|
|
1454
|
+
if (Predicate.isNotNullish(toolCalls)) {
|
|
1455
|
+
for (const toolCall of toolCalls) {
|
|
1456
|
+
const index = toolCall.index ?? toolCalls.length - 1
|
|
1457
|
+
let activeToolCall = activeToolCalls[index]
|
|
1458
|
+
|
|
1459
|
+
// Tool call start - OpenRouter returns all information except the
|
|
1460
|
+
// tool call parameters in the first chunk
|
|
1461
|
+
if (Predicate.isUndefined(activeToolCall)) {
|
|
1462
|
+
if (toolCall.type !== "function") {
|
|
1463
|
+
return yield* AiError.make({
|
|
1464
|
+
module: "OpenRouterLanguageModel",
|
|
1465
|
+
method: "makeStreamResponse",
|
|
1466
|
+
reason: new AiError.InvalidOutputError({
|
|
1467
|
+
description: "Received tool call delta that was not of type: 'function'"
|
|
1468
|
+
})
|
|
1064
1469
|
})
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1470
|
+
}
|
|
1067
1471
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1472
|
+
if (Predicate.isNullish(toolCall.id)) {
|
|
1473
|
+
return yield* AiError.make({
|
|
1474
|
+
module: "OpenRouterLanguageModel",
|
|
1475
|
+
method: "makeStreamResponse",
|
|
1476
|
+
reason: new AiError.InvalidOutputError({
|
|
1477
|
+
description: "Received tool call delta without a tool call identifier"
|
|
1478
|
+
})
|
|
1074
1479
|
})
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1480
|
+
}
|
|
1077
1481
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1482
|
+
if (Predicate.isNullish(toolCall.function?.name)) {
|
|
1483
|
+
return yield* AiError.make({
|
|
1484
|
+
module: "OpenRouterLanguageModel",
|
|
1485
|
+
method: "makeStreamResponse",
|
|
1486
|
+
reason: new AiError.InvalidOutputError({
|
|
1487
|
+
description: "Received tool call delta without a tool call name"
|
|
1488
|
+
})
|
|
1084
1489
|
})
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1490
|
+
}
|
|
1087
1491
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1492
|
+
activeToolCall = {
|
|
1493
|
+
id: toolCall.id,
|
|
1494
|
+
type: "function",
|
|
1495
|
+
name: toolCall.function.name,
|
|
1496
|
+
params: toolCall.function.arguments ?? ""
|
|
1497
|
+
}
|
|
1094
1498
|
|
|
1095
|
-
|
|
1499
|
+
activeToolCalls[index] = activeToolCall
|
|
1096
1500
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1501
|
+
parts.push({
|
|
1502
|
+
type: "tool-params-start",
|
|
1503
|
+
id: activeToolCall.id,
|
|
1504
|
+
name: activeToolCall.name
|
|
1505
|
+
})
|
|
1102
1506
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1507
|
+
// Emit a tool call delta part if parameters were also sent
|
|
1508
|
+
if (activeToolCall.params.length > 0) {
|
|
1509
|
+
parts.push({
|
|
1510
|
+
type: "tool-params-delta",
|
|
1511
|
+
id: activeToolCall.id,
|
|
1512
|
+
delta: activeToolCall.params
|
|
1513
|
+
})
|
|
1514
|
+
}
|
|
1515
|
+
} else {
|
|
1516
|
+
// If an active tool call was found, update and emit the delta for
|
|
1517
|
+
// the tool call's parameters
|
|
1518
|
+
activeToolCall.params += toolCall.function?.arguments ?? ""
|
|
1105
1519
|
parts.push({
|
|
1106
1520
|
type: "tool-params-delta",
|
|
1107
1521
|
id: activeToolCall.id,
|
|
1108
1522
|
delta: activeToolCall.params
|
|
1109
1523
|
})
|
|
1110
1524
|
}
|
|
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
1525
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1526
|
+
// Check if the tool call is complete
|
|
1527
|
+
// @effect-diagnostics-next-line tryCatchInEffectGen:off
|
|
1528
|
+
try {
|
|
1529
|
+
const params = Tool.unsafeSecureJsonParse(activeToolCall.params)
|
|
1126
1530
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1531
|
+
parts.push({
|
|
1532
|
+
type: "tool-params-end",
|
|
1533
|
+
id: activeToolCall.id
|
|
1534
|
+
})
|
|
1131
1535
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1536
|
+
parts.push({
|
|
1537
|
+
type: "tool-call",
|
|
1538
|
+
id: activeToolCall.id,
|
|
1539
|
+
name: activeToolCall.name,
|
|
1540
|
+
params,
|
|
1541
|
+
// Only attach reasoning_details to the first tool call to avoid
|
|
1542
|
+
// duplicating thinking blocks for parallel tool calls (Claude)
|
|
1543
|
+
metadata: reasoningDetailsAttachedToToolCall ? undefined : {
|
|
1544
|
+
openrouter: { reasoningDetails: accumulatedReasoningDetails }
|
|
1545
|
+
}
|
|
1546
|
+
})
|
|
1143
1547
|
|
|
1144
|
-
|
|
1548
|
+
reasoningDetailsAttachedToToolCall = true
|
|
1145
1549
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1550
|
+
// Increment the total tool calls emitted by the stream and
|
|
1551
|
+
// remove the active tool call
|
|
1552
|
+
totalToolCalls += 1
|
|
1553
|
+
delete activeToolCalls[toolCall.index]
|
|
1554
|
+
} catch {
|
|
1555
|
+
// Tool call incomplete, continue parsing
|
|
1556
|
+
continue
|
|
1557
|
+
}
|
|
1153
1558
|
}
|
|
1154
1559
|
}
|
|
1155
|
-
}
|
|
1156
1560
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1561
|
+
const images = delta.images
|
|
1562
|
+
if (Predicate.isNotNullish(images)) {
|
|
1563
|
+
for (const image of images) {
|
|
1564
|
+
parts.push({
|
|
1565
|
+
type: "file",
|
|
1566
|
+
mediaType: getMediaType(image.image_url.url, "image/jpeg"),
|
|
1567
|
+
data: getBase64FromDataUrl(image.image_url.url)
|
|
1568
|
+
})
|
|
1569
|
+
}
|
|
1165
1570
|
}
|
|
1166
1571
|
}
|
|
1167
1572
|
|
|
@@ -1180,7 +1585,7 @@ const makeStreamResponse = Effect.fnUntraced(
|
|
|
1180
1585
|
|
|
1181
1586
|
// Forward any unsent tool calls if finish reason is 'tool-calls'
|
|
1182
1587
|
if (finishReason === "tool-calls") {
|
|
1183
|
-
for (const toolCall of activeToolCalls) {
|
|
1588
|
+
for (const toolCall of Object.values(activeToolCalls)) {
|
|
1184
1589
|
// Coerce invalid tool call parameters to an empty object
|
|
1185
1590
|
let params: unknown
|
|
1186
1591
|
// @effect-diagnostics-next-line tryCatchInEffectGen:off
|
|
@@ -1439,7 +1844,7 @@ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError
|
|
|
1439
1844
|
})
|
|
1440
1845
|
})
|
|
1441
1846
|
|
|
1442
|
-
const tryJsonSchema = <S extends Schema.
|
|
1847
|
+
const tryJsonSchema = <S extends Schema.Constraint>(
|
|
1443
1848
|
schema: S,
|
|
1444
1849
|
method: string,
|
|
1445
1850
|
transformer: LanguageModel.CodecTransformer
|
|
@@ -1470,6 +1875,32 @@ const getResponseFormat = Effect.fnUntraced(function*({ config, options, transfo
|
|
|
1470
1875
|
return undefined
|
|
1471
1876
|
})
|
|
1472
1877
|
|
|
1878
|
+
/**
|
|
1879
|
+
* Maps audio media types to the formats supported by OpenRouter.
|
|
1880
|
+
*
|
|
1881
|
+
* @see https://openrouter.ai/docs/guides/overview/multimodal/audio
|
|
1882
|
+
*/
|
|
1883
|
+
const audioFormats: Record<string, string> = {
|
|
1884
|
+
"audio/aac": "aac",
|
|
1885
|
+
"audio/aiff": "aiff",
|
|
1886
|
+
"audio/x-aiff": "aiff",
|
|
1887
|
+
"audio/flac": "flac",
|
|
1888
|
+
"audio/x-flac": "flac",
|
|
1889
|
+
"audio/l16": "pcm16",
|
|
1890
|
+
"audio/l24": "pcm24",
|
|
1891
|
+
"audio/m4a": "m4a",
|
|
1892
|
+
"audio/x-m4a": "m4a",
|
|
1893
|
+
"audio/mp4": "m4a",
|
|
1894
|
+
"audio/mp3": "mp3",
|
|
1895
|
+
"audio/mpeg": "mp3",
|
|
1896
|
+
"audio/ogg": "ogg",
|
|
1897
|
+
"audio/wav": "wav",
|
|
1898
|
+
"audio/wave": "wav",
|
|
1899
|
+
"audio/x-wav": "wav"
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
const supportedAudioFormats = Array.from(new Set(Object.values(audioFormats))).join(", ")
|
|
1903
|
+
|
|
1473
1904
|
const getMediaType = (dataUrl: string, defaultMediaType: string): string => {
|
|
1474
1905
|
const match = dataUrl.match(/^data:([^;]+)/)
|
|
1475
1906
|
return match ? (match[1] ?? defaultMediaType) : defaultMediaType
|