@effect/ai-openrouter 4.0.0-beta.2 → 4.0.0-beta.20
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 +1233 -1154
- package/dist/Generated.d.ts.map +1 -1
- package/dist/Generated.js +61 -32
- package/dist/Generated.js.map +1 -1
- package/dist/OpenRouterClient.d.ts +1 -1
- package/dist/OpenRouterClient.d.ts.map +1 -1
- package/dist/OpenRouterError.d.ts +22 -32
- package/dist/OpenRouterError.d.ts.map +1 -1
- package/dist/OpenRouterLanguageModel.d.ts +8 -0
- package/dist/OpenRouterLanguageModel.d.ts.map +1 -1
- package/dist/OpenRouterLanguageModel.js +227 -234
- package/dist/OpenRouterLanguageModel.js.map +1 -1
- package/package.json +3 -3
- package/src/Generated.ts +124 -54
- package/src/OpenRouterClient.ts +1 -1
- package/src/OpenRouterError.ts +24 -32
- package/src/OpenRouterLanguageModel.ts +223 -231
package/src/OpenRouterError.ts
CHANGED
|
@@ -42,51 +42,43 @@ export type OpenRouterRateLimitMetadata = OpenRouterErrorMetadata & {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
declare module "effect/unstable/ai/AiError" {
|
|
45
|
-
export interface
|
|
46
|
-
readonly
|
|
47
|
-
readonly openrouter?: OpenRouterRateLimitMetadata | null
|
|
48
|
-
}
|
|
45
|
+
export interface RateLimitErrorMetadata {
|
|
46
|
+
readonly openrouter?: OpenRouterRateLimitMetadata | null
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
export interface
|
|
52
|
-
readonly
|
|
53
|
-
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
54
|
-
}
|
|
49
|
+
export interface QuotaExhaustedErrorMetadata {
|
|
50
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
export interface
|
|
58
|
-
readonly
|
|
59
|
-
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
60
|
-
}
|
|
53
|
+
export interface AuthenticationErrorMetadata {
|
|
54
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
export interface
|
|
64
|
-
readonly
|
|
65
|
-
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
66
|
-
}
|
|
57
|
+
export interface ContentPolicyErrorMetadata {
|
|
58
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
67
59
|
}
|
|
68
60
|
|
|
69
|
-
export interface
|
|
70
|
-
readonly
|
|
71
|
-
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
72
|
-
}
|
|
61
|
+
export interface InvalidRequestErrorMetadata {
|
|
62
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
73
63
|
}
|
|
74
64
|
|
|
75
|
-
export interface
|
|
76
|
-
readonly
|
|
77
|
-
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
78
|
-
}
|
|
65
|
+
export interface InternalProviderErrorMetadata {
|
|
66
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
79
67
|
}
|
|
80
68
|
|
|
81
|
-
export interface
|
|
82
|
-
readonly
|
|
83
|
-
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
84
|
-
}
|
|
69
|
+
export interface InvalidOutputErrorMetadata {
|
|
70
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
85
71
|
}
|
|
86
72
|
|
|
87
|
-
export interface
|
|
88
|
-
readonly
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
export interface StructuredOutputErrorMetadata {
|
|
74
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface UnsupportedSchemaErrorMetadata {
|
|
78
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface UnknownErrorMetadata {
|
|
82
|
+
readonly openrouter?: OpenRouterErrorMetadata | null
|
|
91
83
|
}
|
|
92
84
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as Arr from "effect/Array"
|
|
6
6
|
import * as DateTime from "effect/DateTime"
|
|
7
7
|
import * as Effect from "effect/Effect"
|
|
8
|
-
import * as
|
|
8
|
+
import * as Encoding from "effect/Encoding"
|
|
9
9
|
import { dual } from "effect/Function"
|
|
10
10
|
import * as Layer from "effect/Layer"
|
|
11
11
|
import * as Predicate from "effect/Predicate"
|
|
@@ -233,7 +233,7 @@ export const model = (
|
|
|
233
233
|
model: string,
|
|
234
234
|
config?: Omit<typeof Config.Service, "model">
|
|
235
235
|
): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenRouterClient> =>
|
|
236
|
-
AiModel.make("openai", layer({ model, config }))
|
|
236
|
+
AiModel.make("openai", model, layer({ model, config }))
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
239
|
* Creates an OpenRouter language model service.
|
|
@@ -451,7 +451,7 @@ const prepareMessages = Effect.fnUntraced(
|
|
|
451
451
|
url: part.data instanceof URL
|
|
452
452
|
? part.data.toString()
|
|
453
453
|
: part.data instanceof Uint8Array
|
|
454
|
-
? `data:${mediaType};base64,${
|
|
454
|
+
? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}`
|
|
455
455
|
: part.data
|
|
456
456
|
},
|
|
457
457
|
...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined)
|
|
@@ -470,7 +470,7 @@ const prepareMessages = Effect.fnUntraced(
|
|
|
470
470
|
file_data: part.data instanceof URL
|
|
471
471
|
? part.data.toString()
|
|
472
472
|
: part.data instanceof Uint8Array
|
|
473
|
-
? `data:${part.mediaType};base64,${
|
|
473
|
+
? `data:${part.mediaType};base64,${Encoding.encodeBase64(part.data)}`
|
|
474
474
|
: part.data
|
|
475
475
|
},
|
|
476
476
|
...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined)
|
|
@@ -830,12 +830,12 @@ const makeStreamResponse = Effect.fnUntraced(
|
|
|
830
830
|
let activeTextId: string | undefined = undefined
|
|
831
831
|
|
|
832
832
|
let totalToolCalls = 0
|
|
833
|
-
const activeToolCalls:
|
|
833
|
+
const activeToolCalls: Record<number, {
|
|
834
834
|
readonly id: string
|
|
835
835
|
readonly type: "function"
|
|
836
836
|
readonly name: string
|
|
837
837
|
params: string
|
|
838
|
-
}> =
|
|
838
|
+
}> = {}
|
|
839
839
|
|
|
840
840
|
// Track reasoning details to preserve for multi-turn conversations
|
|
841
841
|
const accumulatedReasoningDetails: DeepMutable<ReasoningDetails> = []
|
|
@@ -885,283 +885,275 @@ const makeStreamResponse = Effect.fnUntraced(
|
|
|
885
885
|
}
|
|
886
886
|
|
|
887
887
|
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
|
-
}
|
|
888
|
+
if (Predicate.isNotUndefined(choice)) {
|
|
889
|
+
if (Predicate.isNotNullish(choice.finish_reason)) {
|
|
890
|
+
finishReason = resolveFinishReason(choice.finish_reason)
|
|
891
|
+
}
|
|
901
892
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
893
|
+
const delta = choice.delta
|
|
894
|
+
if (Predicate.isNullish(delta)) {
|
|
895
|
+
return parts
|
|
896
|
+
}
|
|
906
897
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
898
|
+
const emitReasoning = Effect.fnUntraced(
|
|
899
|
+
function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) {
|
|
900
|
+
if (!reasoningStarted) {
|
|
901
|
+
activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
902
|
+
parts.push({
|
|
903
|
+
type: "reasoning-start",
|
|
904
|
+
id: activeReasoningId,
|
|
905
|
+
metadata
|
|
906
|
+
})
|
|
907
|
+
reasoningStarted = true
|
|
908
|
+
}
|
|
911
909
|
parts.push({
|
|
912
|
-
type: "reasoning-
|
|
913
|
-
id: activeReasoningId
|
|
910
|
+
type: "reasoning-delta",
|
|
911
|
+
id: activeReasoningId!,
|
|
912
|
+
delta,
|
|
914
913
|
metadata
|
|
915
914
|
})
|
|
916
|
-
reasoningStarted = true
|
|
917
915
|
}
|
|
918
|
-
|
|
919
|
-
type: "reasoning-delta",
|
|
920
|
-
id: activeReasoningId!,
|
|
921
|
-
delta,
|
|
922
|
-
metadata
|
|
923
|
-
})
|
|
924
|
-
}
|
|
925
|
-
)
|
|
916
|
+
)
|
|
926
917
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
918
|
+
const reasoningDetails = delta.reasoning_details
|
|
919
|
+
if (Predicate.isNotUndefined(reasoningDetails) && reasoningDetails.length > 0) {
|
|
920
|
+
// Accumulate reasoning_details to preserve for multi-turn conversations
|
|
921
|
+
// Merge consecutive reasoning.text items into a single entry
|
|
922
|
+
for (const detail of reasoningDetails) {
|
|
923
|
+
if (detail.type === "reasoning.text") {
|
|
924
|
+
const lastDetail = accumulatedReasoningDetails[accumulatedReasoningDetails.length - 1]
|
|
925
|
+
if (Predicate.isNotUndefined(lastDetail) && lastDetail.type === "reasoning.text") {
|
|
926
|
+
// Merge with the previous text detail
|
|
927
|
+
lastDetail.text = (lastDetail.text ?? "") + (detail.text ?? "")
|
|
928
|
+
lastDetail.signature = lastDetail.signature ?? detail.signature ?? null
|
|
929
|
+
lastDetail.format = lastDetail.format ?? detail.format ?? null
|
|
930
|
+
} else {
|
|
931
|
+
// Start a new text detail
|
|
932
|
+
accumulatedReasoningDetails.push({ ...detail })
|
|
933
|
+
}
|
|
939
934
|
} else {
|
|
940
|
-
//
|
|
941
|
-
accumulatedReasoningDetails.push(
|
|
935
|
+
// Non-text details (encrypted, summary) are pushed as-is
|
|
936
|
+
accumulatedReasoningDetails.push(detail)
|
|
942
937
|
}
|
|
943
|
-
} else {
|
|
944
|
-
// Non-text details (encrypted, summary) are pushed as-is
|
|
945
|
-
accumulatedReasoningDetails.push(detail)
|
|
946
938
|
}
|
|
947
|
-
}
|
|
948
939
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
940
|
+
// Emit reasoning_details in providerMetadata for each delta chunk
|
|
941
|
+
// so users can accumulate them on their end before sending back
|
|
942
|
+
const metadata: Response.ReasoningDeltaPart["metadata"] = {
|
|
943
|
+
openrouter: {
|
|
944
|
+
reasoningDetails
|
|
945
|
+
}
|
|
954
946
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
947
|
+
for (const detail of reasoningDetails) {
|
|
948
|
+
switch (detail.type) {
|
|
949
|
+
case "reasoning.text": {
|
|
950
|
+
if (Predicate.isNotNullish(detail.text)) {
|
|
951
|
+
yield* emitReasoning(detail.text, metadata)
|
|
952
|
+
}
|
|
953
|
+
break
|
|
961
954
|
}
|
|
962
|
-
break
|
|
963
|
-
}
|
|
964
955
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
956
|
+
case "reasoning.summary": {
|
|
957
|
+
if (Predicate.isNotNullish(detail.summary)) {
|
|
958
|
+
yield* emitReasoning(detail.summary, metadata)
|
|
959
|
+
}
|
|
960
|
+
break
|
|
968
961
|
}
|
|
969
|
-
break
|
|
970
|
-
}
|
|
971
962
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
963
|
+
case "reasoning.encrypted": {
|
|
964
|
+
if (Predicate.isNotNullish(detail.data)) {
|
|
965
|
+
yield* emitReasoning("[REDACTED]", metadata)
|
|
966
|
+
}
|
|
967
|
+
break
|
|
975
968
|
}
|
|
976
|
-
break
|
|
977
969
|
}
|
|
978
970
|
}
|
|
971
|
+
} else if (Predicate.isNotNullish(delta.reasoning)) {
|
|
972
|
+
yield* emitReasoning(delta.reasoning)
|
|
979
973
|
}
|
|
980
|
-
} else if (Predicate.isNotNullish(delta.reasoning)) {
|
|
981
|
-
yield* emitReasoning(delta.reasoning)
|
|
982
|
-
}
|
|
983
974
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
975
|
+
const content = delta.content
|
|
976
|
+
if (Predicate.isNotNullish(content)) {
|
|
977
|
+
// If reasoning was previously active and now we're starting text content,
|
|
978
|
+
// we should end the reasoning first to maintain proper order
|
|
979
|
+
if (reasoningStarted && !textStarted) {
|
|
980
|
+
parts.push({
|
|
981
|
+
type: "reasoning-end",
|
|
982
|
+
id: activeReasoningId!,
|
|
983
|
+
// Include accumulated reasoning_details so the we can update the
|
|
984
|
+
// reasoning part's provider metadata with the correct signature.
|
|
985
|
+
// The signature typically arrives in the last reasoning delta,
|
|
986
|
+
// but reasoning-start only carries the first delta's metadata.
|
|
987
|
+
metadata: accumulatedReasoningDetails.length > 0
|
|
988
|
+
? { openRouter: { reasoningDetails: accumulatedReasoningDetails } }
|
|
989
|
+
: undefined
|
|
990
|
+
})
|
|
991
|
+
reasoningStarted = false
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (!textStarted) {
|
|
995
|
+
activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
996
|
+
parts.push({
|
|
997
|
+
type: "text-start",
|
|
998
|
+
id: activeTextId
|
|
999
|
+
})
|
|
1000
|
+
textStarted = true
|
|
1001
|
+
}
|
|
1002
1002
|
|
|
1003
|
-
if (!textStarted) {
|
|
1004
|
-
activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
|
|
1005
1003
|
parts.push({
|
|
1006
|
-
type: "text-
|
|
1007
|
-
id: activeTextId
|
|
1004
|
+
type: "text-delta",
|
|
1005
|
+
id: activeTextId!,
|
|
1006
|
+
delta: content
|
|
1008
1007
|
})
|
|
1009
|
-
textStarted = true
|
|
1010
1008
|
}
|
|
1011
1009
|
|
|
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)
|
|
1010
|
+
const annotations = delta.annotations
|
|
1011
|
+
if (Predicate.isNotNullish(annotations)) {
|
|
1012
|
+
for (const annotation of annotations) {
|
|
1013
|
+
if (annotation.type === "url_citation") {
|
|
1014
|
+
parts.push({
|
|
1015
|
+
type: "source",
|
|
1016
|
+
sourceType: "url",
|
|
1017
|
+
id: annotation.url_citation.url,
|
|
1018
|
+
url: annotation.url_citation.url,
|
|
1019
|
+
title: annotation.url_citation.title ?? "",
|
|
1020
|
+
metadata: {
|
|
1021
|
+
openrouter: {
|
|
1022
|
+
...(Predicate.isNotUndefined(annotation.url_citation.content)
|
|
1023
|
+
? { content: annotation.url_citation.content }
|
|
1024
|
+
: undefined),
|
|
1025
|
+
...(Predicate.isNotUndefined(annotation.url_citation.start_index)
|
|
1026
|
+
? { startIndex: annotation.url_citation.start_index }
|
|
1027
|
+
: undefined),
|
|
1028
|
+
...(Predicate.isNotUndefined(annotation.url_citation.end_index)
|
|
1029
|
+
? { startIndex: annotation.url_citation.end_index }
|
|
1030
|
+
: undefined)
|
|
1031
|
+
}
|
|
1040
1032
|
}
|
|
1041
|
-
}
|
|
1042
|
-
})
|
|
1043
|
-
|
|
1044
|
-
|
|
1033
|
+
})
|
|
1034
|
+
} else if (annotation.type === "file") {
|
|
1035
|
+
accumulatedFileAnnotations.push(annotation)
|
|
1036
|
+
}
|
|
1045
1037
|
}
|
|
1046
1038
|
}
|
|
1047
|
-
}
|
|
1048
1039
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1040
|
+
const toolCalls = delta.tool_calls
|
|
1041
|
+
if (Predicate.isNotNullish(toolCalls)) {
|
|
1042
|
+
for (const toolCall of toolCalls) {
|
|
1043
|
+
const index = toolCall.index ?? toolCalls.length - 1
|
|
1044
|
+
let activeToolCall = activeToolCalls[index]
|
|
1045
|
+
|
|
1046
|
+
// Tool call start - OpenRouter returns all information except the
|
|
1047
|
+
// tool call parameters in the first chunk
|
|
1048
|
+
if (Predicate.isUndefined(activeToolCall)) {
|
|
1049
|
+
if (toolCall.type !== "function") {
|
|
1050
|
+
return yield* AiError.make({
|
|
1051
|
+
module: "OpenRouterLanguageModel",
|
|
1052
|
+
method: "makeStreamResponse",
|
|
1053
|
+
reason: new AiError.InvalidOutputError({
|
|
1054
|
+
description: "Received tool call delta that was not of type: 'function'"
|
|
1055
|
+
})
|
|
1064
1056
|
})
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1057
|
+
}
|
|
1067
1058
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1059
|
+
if (Predicate.isNullish(toolCall.id)) {
|
|
1060
|
+
return yield* AiError.make({
|
|
1061
|
+
module: "OpenRouterLanguageModel",
|
|
1062
|
+
method: "makeStreamResponse",
|
|
1063
|
+
reason: new AiError.InvalidOutputError({
|
|
1064
|
+
description: "Received tool call delta without a tool call identifier"
|
|
1065
|
+
})
|
|
1074
1066
|
})
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1067
|
+
}
|
|
1077
1068
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1069
|
+
if (Predicate.isNullish(toolCall.function?.name)) {
|
|
1070
|
+
return yield* AiError.make({
|
|
1071
|
+
module: "OpenRouterLanguageModel",
|
|
1072
|
+
method: "makeStreamResponse",
|
|
1073
|
+
reason: new AiError.InvalidOutputError({
|
|
1074
|
+
description: "Received tool call delta without a tool call name"
|
|
1075
|
+
})
|
|
1084
1076
|
})
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1077
|
+
}
|
|
1087
1078
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1079
|
+
activeToolCall = {
|
|
1080
|
+
id: toolCall.id,
|
|
1081
|
+
type: "function",
|
|
1082
|
+
name: toolCall.function.name,
|
|
1083
|
+
params: toolCall.function.arguments ?? ""
|
|
1084
|
+
}
|
|
1094
1085
|
|
|
1095
|
-
|
|
1086
|
+
activeToolCalls[index] = activeToolCall
|
|
1096
1087
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1088
|
+
parts.push({
|
|
1089
|
+
type: "tool-params-start",
|
|
1090
|
+
id: activeToolCall.id,
|
|
1091
|
+
name: activeToolCall.name
|
|
1092
|
+
})
|
|
1102
1093
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1094
|
+
// Emit a tool call delta part if parameters were also sent
|
|
1095
|
+
if (activeToolCall.params.length > 0) {
|
|
1096
|
+
parts.push({
|
|
1097
|
+
type: "tool-params-delta",
|
|
1098
|
+
id: activeToolCall.id,
|
|
1099
|
+
delta: activeToolCall.params
|
|
1100
|
+
})
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
// If an active tool call was found, update and emit the delta for
|
|
1104
|
+
// the tool call's parameters
|
|
1105
|
+
activeToolCall.params += toolCall.function?.arguments ?? ""
|
|
1105
1106
|
parts.push({
|
|
1106
1107
|
type: "tool-params-delta",
|
|
1107
1108
|
id: activeToolCall.id,
|
|
1108
1109
|
delta: activeToolCall.params
|
|
1109
1110
|
})
|
|
1110
1111
|
}
|
|
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
1112
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1113
|
+
// Check if the tool call is complete
|
|
1114
|
+
// @effect-diagnostics-next-line tryCatchInEffectGen:off
|
|
1115
|
+
try {
|
|
1116
|
+
const params = Tool.unsafeSecureJsonParse(activeToolCall.params)
|
|
1126
1117
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1118
|
+
parts.push({
|
|
1119
|
+
type: "tool-params-end",
|
|
1120
|
+
id: activeToolCall.id
|
|
1121
|
+
})
|
|
1131
1122
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1123
|
+
parts.push({
|
|
1124
|
+
type: "tool-call",
|
|
1125
|
+
id: activeToolCall.id,
|
|
1126
|
+
name: activeToolCall.name,
|
|
1127
|
+
params,
|
|
1128
|
+
// Only attach reasoning_details to the first tool call to avoid
|
|
1129
|
+
// duplicating thinking blocks for parallel tool calls (Claude)
|
|
1130
|
+
metadata: reasoningDetailsAttachedToToolCall ? undefined : {
|
|
1131
|
+
openrouter: { reasoningDetails: accumulatedReasoningDetails }
|
|
1132
|
+
}
|
|
1133
|
+
})
|
|
1143
1134
|
|
|
1144
|
-
|
|
1135
|
+
reasoningDetailsAttachedToToolCall = true
|
|
1145
1136
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1137
|
+
// Increment the total tool calls emitted by the stream and
|
|
1138
|
+
// remove the active tool call
|
|
1139
|
+
totalToolCalls += 1
|
|
1140
|
+
delete activeToolCalls[toolCall.index]
|
|
1141
|
+
} catch {
|
|
1142
|
+
// Tool call incomplete, continue parsing
|
|
1143
|
+
continue
|
|
1144
|
+
}
|
|
1153
1145
|
}
|
|
1154
1146
|
}
|
|
1155
|
-
}
|
|
1156
1147
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1148
|
+
const images = delta.images
|
|
1149
|
+
if (Predicate.isNotNullish(images)) {
|
|
1150
|
+
for (const image of images) {
|
|
1151
|
+
parts.push({
|
|
1152
|
+
type: "file",
|
|
1153
|
+
mediaType: getMediaType(image.image_url.url, "image/jpeg"),
|
|
1154
|
+
data: getBase64FromDataUrl(image.image_url.url)
|
|
1155
|
+
})
|
|
1156
|
+
}
|
|
1165
1157
|
}
|
|
1166
1158
|
}
|
|
1167
1159
|
|
|
@@ -1180,7 +1172,7 @@ const makeStreamResponse = Effect.fnUntraced(
|
|
|
1180
1172
|
|
|
1181
1173
|
// Forward any unsent tool calls if finish reason is 'tool-calls'
|
|
1182
1174
|
if (finishReason === "tool-calls") {
|
|
1183
|
-
for (const toolCall of activeToolCalls) {
|
|
1175
|
+
for (const toolCall of Object.values(activeToolCalls)) {
|
|
1184
1176
|
// Coerce invalid tool call parameters to an empty object
|
|
1185
1177
|
let params: unknown
|
|
1186
1178
|
// @effect-diagnostics-next-line tryCatchInEffectGen:off
|