@effect/ai-openrouter 4.0.0-beta.2 → 4.0.0-beta.21

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.
@@ -42,51 +42,43 @@ export type OpenRouterRateLimitMetadata = OpenRouterErrorMetadata & {
42
42
  }
43
43
 
44
44
  declare module "effect/unstable/ai/AiError" {
45
- export interface RateLimitError {
46
- readonly metadata: {
47
- readonly openrouter?: OpenRouterRateLimitMetadata | null
48
- }
45
+ export interface RateLimitErrorMetadata {
46
+ readonly openrouter?: OpenRouterRateLimitMetadata | null
49
47
  }
50
48
 
51
- export interface QuotaExhaustedError {
52
- readonly metadata: {
53
- readonly openrouter?: OpenRouterErrorMetadata | null
54
- }
49
+ export interface QuotaExhaustedErrorMetadata {
50
+ readonly openrouter?: OpenRouterErrorMetadata | null
55
51
  }
56
52
 
57
- export interface AuthenticationError {
58
- readonly metadata: {
59
- readonly openrouter?: OpenRouterErrorMetadata | null
60
- }
53
+ export interface AuthenticationErrorMetadata {
54
+ readonly openrouter?: OpenRouterErrorMetadata | null
61
55
  }
62
56
 
63
- export interface ContentPolicyError {
64
- readonly metadata: {
65
- readonly openrouter?: OpenRouterErrorMetadata | null
66
- }
57
+ export interface ContentPolicyErrorMetadata {
58
+ readonly openrouter?: OpenRouterErrorMetadata | null
67
59
  }
68
60
 
69
- export interface InvalidRequestError {
70
- readonly metadata: {
71
- readonly openrouter?: OpenRouterErrorMetadata | null
72
- }
61
+ export interface InvalidRequestErrorMetadata {
62
+ readonly openrouter?: OpenRouterErrorMetadata | null
73
63
  }
74
64
 
75
- export interface InternalProviderError {
76
- readonly metadata: {
77
- readonly openrouter?: OpenRouterErrorMetadata | null
78
- }
65
+ export interface InternalProviderErrorMetadata {
66
+ readonly openrouter?: OpenRouterErrorMetadata | null
79
67
  }
80
68
 
81
- export interface InvalidOutputError {
82
- readonly metadata: {
83
- readonly openrouter?: OpenRouterErrorMetadata | null
84
- }
69
+ export interface InvalidOutputErrorMetadata {
70
+ readonly openrouter?: OpenRouterErrorMetadata | null
85
71
  }
86
72
 
87
- export interface UnknownError {
88
- readonly metadata: {
89
- readonly openrouter?: OpenRouterErrorMetadata | null
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 Base64 from "effect/encoding/Base64"
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,${Base64.encode(part.data)}`
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,${Base64.encode(part.data)}`
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: Array<{
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.isUndefined(choice)) {
889
- return yield* AiError.make({
890
- module: "OpenRouterLanguageModel",
891
- method: "makeStreamResponse",
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
- const delta = choice.delta
903
- if (Predicate.isNullish(delta)) {
904
- return parts
905
- }
893
+ const delta = choice.delta
894
+ if (Predicate.isNullish(delta)) {
895
+ return parts
896
+ }
906
897
 
907
- const emitReasoning = Effect.fnUntraced(
908
- function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) {
909
- if (!reasoningStarted) {
910
- activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId())
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-start",
913
- id: activeReasoningId,
910
+ type: "reasoning-delta",
911
+ id: activeReasoningId!,
912
+ delta,
914
913
  metadata
915
914
  })
916
- reasoningStarted = true
917
915
  }
918
- parts.push({
919
- type: "reasoning-delta",
920
- id: activeReasoningId!,
921
- delta,
922
- metadata
923
- })
924
- }
925
- )
916
+ )
926
917
 
927
- const reasoningDetails = delta.reasoning_details
928
- if (Predicate.isNotUndefined(reasoningDetails) && reasoningDetails.length > 0) {
929
- // Accumulate reasoning_details to preserve for multi-turn conversations
930
- // Merge consecutive reasoning.text items into a single entry
931
- for (const detail of reasoningDetails) {
932
- if (detail.type === "reasoning.text") {
933
- const lastDetail = accumulatedReasoningDetails[accumulatedReasoningDetails.length - 1]
934
- if (Predicate.isNotUndefined(lastDetail) && lastDetail.type === "reasoning.text") {
935
- // Merge with the previous text detail
936
- lastDetail.text = (lastDetail.text ?? "") + (detail.text ?? "")
937
- lastDetail.signature = lastDetail.signature ?? detail.signature ?? null
938
- lastDetail.format = lastDetail.format ?? detail.format ?? null
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
- // Start a new text detail
941
- accumulatedReasoningDetails.push({ ...detail })
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
- // Emit reasoning_details in providerMetadata for each delta chunk
950
- // so users can accumulate them on their end before sending back
951
- const metadata: Response.ReasoningDeltaPart["metadata"] = {
952
- openrouter: {
953
- reasoningDetails
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
- for (const detail of reasoningDetails) {
957
- switch (detail.type) {
958
- case "reasoning.text": {
959
- if (Predicate.isNotNullish(detail.text)) {
960
- yield* emitReasoning(detail.text, metadata)
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
- case "reasoning.summary": {
966
- if (Predicate.isNotNullish(detail.summary)) {
967
- yield* emitReasoning(detail.summary, metadata)
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
- case "reasoning.encrypted": {
973
- if (Predicate.isNotNullish(detail.data)) {
974
- yield* emitReasoning("[REDACTED]", metadata)
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
- const content = delta.content
985
- if (Predicate.isNotNullish(content)) {
986
- // If reasoning was previously active and now we're starting text content,
987
- // we should end the reasoning first to maintain proper order
988
- if (reasoningStarted && !textStarted) {
989
- parts.push({
990
- type: "reasoning-end",
991
- id: activeReasoningId!,
992
- // Include accumulated reasoning_details so the we can update the
993
- // reasoning part's provider metadata with the correct signature.
994
- // The signature typically arrives in the last reasoning delta,
995
- // but reasoning-start only carries the first delta's metadata.
996
- metadata: accumulatedReasoningDetails.length > 0
997
- ? { openRouter: { reasoningDetails: accumulatedReasoningDetails } }
998
- : undefined
999
- })
1000
- reasoningStarted = false
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-start",
1007
- id: activeTextId
1004
+ type: "text-delta",
1005
+ id: activeTextId!,
1006
+ delta: content
1008
1007
  })
1009
- textStarted = true
1010
1008
  }
1011
1009
 
1012
- parts.push({
1013
- type: "text-delta",
1014
- id: activeTextId!,
1015
- delta: content
1016
- })
1017
- }
1018
-
1019
- const annotations = delta.annotations
1020
- if (Predicate.isNotNullish(annotations)) {
1021
- for (const annotation of annotations) {
1022
- if (annotation.type === "url_citation") {
1023
- parts.push({
1024
- type: "source",
1025
- sourceType: "url",
1026
- id: annotation.url_citation.url,
1027
- url: annotation.url_citation.url,
1028
- title: annotation.url_citation.title ?? "",
1029
- metadata: {
1030
- openrouter: {
1031
- ...(Predicate.isNotUndefined(annotation.url_citation.content)
1032
- ? { content: annotation.url_citation.content }
1033
- : undefined),
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
- } else if (annotation.type === "file") {
1044
- accumulatedFileAnnotations.push(annotation)
1033
+ })
1034
+ } else if (annotation.type === "file") {
1035
+ accumulatedFileAnnotations.push(annotation)
1036
+ }
1045
1037
  }
1046
1038
  }
1047
- }
1048
1039
 
1049
- const toolCalls = delta.tool_calls
1050
- if (Predicate.isNotNullish(toolCalls)) {
1051
- for (const toolCall of toolCalls) {
1052
- const index = toolCall.index ?? toolCalls.length - 1
1053
- let activeToolCall = activeToolCalls[index]
1054
-
1055
- // Tool call start - OpenRouter returns all information except the
1056
- // tool call parameters in the first chunk
1057
- if (Predicate.isUndefined(activeToolCall)) {
1058
- if (toolCall.type !== "function") {
1059
- return yield* AiError.make({
1060
- module: "OpenRouterLanguageModel",
1061
- method: "makeStreamResponse",
1062
- reason: new AiError.InvalidOutputError({
1063
- description: "Received tool call delta that was not of type: 'function'"
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
- if (Predicate.isUndefined(toolCall.id)) {
1069
- return yield* AiError.make({
1070
- module: "OpenRouterLanguageModel",
1071
- method: "makeStreamResponse",
1072
- reason: new AiError.InvalidOutputError({
1073
- description: "Received tool call delta without a tool call identifier"
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
- if (Predicate.isUndefined(toolCall.function?.name)) {
1079
- return yield* AiError.make({
1080
- module: "OpenRouterLanguageModel",
1081
- method: "makeStreamResponse",
1082
- reason: new AiError.InvalidOutputError({
1083
- description: "Received tool call delta without a tool call name"
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
- activeToolCall = {
1089
- id: toolCall.id,
1090
- type: "function",
1091
- name: toolCall.function.name,
1092
- params: toolCall.function.arguments ?? ""
1093
- }
1079
+ activeToolCall = {
1080
+ id: toolCall.id,
1081
+ type: "function",
1082
+ name: toolCall.function.name,
1083
+ params: toolCall.function.arguments ?? ""
1084
+ }
1094
1085
 
1095
- activeToolCalls[index] = activeToolCall
1086
+ activeToolCalls[index] = activeToolCall
1096
1087
 
1097
- parts.push({
1098
- type: "tool-params-start",
1099
- id: activeToolCall.id,
1100
- name: activeToolCall.name
1101
- })
1088
+ parts.push({
1089
+ type: "tool-params-start",
1090
+ id: activeToolCall.id,
1091
+ name: activeToolCall.name
1092
+ })
1102
1093
 
1103
- // Emit a tool call delta part if parameters were also sent
1104
- if (activeToolCall.params.length > 0) {
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
- // Check if the tool call is complete
1123
- // @effect-diagnostics-next-line tryCatchInEffectGen:off
1124
- try {
1125
- const params = Tool.unsafeSecureJsonParse(activeToolCall.params)
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
- parts.push({
1128
- type: "tool-params-end",
1129
- id: activeToolCall.id
1130
- })
1118
+ parts.push({
1119
+ type: "tool-params-end",
1120
+ id: activeToolCall.id
1121
+ })
1131
1122
 
1132
- parts.push({
1133
- type: "tool-call",
1134
- id: activeToolCall.id,
1135
- name: activeToolCall.name,
1136
- params,
1137
- // Only attach reasoning_details to the first tool call to avoid
1138
- // duplicating thinking blocks for parallel tool calls (Claude)
1139
- metadata: reasoningDetailsAttachedToToolCall ? undefined : {
1140
- openrouter: { reasoningDetails: accumulatedReasoningDetails }
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
- reasoningDetailsAttachedToToolCall = true
1135
+ reasoningDetailsAttachedToToolCall = true
1145
1136
 
1146
- // Increment the total tool calls emitted by the stream and
1147
- // remove the active tool call
1148
- totalToolCalls += 1
1149
- delete activeToolCalls[toolCall.index]
1150
- } catch {
1151
- // Tool call incomplete, continue parsing
1152
- continue
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
- const images = delta.images
1158
- if (Predicate.isNotNullish(images)) {
1159
- for (const image of images) {
1160
- parts.push({
1161
- type: "file",
1162
- mediaType: getMediaType(image.image_url.url, "image/jpeg"),
1163
- data: getBase64FromDataUrl(image.image_url.url)
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