@effect/ai-openrouter 4.0.0-beta.6 → 4.0.0-beta.62

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.
@@ -3,16 +3,17 @@
3
3
  */
4
4
  /** @effect-diagnostics preferSchemaOverJson:skip-file */
5
5
  import * as Arr from "effect/Array"
6
+ import * as Context from "effect/Context"
6
7
  import * as DateTime from "effect/DateTime"
7
8
  import * as Effect from "effect/Effect"
8
9
  import * as Encoding from "effect/Encoding"
9
10
  import { dual } from "effect/Function"
10
11
  import * as Layer from "effect/Layer"
12
+ import * as Option from "effect/Option"
11
13
  import * as Predicate from "effect/Predicate"
12
14
  import * as Redactable from "effect/Redactable"
13
15
  import type * as Schema from "effect/Schema"
14
16
  import * as SchemaAST from "effect/SchemaAST"
15
- import * as ServiceMap from "effect/ServiceMap"
16
17
  import * as Stream from "effect/Stream"
17
18
  import type { Span } from "effect/Tracer"
18
19
  import type { DeepMutable, Mutable, Simplify } from "effect/Types"
@@ -42,7 +43,7 @@ import { type ChatStreamingResponseChunkData, OpenRouterClient } from "./OpenRou
42
43
  * @since 1.0.0
43
44
  * @category services
44
45
  */
45
- export class Config extends ServiceMap.Service<
46
+ export class Config extends Context.Service<
46
47
  Config,
47
48
  Simplify<
48
49
  & Partial<
@@ -233,7 +234,7 @@ export const model = (
233
234
  model: string,
234
235
  config?: Omit<typeof Config.Service, "model">
235
236
  ): AiModel.Model<"openai", LanguageModel.LanguageModel, OpenRouterClient> =>
236
- AiModel.make("openai", layer({ model, config }))
237
+ AiModel.make("openai", model, layer({ model, config }))
237
238
 
238
239
  /**
239
240
  * Creates an OpenRouter language model service.
@@ -249,7 +250,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
249
250
  const codecTransformer = getCodecTransformer(model)
250
251
 
251
252
  const makeConfig = Effect.gen(function*() {
252
- const services = yield* Effect.services<never>()
253
+ const services = yield* Effect.context<never>()
253
254
  return { model, ...providerConfig, ...services.mapUnsafe.get(Config.key) }
254
255
  })
255
256
 
@@ -273,6 +274,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
273
274
  )
274
275
 
275
276
  return yield* LanguageModel.make({
277
+ codecTransformer: toCodecOpenAI,
276
278
  generateText: Effect.fnUntraced(
277
279
  function*(options) {
278
280
  const config = yield* makeConfig
@@ -300,10 +302,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig
300
302
  })
301
303
  )
302
304
  )
303
- }).pipe(Effect.provideService(
304
- LanguageModel.CurrentCodecTransformer,
305
- codecTransformer
306
- ))
305
+ })
307
306
  })
308
307
 
309
308
  /**
@@ -588,7 +587,7 @@ const buildHttpRequestDetails = (
588
587
  method: request.method,
589
588
  url: request.url,
590
589
  urlParams: Array.from(request.urlParams),
591
- hash: request.hash,
590
+ hash: Option.getOrUndefined(request.hash),
592
591
  headers: Redactable.redact(request.headers) as Record<string, string>
593
592
  })
594
593
 
@@ -885,283 +884,275 @@ const makeStreamResponse = Effect.fnUntraced(
885
884
  }
886
885
 
887
886
  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
- }
887
+ if (Predicate.isNotUndefined(choice)) {
888
+ if (Predicate.isNotNullish(choice.finish_reason)) {
889
+ finishReason = resolveFinishReason(choice.finish_reason)
890
+ }
901
891
 
902
- const delta = choice.delta
903
- if (Predicate.isNullish(delta)) {
904
- return parts
905
- }
892
+ const delta = choice.delta
893
+ if (Predicate.isNullish(delta)) {
894
+ return parts
895
+ }
906
896
 
907
- const emitReasoning = Effect.fnUntraced(
908
- function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) {
909
- if (!reasoningStarted) {
910
- activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId())
897
+ const emitReasoning = Effect.fnUntraced(
898
+ function*(delta: string, metadata?: Response.ReasoningDeltaPart["metadata"] | undefined) {
899
+ if (!reasoningStarted) {
900
+ activeReasoningId = openRouterResponseId ?? (yield* idGenerator.generateId())
901
+ parts.push({
902
+ type: "reasoning-start",
903
+ id: activeReasoningId,
904
+ metadata
905
+ })
906
+ reasoningStarted = true
907
+ }
911
908
  parts.push({
912
- type: "reasoning-start",
913
- id: activeReasoningId,
909
+ type: "reasoning-delta",
910
+ id: activeReasoningId!,
911
+ delta,
914
912
  metadata
915
913
  })
916
- reasoningStarted = true
917
914
  }
918
- parts.push({
919
- type: "reasoning-delta",
920
- id: activeReasoningId!,
921
- delta,
922
- metadata
923
- })
924
- }
925
- )
915
+ )
926
916
 
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
917
+ const reasoningDetails = delta.reasoning_details
918
+ if (Predicate.isNotUndefined(reasoningDetails) && reasoningDetails.length > 0) {
919
+ // Accumulate reasoning_details to preserve for multi-turn conversations
920
+ // Merge consecutive reasoning.text items into a single entry
921
+ for (const detail of reasoningDetails) {
922
+ if (detail.type === "reasoning.text") {
923
+ const lastDetail = accumulatedReasoningDetails[accumulatedReasoningDetails.length - 1]
924
+ if (Predicate.isNotUndefined(lastDetail) && lastDetail.type === "reasoning.text") {
925
+ // Merge with the previous text detail
926
+ lastDetail.text = (lastDetail.text ?? "") + (detail.text ?? "")
927
+ lastDetail.signature = lastDetail.signature ?? detail.signature ?? null
928
+ lastDetail.format = lastDetail.format ?? detail.format ?? null
929
+ } else {
930
+ // Start a new text detail
931
+ accumulatedReasoningDetails.push({ ...detail })
932
+ }
939
933
  } else {
940
- // Start a new text detail
941
- accumulatedReasoningDetails.push({ ...detail })
934
+ // Non-text details (encrypted, summary) are pushed as-is
935
+ accumulatedReasoningDetails.push(detail)
942
936
  }
943
- } else {
944
- // Non-text details (encrypted, summary) are pushed as-is
945
- accumulatedReasoningDetails.push(detail)
946
937
  }
947
- }
948
938
 
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
939
+ // Emit reasoning_details in providerMetadata for each delta chunk
940
+ // so users can accumulate them on their end before sending back
941
+ const metadata: Response.ReasoningDeltaPart["metadata"] = {
942
+ openrouter: {
943
+ reasoningDetails
944
+ }
954
945
  }
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)
946
+ for (const detail of reasoningDetails) {
947
+ switch (detail.type) {
948
+ case "reasoning.text": {
949
+ if (Predicate.isNotNullish(detail.text)) {
950
+ yield* emitReasoning(detail.text, metadata)
951
+ }
952
+ break
961
953
  }
962
- break
963
- }
964
954
 
965
- case "reasoning.summary": {
966
- if (Predicate.isNotNullish(detail.summary)) {
967
- yield* emitReasoning(detail.summary, metadata)
955
+ case "reasoning.summary": {
956
+ if (Predicate.isNotNullish(detail.summary)) {
957
+ yield* emitReasoning(detail.summary, metadata)
958
+ }
959
+ break
968
960
  }
969
- break
970
- }
971
961
 
972
- case "reasoning.encrypted": {
973
- if (Predicate.isNotNullish(detail.data)) {
974
- yield* emitReasoning("[REDACTED]", metadata)
962
+ case "reasoning.encrypted": {
963
+ if (Predicate.isNotNullish(detail.data)) {
964
+ yield* emitReasoning("[REDACTED]", metadata)
965
+ }
966
+ break
975
967
  }
976
- break
977
968
  }
978
969
  }
970
+ } else if (Predicate.isNotNullish(delta.reasoning)) {
971
+ yield* emitReasoning(delta.reasoning)
979
972
  }
980
- } else if (Predicate.isNotNullish(delta.reasoning)) {
981
- yield* emitReasoning(delta.reasoning)
982
- }
983
973
 
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
- }
974
+ const content = delta.content
975
+ if (Predicate.isNotNullish(content)) {
976
+ // If reasoning was previously active and now we're starting text content,
977
+ // we should end the reasoning first to maintain proper order
978
+ if (reasoningStarted && !textStarted) {
979
+ parts.push({
980
+ type: "reasoning-end",
981
+ id: activeReasoningId!,
982
+ // Include accumulated reasoning_details so the we can update the
983
+ // reasoning part's provider metadata with the correct signature.
984
+ // The signature typically arrives in the last reasoning delta,
985
+ // but reasoning-start only carries the first delta's metadata.
986
+ metadata: accumulatedReasoningDetails.length > 0
987
+ ? { openRouter: { reasoningDetails: accumulatedReasoningDetails } }
988
+ : undefined
989
+ })
990
+ reasoningStarted = false
991
+ }
992
+
993
+ if (!textStarted) {
994
+ activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
995
+ parts.push({
996
+ type: "text-start",
997
+ id: activeTextId
998
+ })
999
+ textStarted = true
1000
+ }
1002
1001
 
1003
- if (!textStarted) {
1004
- activeTextId = openRouterResponseId ?? (yield* idGenerator.generateId())
1005
1002
  parts.push({
1006
- type: "text-start",
1007
- id: activeTextId
1003
+ type: "text-delta",
1004
+ id: activeTextId!,
1005
+ delta: content
1008
1006
  })
1009
- textStarted = true
1010
1007
  }
1011
1008
 
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)
1009
+ const annotations = delta.annotations
1010
+ if (Predicate.isNotNullish(annotations)) {
1011
+ for (const annotation of annotations) {
1012
+ if (annotation.type === "url_citation") {
1013
+ parts.push({
1014
+ type: "source",
1015
+ sourceType: "url",
1016
+ id: annotation.url_citation.url,
1017
+ url: annotation.url_citation.url,
1018
+ title: annotation.url_citation.title ?? "",
1019
+ metadata: {
1020
+ openrouter: {
1021
+ ...(Predicate.isNotUndefined(annotation.url_citation.content)
1022
+ ? { content: annotation.url_citation.content }
1023
+ : undefined),
1024
+ ...(Predicate.isNotUndefined(annotation.url_citation.start_index)
1025
+ ? { startIndex: annotation.url_citation.start_index }
1026
+ : undefined),
1027
+ ...(Predicate.isNotUndefined(annotation.url_citation.end_index)
1028
+ ? { startIndex: annotation.url_citation.end_index }
1029
+ : undefined)
1030
+ }
1040
1031
  }
1041
- }
1042
- })
1043
- } else if (annotation.type === "file") {
1044
- accumulatedFileAnnotations.push(annotation)
1032
+ })
1033
+ } else if (annotation.type === "file") {
1034
+ accumulatedFileAnnotations.push(annotation)
1035
+ }
1045
1036
  }
1046
1037
  }
1047
- }
1048
1038
 
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'"
1039
+ const toolCalls = delta.tool_calls
1040
+ if (Predicate.isNotNullish(toolCalls)) {
1041
+ for (const toolCall of toolCalls) {
1042
+ const index = toolCall.index ?? toolCalls.length - 1
1043
+ let activeToolCall = activeToolCalls[index]
1044
+
1045
+ // Tool call start - OpenRouter returns all information except the
1046
+ // tool call parameters in the first chunk
1047
+ if (Predicate.isUndefined(activeToolCall)) {
1048
+ if (toolCall.type !== "function") {
1049
+ return yield* AiError.make({
1050
+ module: "OpenRouterLanguageModel",
1051
+ method: "makeStreamResponse",
1052
+ reason: new AiError.InvalidOutputError({
1053
+ description: "Received tool call delta that was not of type: 'function'"
1054
+ })
1064
1055
  })
1065
- })
1066
- }
1056
+ }
1067
1057
 
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"
1058
+ if (Predicate.isNullish(toolCall.id)) {
1059
+ return yield* AiError.make({
1060
+ module: "OpenRouterLanguageModel",
1061
+ method: "makeStreamResponse",
1062
+ reason: new AiError.InvalidOutputError({
1063
+ description: "Received tool call delta without a tool call identifier"
1064
+ })
1074
1065
  })
1075
- })
1076
- }
1066
+ }
1077
1067
 
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"
1068
+ if (Predicate.isNullish(toolCall.function?.name)) {
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 name"
1074
+ })
1084
1075
  })
1085
- })
1086
- }
1076
+ }
1087
1077
 
1088
- activeToolCall = {
1089
- id: toolCall.id,
1090
- type: "function",
1091
- name: toolCall.function.name,
1092
- params: toolCall.function.arguments ?? ""
1093
- }
1078
+ activeToolCall = {
1079
+ id: toolCall.id,
1080
+ type: "function",
1081
+ name: toolCall.function.name,
1082
+ params: toolCall.function.arguments ?? ""
1083
+ }
1094
1084
 
1095
- activeToolCalls[index] = activeToolCall
1085
+ activeToolCalls[index] = activeToolCall
1096
1086
 
1097
- parts.push({
1098
- type: "tool-params-start",
1099
- id: activeToolCall.id,
1100
- name: activeToolCall.name
1101
- })
1087
+ parts.push({
1088
+ type: "tool-params-start",
1089
+ id: activeToolCall.id,
1090
+ name: activeToolCall.name
1091
+ })
1102
1092
 
1103
- // Emit a tool call delta part if parameters were also sent
1104
- if (activeToolCall.params.length > 0) {
1093
+ // Emit a tool call delta part if parameters were also sent
1094
+ if (activeToolCall.params.length > 0) {
1095
+ parts.push({
1096
+ type: "tool-params-delta",
1097
+ id: activeToolCall.id,
1098
+ delta: activeToolCall.params
1099
+ })
1100
+ }
1101
+ } else {
1102
+ // If an active tool call was found, update and emit the delta for
1103
+ // the tool call's parameters
1104
+ activeToolCall.params += toolCall.function?.arguments ?? ""
1105
1105
  parts.push({
1106
1106
  type: "tool-params-delta",
1107
1107
  id: activeToolCall.id,
1108
1108
  delta: activeToolCall.params
1109
1109
  })
1110
1110
  }
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
1111
 
1122
- // Check if the tool call is complete
1123
- // @effect-diagnostics-next-line tryCatchInEffectGen:off
1124
- try {
1125
- const params = Tool.unsafeSecureJsonParse(activeToolCall.params)
1112
+ // Check if the tool call is complete
1113
+ // @effect-diagnostics-next-line tryCatchInEffectGen:off
1114
+ try {
1115
+ const params = Tool.unsafeSecureJsonParse(activeToolCall.params)
1126
1116
 
1127
- parts.push({
1128
- type: "tool-params-end",
1129
- id: activeToolCall.id
1130
- })
1117
+ parts.push({
1118
+ type: "tool-params-end",
1119
+ id: activeToolCall.id
1120
+ })
1131
1121
 
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
- })
1122
+ parts.push({
1123
+ type: "tool-call",
1124
+ id: activeToolCall.id,
1125
+ name: activeToolCall.name,
1126
+ params,
1127
+ // Only attach reasoning_details to the first tool call to avoid
1128
+ // duplicating thinking blocks for parallel tool calls (Claude)
1129
+ metadata: reasoningDetailsAttachedToToolCall ? undefined : {
1130
+ openrouter: { reasoningDetails: accumulatedReasoningDetails }
1131
+ }
1132
+ })
1143
1133
 
1144
- reasoningDetailsAttachedToToolCall = true
1134
+ reasoningDetailsAttachedToToolCall = true
1145
1135
 
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
1136
+ // Increment the total tool calls emitted by the stream and
1137
+ // remove the active tool call
1138
+ totalToolCalls += 1
1139
+ delete activeToolCalls[toolCall.index]
1140
+ } catch {
1141
+ // Tool call incomplete, continue parsing
1142
+ continue
1143
+ }
1153
1144
  }
1154
1145
  }
1155
- }
1156
1146
 
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
- })
1147
+ const images = delta.images
1148
+ if (Predicate.isNotNullish(images)) {
1149
+ for (const image of images) {
1150
+ parts.push({
1151
+ type: "file",
1152
+ mediaType: getMediaType(image.image_url.url, "image/jpeg"),
1153
+ data: getBase64FromDataUrl(image.image_url.url)
1154
+ })
1155
+ }
1165
1156
  }
1166
1157
  }
1167
1158
 
@@ -192,12 +192,14 @@ export const parseRateLimitHeaders = (headers: Record<string, string>) => {
192
192
  let retryAfter: Duration.Duration | undefined
193
193
  if (Predicate.isNotUndefined(retryAfterRaw)) {
194
194
  const parsed = Number.parse(retryAfterRaw)
195
- if (Predicate.isNotUndefined(parsed)) {
196
- retryAfter = Duration.seconds(parsed)
195
+ if (Option.isSome(parsed)) {
196
+ retryAfter = Duration.seconds(parsed.value)
197
197
  }
198
198
  }
199
199
  const remainingRaw = headers["x-ratelimit-remaining-requests"]
200
- const remaining = Predicate.isNotUndefined(remainingRaw) ? Number.parse(remainingRaw) ?? null : null
200
+ const remaining = Predicate.isNotUndefined(remainingRaw)
201
+ ? Option.getOrNull(Number.parse(remainingRaw))
202
+ : null
201
203
  return {
202
204
  retryAfter,
203
205
  limit: headers["x-ratelimit-limit-requests"] ?? null,
@@ -218,7 +220,7 @@ export const buildHttpRequestDetails = (
218
220
  method: request.method,
219
221
  url: request.url,
220
222
  urlParams: Array.from(request.urlParams),
221
- hash: request.hash,
223
+ hash: Option.getOrUndefined(request.hash),
222
224
  headers: Redactable.redact(request.headers) as Record<string, string>
223
225
  })
224
226