@effect/ai-openrouter 4.0.0-beta.7 → 4.0.0-beta.9

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.
@@ -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