@effect/ai-openrouter 4.0.0-beta.3 → 4.0.0-beta.30

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.
@@ -273,6 +273,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
273
273
  )
274
274
 
275
275
  return yield* LanguageModel.make({
276
+ codecTransformer: toCodecOpenAI,
276
277
  generateText: Effect.fnUntraced(
277
278
  function*(options) {
278
279
  const config = yield* makeConfig
@@ -300,10 +301,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
300
301
  })
301
302
  )
302
303
  )
303
- }).pipe(Effect.provideService(
304
- LanguageModel.CurrentCodecTransformer,
305
- codecTransformer
306
- ))
304
+ })
307
305
  })
308
306
 
309
307
  /**
@@ -451,7 +449,7 @@ const prepareMessages = Effect.fnUntraced(
451
449
  url: part.data instanceof URL
452
450
  ? part.data.toString()
453
451
  : part.data instanceof Uint8Array
454
- ? `data:${mediaType};base64,${Base64.encode(part.data)}`
452
+ ? `data:${mediaType};base64,${Encoding.encodeBase64(part.data)}`
455
453
  : part.data
456
454
  },
457
455
  ...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined)
@@ -470,7 +468,7 @@ const prepareMessages = Effect.fnUntraced(
470
468
  file_data: part.data instanceof URL
471
469
  ? part.data.toString()
472
470
  : part.data instanceof Uint8Array
473
- ? `data:${part.mediaType};base64,${Base64.encode(part.data)}`
471
+ ? `data:${part.mediaType};base64,${Encoding.encodeBase64(part.data)}`
474
472
  : part.data
475
473
  },
476
474
  ...(Predicate.isNotNull(partCacheControl) ? { cache_control: partCacheControl } : undefined)
@@ -885,283 +883,275 @@ const makeStreamResponse = Effect.fnUntraced(
885
883
  }
886
884
 
887
885
  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
- }
886
+ if (Predicate.isNotUndefined(choice)) {
887
+ if (Predicate.isNotNullish(choice.finish_reason)) {
888
+ finishReason = resolveFinishReason(choice.finish_reason)
889
+ }
901
890
 
902
- const delta = choice.delta
903
- if (Predicate.isNullish(delta)) {
904
- return parts
905
- }
891
+ const delta = choice.delta
892
+ if (Predicate.isNullish(delta)) {
893
+ return parts
894
+ }
906
895
 
907
- const emitReasoning = Effect.fnUntraced(
908
- function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) {
909
- if (!reasoningStarted) {
910
- activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId())
896
+ const emitReasoning = Effect.fnUntraced(
897
+ function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) {
898
+ if (!reasoningStarted) {
899
+ activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId())
900
+ parts.push({
901
+ type: "reasoning-start",
902
+ id: activeReasoningId,
903
+ metadata
904
+ })
905
+ reasoningStarted = true
906
+ }
911
907
  parts.push({
912
- type: "reasoning-start",
913
- id: activeReasoningId,
908
+ type: "reasoning-delta",
909
+ id: activeReasoningId!,
910
+ delta,
914
911
  metadata
915
912
  })
916
- reasoningStarted = true
917
913
  }
918
- parts.push({
919
- type: "reasoning-delta",
920
- id: activeReasoningId!,
921
- delta,
922
- metadata
923
- })
924
- }
925
- )
914
+ )
926
915
 
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
916
+ const reasoningDetails = delta.reasoning_details
917
+ if (Predicate.isNotUndefined(reasoningDetails) && reasoningDetails.length > 0) {
918
+ // Accumulate reasoning_details to preserve for multi-turn conversations
919
+ // Merge consecutive reasoning.text items into a single entry
920
+ for (const detail of reasoningDetails) {
921
+ if (detail.type === "reasoning.text") {
922
+ const lastDetail = accumulatedReasoningDetails[accumulatedReasoningDetails.length - 1]
923
+ if (Predicate.isNotUndefined(lastDetail) && lastDetail.type === "reasoning.text") {
924
+ // Merge with the previous text detail
925
+ lastDetail.text = (lastDetail.text ?? "") + (detail.text ?? "")
926
+ lastDetail.signature = lastDetail.signature ?? detail.signature ?? null
927
+ lastDetail.format = lastDetail.format ?? detail.format ?? null
928
+ } else {
929
+ // Start a new text detail
930
+ accumulatedReasoningDetails.push({ ...detail })
931
+ }
939
932
  } else {
940
- // Start a new text detail
941
- accumulatedReasoningDetails.push({ ...detail })
933
+ // Non-text details (encrypted, summary) are pushed as-is
934
+ accumulatedReasoningDetails.push(detail)
942
935
  }
943
- } else {
944
- // Non-text details (encrypted, summary) are pushed as-is
945
- accumulatedReasoningDetails.push(detail)
946
936
  }
947
- }
948
937
 
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
938
+ // Emit reasoning_details in providerMetadata for each delta chunk
939
+ // so users can accumulate them on their end before sending back
940
+ const metadata: Response.ReasoningDeltaPart["metadata"] = {
941
+ openrouter: {
942
+ reasoningDetails
943
+ }
954
944
  }
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)
945
+ for (const detail of reasoningDetails) {
946
+ switch (detail.type) {
947
+ case "reasoning.text": {
948
+ if (Predicate.isNotNullish(detail.text)) {
949
+ yield* emitReasoning(detail.text, metadata)
950
+ }
951
+ break
961
952
  }
962
- break
963
- }
964
953
 
965
- case "reasoning.summary": {
966
- if (Predicate.isNotNullish(detail.summary)) {
967
- yield* emitReasoning(detail.summary, metadata)
954
+ case "reasoning.summary": {
955
+ if (Predicate.isNotNullish(detail.summary)) {
956
+ yield* emitReasoning(detail.summary, metadata)
957
+ }
958
+ break
968
959
  }
969
- break
970
- }
971
960
 
972
- case "reasoning.encrypted": {
973
- if (Predicate.isNotNullish(detail.data)) {
974
- yield* emitReasoning("[REDACTED]", metadata)
961
+ case "reasoning.encrypted": {
962
+ if (Predicate.isNotNullish(detail.data)) {
963
+ yield* emitReasoning("[REDACTED]", metadata)
964
+ }
965
+ break
975
966
  }
976
- break
977
967
  }
978
968
  }
969
+ } else if (Predicate.isNotNullish(delta.reasoning)) {
970
+ yield* emitReasoning(delta.reasoning)
979
971
  }
980
- } else if (Predicate.isNotNullish(delta.reasoning)) {
981
- yield* emitReasoning(delta.reasoning)
982
- }
983
972
 
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
- }
973
+ const content = delta.content
974
+ if (Predicate.isNotNullish(content)) {
975
+ // If reasoning was previously active and now we're starting text content,
976
+ // we should end the reasoning first to maintain proper order
977
+ if (reasoningStarted && !textStarted) {
978
+ parts.push({
979
+ type: "reasoning-end",
980
+ id: activeReasoningId!,
981
+ // Include accumulated reasoning_details so the we can update the
982
+ // reasoning part's provider metadata with the correct signature.
983
+ // The signature typically arrives in the last reasoning delta,
984
+ // but reasoning-start only carries the first delta's metadata.
985
+ metadata: accumulatedReasoningDetails.length > 0
986
+ ? { openRouter: { reasoningDetails: accumulatedReasoningDetails } }
987
+ : undefined
988
+ })
989
+ reasoningStarted = false
990
+ }
991
+
992
+ if (!textStarted) {
993
+ activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
994
+ parts.push({
995
+ type: "text-start",
996
+ id: activeTextId
997
+ })
998
+ textStarted = true
999
+ }
1002
1000
 
1003
- if (!textStarted) {
1004
- activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
1005
1001
  parts.push({
1006
- type: "text-start",
1007
- id: activeTextId
1002
+ type: "text-delta",
1003
+ id: activeTextId!,
1004
+ delta: content
1008
1005
  })
1009
- textStarted = true
1010
1006
  }
1011
1007
 
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)
1008
+ const annotations = delta.annotations
1009
+ if (Predicate.isNotNullish(annotations)) {
1010
+ for (const annotation of annotations) {
1011
+ if (annotation.type === "url_citation") {
1012
+ parts.push({
1013
+ type: "source",
1014
+ sourceType: "url",
1015
+ id: annotation.url_citation.url,
1016
+ url: annotation.url_citation.url,
1017
+ title: annotation.url_citation.title ?? "",
1018
+ metadata: {
1019
+ openrouter: {
1020
+ ...(Predicate.isNotUndefined(annotation.url_citation.content)
1021
+ ? { content: annotation.url_citation.content }
1022
+ : undefined),
1023
+ ...(Predicate.isNotUndefined(annotation.url_citation.start_index)
1024
+ ? { startIndex: annotation.url_citation.start_index }
1025
+ : undefined),
1026
+ ...(Predicate.isNotUndefined(annotation.url_citation.end_index)
1027
+ ? { startIndex: annotation.url_citation.end_index }
1028
+ : undefined)
1029
+ }
1040
1030
  }
1041
- }
1042
- })
1043
- } else if (annotation.type === "file") {
1044
- accumulatedFileAnnotations.push(annotation)
1031
+ })
1032
+ } else if (annotation.type === "file") {
1033
+ accumulatedFileAnnotations.push(annotation)
1034
+ }
1045
1035
  }
1046
1036
  }
1047
- }
1048
1037
 
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'"
1038
+ const toolCalls = delta.tool_calls
1039
+ if (Predicate.isNotNullish(toolCalls)) {
1040
+ for (const toolCall of toolCalls) {
1041
+ const index = toolCall.index ?? toolCalls.length - 1
1042
+ let activeToolCall = activeToolCalls[index]
1043
+
1044
+ // Tool call start - OpenRouter returns all information except the
1045
+ // tool call parameters in the first chunk
1046
+ if (Predicate.isUndefined(activeToolCall)) {
1047
+ if (toolCall.type !== "function") {
1048
+ return yield* AiError.make({
1049
+ module: "OpenRouterLanguageModel",
1050
+ method: "makeStreamResponse",
1051
+ reason: new AiError.InvalidOutputError({
1052
+ description: "Received tool call delta that was not of type: 'function'"
1053
+ })
1064
1054
  })
1065
- })
1066
- }
1055
+ }
1067
1056
 
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"
1057
+ if (Predicate.isNullish(toolCall.id)) {
1058
+ return yield* AiError.make({
1059
+ module: "OpenRouterLanguageModel",
1060
+ method: "makeStreamResponse",
1061
+ reason: new AiError.InvalidOutputError({
1062
+ description: "Received tool call delta without a tool call identifier"
1063
+ })
1074
1064
  })
1075
- })
1076
- }
1065
+ }
1077
1066
 
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"
1067
+ if (Predicate.isNullish(toolCall.function?.name)) {
1068
+ return yield* AiError.make({
1069
+ module: "OpenRouterLanguageModel",
1070
+ method: "makeStreamResponse",
1071
+ reason: new AiError.InvalidOutputError({
1072
+ description: "Received tool call delta without a tool call name"
1073
+ })
1084
1074
  })
1085
- })
1086
- }
1075
+ }
1087
1076
 
1088
- activeToolCall = {
1089
- id: toolCall.id,
1090
- type: "function",
1091
- name: toolCall.function.name,
1092
- params: toolCall.function.arguments ?? ""
1093
- }
1077
+ activeToolCall = {
1078
+ id: toolCall.id,
1079
+ type: "function",
1080
+ name: toolCall.function.name,
1081
+ params: toolCall.function.arguments ?? ""
1082
+ }
1094
1083
 
1095
- activeToolCalls[index] = activeToolCall
1084
+ activeToolCalls[index] = activeToolCall
1096
1085
 
1097
- parts.push({
1098
- type: "tool-params-start",
1099
- id: activeToolCall.id,
1100
- name: activeToolCall.name
1101
- })
1086
+ parts.push({
1087
+ type: "tool-params-start",
1088
+ id: activeToolCall.id,
1089
+ name: activeToolCall.name
1090
+ })
1102
1091
 
1103
- // Emit a tool call delta part if parameters were also sent
1104
- if (activeToolCall.params.length > 0) {
1092
+ // Emit a tool call delta part if parameters were also sent
1093
+ if (activeToolCall.params.length > 0) {
1094
+ parts.push({
1095
+ type: "tool-params-delta",
1096
+ id: activeToolCall.id,
1097
+ delta: activeToolCall.params
1098
+ })
1099
+ }
1100
+ } else {
1101
+ // If an active tool call was found, update and emit the delta for
1102
+ // the tool call's parameters
1103
+ activeToolCall.params += toolCall.function?.arguments ?? ""
1105
1104
  parts.push({
1106
1105
  type: "tool-params-delta",
1107
1106
  id: activeToolCall.id,
1108
1107
  delta: activeToolCall.params
1109
1108
  })
1110
1109
  }
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
1110
 
1122
- // Check if the tool call is complete
1123
- // @effect-diagnostics-next-line tryCatchInEffectGen:off
1124
- try {
1125
- const params = Tool.unsafeSecureJsonParse(activeToolCall.params)
1111
+ // Check if the tool call is complete
1112
+ // @effect-diagnostics-next-line tryCatchInEffectGen:off
1113
+ try {
1114
+ const params = Tool.unsafeSecureJsonParse(activeToolCall.params)
1126
1115
 
1127
- parts.push({
1128
- type: "tool-params-end",
1129
- id: activeToolCall.id
1130
- })
1116
+ parts.push({
1117
+ type: "tool-params-end",
1118
+ id: activeToolCall.id
1119
+ })
1131
1120
 
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
- })
1121
+ parts.push({
1122
+ type: "tool-call",
1123
+ id: activeToolCall.id,
1124
+ name: activeToolCall.name,
1125
+ params,
1126
+ // Only attach reasoning_details to the first tool call to avoid
1127
+ // duplicating thinking blocks for parallel tool calls (Claude)
1128
+ metadata: reasoningDetailsAttachedToToolCall ? undefined : {
1129
+ openrouter: { reasoningDetails: accumulatedReasoningDetails }
1130
+ }
1131
+ })
1143
1132
 
1144
- reasoningDetailsAttachedToToolCall = true
1133
+ reasoningDetailsAttachedToToolCall = true
1145
1134
 
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
1135
+ // Increment the total tool calls emitted by the stream and
1136
+ // remove the active tool call
1137
+ totalToolCalls += 1
1138
+ delete activeToolCalls[toolCall.index]
1139
+ } catch {
1140
+ // Tool call incomplete, continue parsing
1141
+ continue
1142
+ }
1153
1143
  }
1154
1144
  }
1155
- }
1156
1145
 
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
- })
1146
+ const images = delta.images
1147
+ if (Predicate.isNotNullish(images)) {
1148
+ for (const image of images) {
1149
+ parts.push({
1150
+ type: "file",
1151
+ mediaType: getMediaType(image.image_url.url, "image/jpeg"),
1152
+ data: getBase64FromDataUrl(image.image_url.url)
1153
+ })
1154
+ }
1165
1155
  }
1166
1156
  }
1167
1157