@effect/ai-openrouter 4.0.0-beta.7 → 4.0.0-beta.70
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 +46 -13
- package/dist/OpenRouterClient.d.ts.map +1 -1
- package/dist/OpenRouterClient.js +8 -8
- package/dist/OpenRouterClient.js.map +1 -1
- package/dist/OpenRouterConfig.d.ts +47 -10
- package/dist/OpenRouterConfig.d.ts.map +1 -1
- package/dist/OpenRouterConfig.js +33 -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 +272 -19
- package/dist/OpenRouterLanguageModel.d.ts.map +1 -1
- package/dist/OpenRouterLanguageModel.js +257 -240
- package/dist/OpenRouterLanguageModel.js.map +1 -1
- package/dist/index.d.ts +66 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -6
- 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 +47 -14
- package/src/OpenRouterConfig.ts +56 -13
- package/src/OpenRouterError.ts +168 -35
- package/src/OpenRouterLanguageModel.ts +501 -245
- package/src/index.ts +66 -6
- package/src/internal/errors.ts +6 -4
- package/src/internal/utilities.ts +0 -1
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The `OpenRouterLanguageModel` module provides constructors for using
|
|
3
|
+
* OpenRouter chat completion models through the Effect AI `LanguageModel`
|
|
4
|
+
* interface. It adapts Effect prompts, tools, structured output schemas, file
|
|
5
|
+
* parts, reasoning details, cache-control hints, and telemetry annotations into
|
|
6
|
+
* the OpenRouter request and response formats.
|
|
7
|
+
*
|
|
8
|
+
* Use this module when an application wants to select an OpenRouter model by
|
|
9
|
+
* name while keeping the rest of its AI workflow provider-agnostic. The
|
|
10
|
+
* exported layer and model constructors install a `LanguageModel` service backed
|
|
11
|
+
* by `OpenRouterClient`, and `withConfigOverride` can scope per-request
|
|
12
|
+
* OpenRouter options such as sampling, routing, tool use, or JSON schema
|
|
13
|
+
* behavior.
|
|
14
|
+
*
|
|
15
|
+
* OpenRouter routes requests to many underlying providers, so model support for
|
|
16
|
+
* images, files, tools, structured outputs, caching, and reasoning metadata can
|
|
17
|
+
* vary. Provider-specific prompt and response metadata is preserved under the
|
|
18
|
+
* `openrouter` option namespace so multi-turn conversations can round-trip
|
|
19
|
+
* details such as reasoning blocks and file annotations when the selected model
|
|
20
|
+
* supports them.
|
|
21
|
+
*
|
|
22
|
+
* @since 4.0.0
|
|
3
23
|
*/
|
|
4
24
|
/** @effect-diagnostics preferSchemaOverJson:skip-file */
|
|
5
25
|
import * as Arr from "effect/Array";
|
|
26
|
+
import * as Context from "effect/Context";
|
|
6
27
|
import * as DateTime from "effect/DateTime";
|
|
7
28
|
import * as Effect from "effect/Effect";
|
|
8
29
|
import * as Encoding from "effect/Encoding";
|
|
9
30
|
import { dual } from "effect/Function";
|
|
10
31
|
import * as Layer from "effect/Layer";
|
|
32
|
+
import * as Option from "effect/Option";
|
|
11
33
|
import * as Predicate from "effect/Predicate";
|
|
12
34
|
import * as Redactable from "effect/Redactable";
|
|
13
35
|
import * as SchemaAST from "effect/SchemaAST";
|
|
14
|
-
import * as ServiceMap from "effect/ServiceMap";
|
|
15
36
|
import * as Stream from "effect/Stream";
|
|
16
37
|
import * as AiError from "effect/unstable/ai/AiError";
|
|
17
38
|
import { toCodecAnthropic } from "effect/unstable/ai/AnthropicStructuredOutput";
|
|
@@ -29,26 +50,28 @@ import { OpenRouterClient } from "./OpenRouterClient.js";
|
|
|
29
50
|
/**
|
|
30
51
|
* Service definition for OpenRouter language model configuration.
|
|
31
52
|
*
|
|
32
|
-
* @since 1.0.0
|
|
33
53
|
* @category services
|
|
54
|
+
* @since 4.0.0
|
|
34
55
|
*/
|
|
35
|
-
export class Config extends /*#__PURE__*/
|
|
56
|
+
export class Config extends /*#__PURE__*/Context.Service()("@effect/ai-openrouter/OpenRouterLanguageModel/Config") {}
|
|
36
57
|
// =============================================================================
|
|
37
58
|
// Language Model
|
|
38
59
|
// =============================================================================
|
|
39
60
|
/**
|
|
40
|
-
*
|
|
61
|
+
* Creates an AI model descriptor for an OpenRouter language model.
|
|
62
|
+
*
|
|
41
63
|
* @category constructors
|
|
64
|
+
* @since 4.0.0
|
|
42
65
|
*/
|
|
43
|
-
export const model = (model, config) => AiModel.make("openai", layer({
|
|
66
|
+
export const model = (model, config) => AiModel.make("openai", model, layer({
|
|
44
67
|
model,
|
|
45
68
|
config
|
|
46
69
|
}));
|
|
47
70
|
/**
|
|
48
71
|
* Creates an OpenRouter language model service.
|
|
49
72
|
*
|
|
50
|
-
* @since 1.0.0
|
|
51
73
|
* @category constructors
|
|
74
|
+
* @since 4.0.0
|
|
52
75
|
*/
|
|
53
76
|
export const make = /*#__PURE__*/Effect.fnUntraced(function* ({
|
|
54
77
|
model,
|
|
@@ -57,7 +80,7 @@ export const make = /*#__PURE__*/Effect.fnUntraced(function* ({
|
|
|
57
80
|
const client = yield* OpenRouterClient;
|
|
58
81
|
const codecTransformer = getCodecTransformer(model);
|
|
59
82
|
const makeConfig = Effect.gen(function* () {
|
|
60
|
-
const services = yield* Effect.
|
|
83
|
+
const services = yield* Effect.context();
|
|
61
84
|
return {
|
|
62
85
|
model,
|
|
63
86
|
...providerConfig,
|
|
@@ -99,6 +122,7 @@ export const make = /*#__PURE__*/Effect.fnUntraced(function* ({
|
|
|
99
122
|
return request;
|
|
100
123
|
});
|
|
101
124
|
return yield* LanguageModel.make({
|
|
125
|
+
codecTransformer: toCodecOpenAI,
|
|
102
126
|
generateText: Effect.fnUntraced(function* (options) {
|
|
103
127
|
const config = yield* makeConfig;
|
|
104
128
|
const request = yield* makeRequest({
|
|
@@ -129,20 +153,20 @@ export const make = /*#__PURE__*/Effect.fnUntraced(function* ({
|
|
|
129
153
|
annotateStreamResponse(options.span, response);
|
|
130
154
|
return response;
|
|
131
155
|
})))
|
|
132
|
-
})
|
|
156
|
+
});
|
|
133
157
|
});
|
|
134
158
|
/**
|
|
135
159
|
* Creates a layer for the OpenRouter language model.
|
|
136
160
|
*
|
|
137
|
-
* @since 1.0.0
|
|
138
161
|
* @category layers
|
|
162
|
+
* @since 4.0.0
|
|
139
163
|
*/
|
|
140
164
|
export const layer = options => Layer.effect(LanguageModel.LanguageModel, make(options));
|
|
141
165
|
/**
|
|
142
166
|
* Provides config overrides for OpenRouter language model operations.
|
|
143
167
|
*
|
|
144
|
-
* @since 1.0.0
|
|
145
168
|
* @category configuration
|
|
169
|
+
* @since 4.0.0
|
|
146
170
|
*/
|
|
147
171
|
export const withConfigOverride = /*#__PURE__*/dual(2, (self, overrides) => Effect.flatMap(Effect.serviceOption(Config), config => Effect.provideService(self, Config, {
|
|
148
172
|
...(config._tag === "Some" ? config.value : {}),
|
|
@@ -344,7 +368,7 @@ const buildHttpRequestDetails = request => ({
|
|
|
344
368
|
method: request.method,
|
|
345
369
|
url: request.url,
|
|
346
370
|
urlParams: Array.from(request.urlParams),
|
|
347
|
-
hash: request.hash,
|
|
371
|
+
hash: Option.getOrUndefined(request.hash),
|
|
348
372
|
headers: Redactable.redact(request.headers)
|
|
349
373
|
});
|
|
350
374
|
const buildHttpResponseDetails = response => ({
|
|
@@ -636,267 +660,260 @@ const makeStreamResponse = /*#__PURE__*/Effect.fnUntraced(function* ({
|
|
|
636
660
|
usage.outputTokens = computed.outputTokens;
|
|
637
661
|
}
|
|
638
662
|
const choice = event.choices[0];
|
|
639
|
-
if (Predicate.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId());
|
|
663
|
+
if (Predicate.isNotUndefined(choice)) {
|
|
664
|
+
if (Predicate.isNotNullish(choice.finish_reason)) {
|
|
665
|
+
finishReason = resolveFinishReason(choice.finish_reason);
|
|
666
|
+
}
|
|
667
|
+
const delta = choice.delta;
|
|
668
|
+
if (Predicate.isNullish(delta)) {
|
|
669
|
+
return parts;
|
|
670
|
+
}
|
|
671
|
+
const emitReasoning = Effect.fnUntraced(function* (delta, metadata) {
|
|
672
|
+
if (!reasoningStarted) {
|
|
673
|
+
activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId());
|
|
674
|
+
parts.push({
|
|
675
|
+
type: "reasoning-start",
|
|
676
|
+
id: activeReasoningId,
|
|
677
|
+
metadata
|
|
678
|
+
});
|
|
679
|
+
reasoningStarted = true;
|
|
680
|
+
}
|
|
658
681
|
parts.push({
|
|
659
|
-
type: "reasoning-
|
|
682
|
+
type: "reasoning-delta",
|
|
660
683
|
id: activeReasoningId,
|
|
684
|
+
delta,
|
|
661
685
|
metadata
|
|
662
686
|
});
|
|
663
|
-
reasoningStarted = true;
|
|
664
|
-
}
|
|
665
|
-
parts.push({
|
|
666
|
-
type: "reasoning-delta",
|
|
667
|
-
id: activeReasoningId,
|
|
668
|
-
delta,
|
|
669
|
-
metadata
|
|
670
687
|
});
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
688
|
+
const reasoningDetails = delta.reasoning_details;
|
|
689
|
+
if (Predicate.isNotUndefined(reasoningDetails) && reasoningDetails.length > 0) {
|
|
690
|
+
// Accumulate reasoning_details to preserve for multi-turn conversations
|
|
691
|
+
// Merge consecutive reasoning.text items into a single entry
|
|
692
|
+
for (const detail of reasoningDetails) {
|
|
693
|
+
if (detail.type === "reasoning.text") {
|
|
694
|
+
const lastDetail = accumulatedReasoningDetails[accumulatedReasoningDetails.length - 1];
|
|
695
|
+
if (Predicate.isNotUndefined(lastDetail) && lastDetail.type === "reasoning.text") {
|
|
696
|
+
// Merge with the previous text detail
|
|
697
|
+
lastDetail.text = (lastDetail.text ?? "") + (detail.text ?? "");
|
|
698
|
+
lastDetail.signature = lastDetail.signature ?? detail.signature ?? null;
|
|
699
|
+
lastDetail.format = lastDetail.format ?? detail.format ?? null;
|
|
700
|
+
} else {
|
|
701
|
+
// Start a new text detail
|
|
702
|
+
accumulatedReasoningDetails.push({
|
|
703
|
+
...detail
|
|
704
|
+
});
|
|
705
|
+
}
|
|
684
706
|
} else {
|
|
685
|
-
//
|
|
686
|
-
accumulatedReasoningDetails.push(
|
|
687
|
-
...detail
|
|
688
|
-
});
|
|
707
|
+
// Non-text details (encrypted, summary) are pushed as-is
|
|
708
|
+
accumulatedReasoningDetails.push(detail);
|
|
689
709
|
}
|
|
690
|
-
} else {
|
|
691
|
-
// Non-text details (encrypted, summary) are pushed as-is
|
|
692
|
-
accumulatedReasoningDetails.push(detail);
|
|
693
710
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
711
|
+
// Emit reasoning_details in providerMetadata for each delta chunk
|
|
712
|
+
// so users can accumulate them on their end before sending back
|
|
713
|
+
const metadata = {
|
|
714
|
+
openrouter: {
|
|
715
|
+
reasoningDetails
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
for (const detail of reasoningDetails) {
|
|
719
|
+
switch (detail.type) {
|
|
720
|
+
case "reasoning.text":
|
|
721
|
+
{
|
|
722
|
+
if (Predicate.isNotNullish(detail.text)) {
|
|
723
|
+
yield* emitReasoning(detail.text, metadata);
|
|
724
|
+
}
|
|
725
|
+
break;
|
|
708
726
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
727
|
+
case "reasoning.summary":
|
|
728
|
+
{
|
|
729
|
+
if (Predicate.isNotNullish(detail.summary)) {
|
|
730
|
+
yield* emitReasoning(detail.summary, metadata);
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
715
733
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
734
|
+
case "reasoning.encrypted":
|
|
735
|
+
{
|
|
736
|
+
if (Predicate.isNotNullish(detail.data)) {
|
|
737
|
+
yield* emitReasoning("[REDACTED]", metadata);
|
|
738
|
+
}
|
|
739
|
+
break;
|
|
722
740
|
}
|
|
723
|
-
|
|
724
|
-
}
|
|
741
|
+
}
|
|
725
742
|
}
|
|
743
|
+
} else if (Predicate.isNotNullish(delta.reasoning)) {
|
|
744
|
+
yield* emitReasoning(delta.reasoning);
|
|
726
745
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
// If reasoning was previously active and now we're starting text content,
|
|
733
|
-
// we should end the reasoning first to maintain proper order
|
|
734
|
-
if (reasoningStarted && !textStarted) {
|
|
735
|
-
parts.push({
|
|
736
|
-
type: "reasoning-end",
|
|
737
|
-
id: activeReasoningId,
|
|
738
|
-
// Include accumulated reasoning_details so the we can update the
|
|
739
|
-
// reasoning part's provider metadata with the correct signature.
|
|
740
|
-
// The signature typically arrives in the last reasoning delta,
|
|
741
|
-
// but reasoning-start only carries the first delta's metadata.
|
|
742
|
-
metadata: accumulatedReasoningDetails.length > 0 ? {
|
|
743
|
-
openRouter: {
|
|
744
|
-
reasoningDetails: accumulatedReasoningDetails
|
|
745
|
-
}
|
|
746
|
-
} : undefined
|
|
747
|
-
});
|
|
748
|
-
reasoningStarted = false;
|
|
749
|
-
}
|
|
750
|
-
if (!textStarted) {
|
|
751
|
-
activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId());
|
|
752
|
-
parts.push({
|
|
753
|
-
type: "text-start",
|
|
754
|
-
id: activeTextId
|
|
755
|
-
});
|
|
756
|
-
textStarted = true;
|
|
757
|
-
}
|
|
758
|
-
parts.push({
|
|
759
|
-
type: "text-delta",
|
|
760
|
-
id: activeTextId,
|
|
761
|
-
delta: content
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
const annotations = delta.annotations;
|
|
765
|
-
if (Predicate.isNotNullish(annotations)) {
|
|
766
|
-
for (const annotation of annotations) {
|
|
767
|
-
if (annotation.type === "url_citation") {
|
|
746
|
+
const content = delta.content;
|
|
747
|
+
if (Predicate.isNotNullish(content)) {
|
|
748
|
+
// If reasoning was previously active and now we're starting text content,
|
|
749
|
+
// we should end the reasoning first to maintain proper order
|
|
750
|
+
if (reasoningStarted && !textStarted) {
|
|
768
751
|
parts.push({
|
|
769
|
-
type: "
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
metadata
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
} : undefined),
|
|
779
|
-
...(Predicate.isNotUndefined(annotation.url_citation.start_index) ? {
|
|
780
|
-
startIndex: annotation.url_citation.start_index
|
|
781
|
-
} : undefined),
|
|
782
|
-
...(Predicate.isNotUndefined(annotation.url_citation.end_index) ? {
|
|
783
|
-
startIndex: annotation.url_citation.end_index
|
|
784
|
-
} : undefined)
|
|
752
|
+
type: "reasoning-end",
|
|
753
|
+
id: activeReasoningId,
|
|
754
|
+
// Include accumulated reasoning_details so the we can update the
|
|
755
|
+
// reasoning part's provider metadata with the correct signature.
|
|
756
|
+
// The signature typically arrives in the last reasoning delta,
|
|
757
|
+
// but reasoning-start only carries the first delta's metadata.
|
|
758
|
+
metadata: accumulatedReasoningDetails.length > 0 ? {
|
|
759
|
+
openRouter: {
|
|
760
|
+
reasoningDetails: accumulatedReasoningDetails
|
|
785
761
|
}
|
|
786
|
-
}
|
|
762
|
+
} : undefined
|
|
787
763
|
});
|
|
788
|
-
|
|
789
|
-
accumulatedFileAnnotations.push(annotation);
|
|
764
|
+
reasoningStarted = false;
|
|
790
765
|
}
|
|
766
|
+
if (!textStarted) {
|
|
767
|
+
activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId());
|
|
768
|
+
parts.push({
|
|
769
|
+
type: "text-start",
|
|
770
|
+
id: activeTextId
|
|
771
|
+
});
|
|
772
|
+
textStarted = true;
|
|
773
|
+
}
|
|
774
|
+
parts.push({
|
|
775
|
+
type: "text-delta",
|
|
776
|
+
id: activeTextId,
|
|
777
|
+
delta: content
|
|
778
|
+
});
|
|
791
779
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
description: "Received tool call delta without a tool call identifier"
|
|
816
|
-
})
|
|
780
|
+
const annotations = delta.annotations;
|
|
781
|
+
if (Predicate.isNotNullish(annotations)) {
|
|
782
|
+
for (const annotation of annotations) {
|
|
783
|
+
if (annotation.type === "url_citation") {
|
|
784
|
+
parts.push({
|
|
785
|
+
type: "source",
|
|
786
|
+
sourceType: "url",
|
|
787
|
+
id: annotation.url_citation.url,
|
|
788
|
+
url: annotation.url_citation.url,
|
|
789
|
+
title: annotation.url_citation.title ?? "",
|
|
790
|
+
metadata: {
|
|
791
|
+
openrouter: {
|
|
792
|
+
...(Predicate.isNotUndefined(annotation.url_citation.content) ? {
|
|
793
|
+
content: annotation.url_citation.content
|
|
794
|
+
} : undefined),
|
|
795
|
+
...(Predicate.isNotUndefined(annotation.url_citation.start_index) ? {
|
|
796
|
+
startIndex: annotation.url_citation.start_index
|
|
797
|
+
} : undefined),
|
|
798
|
+
...(Predicate.isNotUndefined(annotation.url_citation.end_index) ? {
|
|
799
|
+
startIndex: annotation.url_citation.end_index
|
|
800
|
+
} : undefined)
|
|
801
|
+
}
|
|
802
|
+
}
|
|
817
803
|
});
|
|
804
|
+
} else if (annotation.type === "file") {
|
|
805
|
+
accumulatedFileAnnotations.push(annotation);
|
|
818
806
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const toolCalls = delta.tool_calls;
|
|
810
|
+
if (Predicate.isNotNullish(toolCalls)) {
|
|
811
|
+
for (const toolCall of toolCalls) {
|
|
812
|
+
const index = toolCall.index ?? toolCalls.length - 1;
|
|
813
|
+
let activeToolCall = activeToolCalls[index];
|
|
814
|
+
// Tool call start - OpenRouter returns all information except the
|
|
815
|
+
// tool call parameters in the first chunk
|
|
816
|
+
if (Predicate.isUndefined(activeToolCall)) {
|
|
817
|
+
if (toolCall.type !== "function") {
|
|
818
|
+
return yield* AiError.make({
|
|
819
|
+
module: "OpenRouterLanguageModel",
|
|
820
|
+
method: "makeStreamResponse",
|
|
821
|
+
reason: new AiError.InvalidOutputError({
|
|
822
|
+
description: "Received tool call delta that was not of type: 'function'"
|
|
823
|
+
})
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
if (Predicate.isNullish(toolCall.id)) {
|
|
827
|
+
return yield* AiError.make({
|
|
828
|
+
module: "OpenRouterLanguageModel",
|
|
829
|
+
method: "makeStreamResponse",
|
|
830
|
+
reason: new AiError.InvalidOutputError({
|
|
831
|
+
description: "Received tool call delta without a tool call identifier"
|
|
832
|
+
})
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
if (Predicate.isNullish(toolCall.function?.name)) {
|
|
836
|
+
return yield* AiError.make({
|
|
837
|
+
module: "OpenRouterLanguageModel",
|
|
838
|
+
method: "makeStreamResponse",
|
|
839
|
+
reason: new AiError.InvalidOutputError({
|
|
840
|
+
description: "Received tool call delta without a tool call name"
|
|
841
|
+
})
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
activeToolCall = {
|
|
845
|
+
id: toolCall.id,
|
|
846
|
+
type: "function",
|
|
847
|
+
name: toolCall.function.name,
|
|
848
|
+
params: toolCall.function.arguments ?? ""
|
|
849
|
+
};
|
|
850
|
+
activeToolCalls[index] = activeToolCall;
|
|
851
|
+
parts.push({
|
|
852
|
+
type: "tool-params-start",
|
|
853
|
+
id: activeToolCall.id,
|
|
854
|
+
name: activeToolCall.name
|
|
826
855
|
});
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
});
|
|
840
|
-
// Emit a tool call delta part if parameters were also sent
|
|
841
|
-
if (activeToolCall.params.length > 0) {
|
|
856
|
+
// Emit a tool call delta part if parameters were also sent
|
|
857
|
+
if (activeToolCall.params.length > 0) {
|
|
858
|
+
parts.push({
|
|
859
|
+
type: "tool-params-delta",
|
|
860
|
+
id: activeToolCall.id,
|
|
861
|
+
delta: activeToolCall.params
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
// If an active tool call was found, update and emit the delta for
|
|
866
|
+
// the tool call's parameters
|
|
867
|
+
activeToolCall.params += toolCall.function?.arguments ?? "";
|
|
842
868
|
parts.push({
|
|
843
869
|
type: "tool-params-delta",
|
|
844
870
|
id: activeToolCall.id,
|
|
845
871
|
delta: activeToolCall.params
|
|
846
872
|
});
|
|
847
873
|
}
|
|
848
|
-
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
874
|
+
// Check if the tool call is complete
|
|
875
|
+
// @effect-diagnostics-next-line tryCatchInEffectGen:off
|
|
876
|
+
try {
|
|
877
|
+
const params = Tool.unsafeSecureJsonParse(activeToolCall.params);
|
|
878
|
+
parts.push({
|
|
879
|
+
type: "tool-params-end",
|
|
880
|
+
id: activeToolCall.id
|
|
881
|
+
});
|
|
882
|
+
parts.push({
|
|
883
|
+
type: "tool-call",
|
|
884
|
+
id: activeToolCall.id,
|
|
885
|
+
name: activeToolCall.name,
|
|
886
|
+
params,
|
|
887
|
+
// Only attach reasoning_details to the first tool call to avoid
|
|
888
|
+
// duplicating thinking blocks for parallel tool calls (Claude)
|
|
889
|
+
metadata: reasoningDetailsAttachedToToolCall ? undefined : {
|
|
890
|
+
openrouter: {
|
|
891
|
+
reasoningDetails: accumulatedReasoningDetails
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
reasoningDetailsAttachedToToolCall = true;
|
|
896
|
+
// Increment the total tool calls emitted by the stream and
|
|
897
|
+
// remove the active tool call
|
|
898
|
+
totalToolCalls += 1;
|
|
899
|
+
delete activeToolCalls[toolCall.index];
|
|
900
|
+
} catch {
|
|
901
|
+
// Tool call incomplete, continue parsing
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
857
904
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
parts.push({
|
|
863
|
-
type: "tool-params-end",
|
|
864
|
-
id: activeToolCall.id
|
|
865
|
-
});
|
|
905
|
+
}
|
|
906
|
+
const images = delta.images;
|
|
907
|
+
if (Predicate.isNotNullish(images)) {
|
|
908
|
+
for (const image of images) {
|
|
866
909
|
parts.push({
|
|
867
|
-
type: "
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
params,
|
|
871
|
-
// Only attach reasoning_details to the first tool call to avoid
|
|
872
|
-
// duplicating thinking blocks for parallel tool calls (Claude)
|
|
873
|
-
metadata: reasoningDetailsAttachedToToolCall ? undefined : {
|
|
874
|
-
openrouter: {
|
|
875
|
-
reasoningDetails: accumulatedReasoningDetails
|
|
876
|
-
}
|
|
877
|
-
}
|
|
910
|
+
type: "file",
|
|
911
|
+
mediaType: getMediaType(image.image_url.url, "image/jpeg"),
|
|
912
|
+
data: getBase64FromDataUrl(image.image_url.url)
|
|
878
913
|
});
|
|
879
|
-
reasoningDetailsAttachedToToolCall = true;
|
|
880
|
-
// Increment the total tool calls emitted by the stream and
|
|
881
|
-
// remove the active tool call
|
|
882
|
-
totalToolCalls += 1;
|
|
883
|
-
delete activeToolCalls[toolCall.index];
|
|
884
|
-
} catch {
|
|
885
|
-
// Tool call incomplete, continue parsing
|
|
886
|
-
continue;
|
|
887
914
|
}
|
|
888
915
|
}
|
|
889
916
|
}
|
|
890
|
-
const images = delta.images;
|
|
891
|
-
if (Predicate.isNotNullish(images)) {
|
|
892
|
-
for (const image of images) {
|
|
893
|
-
parts.push({
|
|
894
|
-
type: "file",
|
|
895
|
-
mediaType: getMediaType(image.image_url.url, "image/jpeg"),
|
|
896
|
-
data: getBase64FromDataUrl(image.image_url.url)
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
917
|
// Usage is only emitted by the last part of the stream, so we need to
|
|
901
918
|
// handle flushing any remaining text / reasoning / tool calls
|
|
902
919
|
if (Predicate.isNotUndefined(event.usage)) {
|