@effect/ai-openrouter 0.8.3 → 4.0.0-beta.1
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 +19505 -0
- package/dist/Generated.d.ts.map +1 -0
- package/dist/Generated.js +5115 -0
- package/dist/Generated.js.map +1 -0
- package/dist/OpenRouterClient.d.ts +116 -0
- package/dist/OpenRouterClient.d.ts.map +1 -0
- package/dist/OpenRouterClient.js +120 -0
- package/dist/OpenRouterClient.js.map +1 -0
- package/dist/{dts/OpenRouterConfig.d.ts → OpenRouterConfig.d.ts} +9 -9
- package/dist/OpenRouterConfig.d.ts.map +1 -0
- package/dist/{esm/OpenRouterConfig.js → OpenRouterConfig.js} +8 -5
- package/dist/OpenRouterConfig.js.map +1 -0
- package/dist/OpenRouterError.d.ts +83 -0
- package/dist/OpenRouterError.d.ts.map +1 -0
- package/dist/OpenRouterError.js +10 -0
- package/dist/OpenRouterError.js.map +1 -0
- package/dist/OpenRouterLanguageModel.d.ts +285 -0
- package/dist/OpenRouterLanguageModel.d.ts.map +1 -0
- package/dist/OpenRouterLanguageModel.js +1210 -0
- package/dist/OpenRouterLanguageModel.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/errors.d.ts +2 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +347 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/{dts/internal → internal}/utilities.d.ts.map +1 -1
- package/dist/internal/utilities.js +77 -0
- package/dist/internal/utilities.js.map +1 -0
- package/package.json +45 -62
- package/src/Generated.ts +9312 -5435
- package/src/OpenRouterClient.ts +223 -304
- package/src/OpenRouterConfig.ts +14 -14
- package/src/OpenRouterError.ts +92 -0
- package/src/OpenRouterLanguageModel.ts +941 -570
- package/src/index.ts +20 -4
- package/src/internal/errors.ts +373 -0
- package/src/internal/utilities.ts +78 -11
- package/Generated/package.json +0 -6
- package/OpenRouterClient/package.json +0 -6
- package/OpenRouterConfig/package.json +0 -6
- package/OpenRouterLanguageModel/package.json +0 -6
- package/README.md +0 -5
- package/dist/cjs/Generated.js +0 -5813
- package/dist/cjs/Generated.js.map +0 -1
- package/dist/cjs/OpenRouterClient.js +0 -229
- package/dist/cjs/OpenRouterClient.js.map +0 -1
- package/dist/cjs/OpenRouterConfig.js +0 -30
- package/dist/cjs/OpenRouterConfig.js.map +0 -1
- package/dist/cjs/OpenRouterLanguageModel.js +0 -825
- package/dist/cjs/OpenRouterLanguageModel.js.map +0 -1
- package/dist/cjs/index.js +0 -16
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/internal/utilities.js +0 -29
- package/dist/cjs/internal/utilities.js.map +0 -1
- package/dist/dts/Generated.d.ts +0 -11026
- package/dist/dts/Generated.d.ts.map +0 -1
- package/dist/dts/OpenRouterClient.d.ts +0 -407
- package/dist/dts/OpenRouterClient.d.ts.map +0 -1
- package/dist/dts/OpenRouterConfig.d.ts.map +0 -1
- package/dist/dts/OpenRouterLanguageModel.d.ts +0 -215
- package/dist/dts/OpenRouterLanguageModel.d.ts.map +0 -1
- package/dist/dts/index.d.ts +0 -17
- package/dist/dts/index.d.ts.map +0 -1
- package/dist/esm/Generated.js +0 -5457
- package/dist/esm/Generated.js.map +0 -1
- package/dist/esm/OpenRouterClient.js +0 -214
- package/dist/esm/OpenRouterClient.js.map +0 -1
- package/dist/esm/OpenRouterConfig.js.map +0 -1
- package/dist/esm/OpenRouterLanguageModel.js +0 -814
- package/dist/esm/OpenRouterLanguageModel.js.map +0 -1
- package/dist/esm/index.js +0 -17
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/internal/utilities.js +0 -21
- package/dist/esm/internal/utilities.js.map +0 -1
- package/dist/esm/package.json +0 -4
- package/index/package.json +0 -6
- /package/dist/{dts/internal → internal}/utilities.d.ts +0 -0
package/src/index.ts
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @since 1.0.0
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// @barrel: Auto-generated exports. Do not edit manually.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @since 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
export * as Generated from "./Generated.ts"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @since 1.0.0
|
|
14
|
+
*/
|
|
15
|
+
export * as OpenRouterClient from "./OpenRouterClient.ts"
|
|
5
16
|
|
|
6
17
|
/**
|
|
7
18
|
* @since 1.0.0
|
|
8
19
|
*/
|
|
9
|
-
export * as
|
|
20
|
+
export * as OpenRouterConfig from "./OpenRouterConfig.ts"
|
|
10
21
|
|
|
11
22
|
/**
|
|
23
|
+
* OpenRouter error metadata augmentation.
|
|
24
|
+
*
|
|
25
|
+
* Provides OpenRouter-specific metadata fields for AI error types through
|
|
26
|
+
* module augmentation, enabling typed access to OpenRouter error details.
|
|
27
|
+
*
|
|
12
28
|
* @since 1.0.0
|
|
13
29
|
*/
|
|
14
|
-
export * as
|
|
30
|
+
export * as OpenRouterError from "./OpenRouterError.ts"
|
|
15
31
|
|
|
16
32
|
/**
|
|
17
33
|
* @since 1.0.0
|
|
18
34
|
*/
|
|
19
|
-
export * as OpenRouterLanguageModel from "./OpenRouterLanguageModel.
|
|
35
|
+
export * as OpenRouterLanguageModel from "./OpenRouterLanguageModel.ts"
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import { dual } from "effect/Function"
|
|
4
|
+
import * as Number from "effect/Number"
|
|
5
|
+
import * as Option from "effect/Option"
|
|
6
|
+
import * as Predicate from "effect/Predicate"
|
|
7
|
+
import * as Redactable from "effect/Redactable"
|
|
8
|
+
import * as Schema from "effect/Schema"
|
|
9
|
+
import * as AiError from "effect/unstable/ai/AiError"
|
|
10
|
+
import type * as Response from "effect/unstable/ai/Response"
|
|
11
|
+
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
|
|
12
|
+
import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
|
|
13
|
+
import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
|
|
14
|
+
import type * as Generated from "../Generated.ts"
|
|
15
|
+
import type { OpenRouterErrorMetadata } from "../OpenRouterError.ts"
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// OpenRouter Error Body Schema
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/** @internal */
|
|
22
|
+
export const OpenRouterErrorBody = Schema.Struct({
|
|
23
|
+
error: Schema.Struct({
|
|
24
|
+
message: Schema.String,
|
|
25
|
+
type: Schema.optional(Schema.NullOr(Schema.String)),
|
|
26
|
+
code: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Number.check(Schema.isFinite())])))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/** @internal */
|
|
31
|
+
export type OpenRouterClientErrorBody = {
|
|
32
|
+
readonly error: {
|
|
33
|
+
readonly code: string | number | null
|
|
34
|
+
readonly message: string
|
|
35
|
+
readonly param?: string | null
|
|
36
|
+
readonly type?: string | null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Error Mappers
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/** @internal */
|
|
45
|
+
export const mapSchemaError = dual<
|
|
46
|
+
(method: string) => (error: Schema.SchemaError) => AiError.AiError,
|
|
47
|
+
(error: Schema.SchemaError, method: string) => AiError.AiError
|
|
48
|
+
>(2, (error, method) =>
|
|
49
|
+
AiError.make({
|
|
50
|
+
module: "OpenRouterClient",
|
|
51
|
+
method,
|
|
52
|
+
reason: AiError.InvalidOutputError.fromSchemaError(error)
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
/** @internal */
|
|
56
|
+
export const mapClientError = dual<
|
|
57
|
+
(method: string) => (error: Generated.OpenRouterClientError<string, OpenRouterClientErrorBody>) => AiError.AiError,
|
|
58
|
+
(error: Generated.OpenRouterClientError<string, OpenRouterClientErrorBody>, method: string) => AiError.AiError
|
|
59
|
+
>(2, (error, method) => {
|
|
60
|
+
const { request, response, cause } = error
|
|
61
|
+
const status = response.status
|
|
62
|
+
const headers = response.headers as Record<string, string>
|
|
63
|
+
const metadata: OpenRouterErrorMetadata = {
|
|
64
|
+
errorCode: cause.error.code ?? null,
|
|
65
|
+
errorType: cause.error.type ?? null,
|
|
66
|
+
requestId: headers["x-request-id"] ?? null
|
|
67
|
+
}
|
|
68
|
+
const http = buildHttpContext({ request, response, body: JSON.stringify(cause) })
|
|
69
|
+
const reason = mapStatusCodeToReason({
|
|
70
|
+
status,
|
|
71
|
+
headers,
|
|
72
|
+
message: cause.error.message,
|
|
73
|
+
metadata,
|
|
74
|
+
http
|
|
75
|
+
})
|
|
76
|
+
return AiError.make({ module: "OpenRouterClient", method, reason })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/** @internal */
|
|
80
|
+
export const mapHttpClientError = dual<
|
|
81
|
+
(method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect<never, AiError.AiError>,
|
|
82
|
+
(error: HttpClientError.HttpClientError, method: string) => Effect.Effect<never, AiError.AiError>
|
|
83
|
+
>(2, (error, method) => {
|
|
84
|
+
const reason = error.reason
|
|
85
|
+
switch (reason._tag) {
|
|
86
|
+
case "TransportError": {
|
|
87
|
+
return Effect.fail(AiError.make({
|
|
88
|
+
module: "OpenRouterClient",
|
|
89
|
+
method,
|
|
90
|
+
reason: new AiError.NetworkError({
|
|
91
|
+
reason: "TransportError",
|
|
92
|
+
description: reason.description,
|
|
93
|
+
request: buildHttpRequestDetails(reason.request)
|
|
94
|
+
})
|
|
95
|
+
}))
|
|
96
|
+
}
|
|
97
|
+
case "EncodeError": {
|
|
98
|
+
return Effect.fail(AiError.make({
|
|
99
|
+
module: "OpenRouterClient",
|
|
100
|
+
method,
|
|
101
|
+
reason: new AiError.NetworkError({
|
|
102
|
+
reason: "EncodeError",
|
|
103
|
+
description: reason.description,
|
|
104
|
+
request: buildHttpRequestDetails(reason.request)
|
|
105
|
+
})
|
|
106
|
+
}))
|
|
107
|
+
}
|
|
108
|
+
case "InvalidUrlError": {
|
|
109
|
+
return Effect.fail(AiError.make({
|
|
110
|
+
module: "OpenRouterClient",
|
|
111
|
+
method,
|
|
112
|
+
reason: new AiError.NetworkError({
|
|
113
|
+
reason: "InvalidUrlError",
|
|
114
|
+
description: reason.description,
|
|
115
|
+
request: buildHttpRequestDetails(reason.request)
|
|
116
|
+
})
|
|
117
|
+
}))
|
|
118
|
+
}
|
|
119
|
+
case "StatusCodeError": {
|
|
120
|
+
return mapStatusCodeError(reason, method)
|
|
121
|
+
}
|
|
122
|
+
case "DecodeError": {
|
|
123
|
+
return Effect.fail(AiError.make({
|
|
124
|
+
module: "OpenRouterClient",
|
|
125
|
+
method,
|
|
126
|
+
reason: new AiError.InvalidOutputError({
|
|
127
|
+
description: reason.description ?? "Failed to decode response"
|
|
128
|
+
})
|
|
129
|
+
}))
|
|
130
|
+
}
|
|
131
|
+
case "EmptyBodyError": {
|
|
132
|
+
return Effect.fail(AiError.make({
|
|
133
|
+
module: "OpenRouterClient",
|
|
134
|
+
method,
|
|
135
|
+
reason: new AiError.InvalidOutputError({
|
|
136
|
+
description: reason.description ?? "Response body was empty"
|
|
137
|
+
})
|
|
138
|
+
}))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
/** @internal */
|
|
144
|
+
const mapStatusCodeError = Effect.fnUntraced(function*(
|
|
145
|
+
error: HttpClientError.StatusCodeError,
|
|
146
|
+
method: string
|
|
147
|
+
) {
|
|
148
|
+
const { request, response, description } = error
|
|
149
|
+
const status = response.status
|
|
150
|
+
const headers = response.headers as Record<string, string>
|
|
151
|
+
const requestId = headers["x-request-id"]
|
|
152
|
+
|
|
153
|
+
let body: string | undefined = description
|
|
154
|
+
if (!description || !description.startsWith("{")) {
|
|
155
|
+
const responseBody = yield* Effect.option(response.text)
|
|
156
|
+
if (Option.isSome(responseBody) && responseBody.value) {
|
|
157
|
+
body = responseBody.value
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let json: unknown = undefined
|
|
162
|
+
// @effect-diagnostics effect/tryCatchInEffectGen:off
|
|
163
|
+
try {
|
|
164
|
+
json = Predicate.isNotUndefined(body) ? JSON.parse(body) : undefined
|
|
165
|
+
} catch {
|
|
166
|
+
json = undefined
|
|
167
|
+
}
|
|
168
|
+
const decoded = Schema.decodeUnknownOption(OpenRouterErrorBody)(json)
|
|
169
|
+
|
|
170
|
+
const reason = mapStatusCodeToReason({
|
|
171
|
+
status,
|
|
172
|
+
headers,
|
|
173
|
+
message: Option.isSome(decoded) ? decoded.value.error.message : undefined,
|
|
174
|
+
http: buildHttpContext({ request, response, body }),
|
|
175
|
+
metadata: {
|
|
176
|
+
errorCode: Option.isSome(decoded) ? decoded.value.error.code ?? null : null,
|
|
177
|
+
errorType: Option.isSome(decoded) ? decoded.value.error.type ?? null : null,
|
|
178
|
+
requestId: requestId ?? null
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return yield* AiError.make({ module: "OpenRouterClient", method, reason })
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// Rate Limits
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
/** @internal */
|
|
190
|
+
export const parseRateLimitHeaders = (headers: Record<string, string>) => {
|
|
191
|
+
const retryAfterRaw = headers["retry-after"]
|
|
192
|
+
let retryAfter: Duration.Duration | undefined
|
|
193
|
+
if (Predicate.isNotUndefined(retryAfterRaw)) {
|
|
194
|
+
const parsed = Number.parse(retryAfterRaw)
|
|
195
|
+
if (Predicate.isNotUndefined(parsed)) {
|
|
196
|
+
retryAfter = Duration.seconds(parsed)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const remainingRaw = headers["x-ratelimit-remaining-requests"]
|
|
200
|
+
const remaining = Predicate.isNotUndefined(remainingRaw) ? Number.parse(remainingRaw) ?? null : null
|
|
201
|
+
return {
|
|
202
|
+
retryAfter,
|
|
203
|
+
limit: headers["x-ratelimit-limit-requests"] ?? null,
|
|
204
|
+
remaining,
|
|
205
|
+
resetRequests: headers["x-ratelimit-reset-requests"] ?? null,
|
|
206
|
+
resetTokens: headers["x-ratelimit-reset-tokens"] ?? null
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// =============================================================================
|
|
211
|
+
// HTTP Context
|
|
212
|
+
// =============================================================================
|
|
213
|
+
|
|
214
|
+
/** @internal */
|
|
215
|
+
export const buildHttpRequestDetails = (
|
|
216
|
+
request: HttpClientRequest.HttpClientRequest
|
|
217
|
+
): typeof Response.HttpRequestDetails.Type => ({
|
|
218
|
+
method: request.method,
|
|
219
|
+
url: request.url,
|
|
220
|
+
urlParams: Array.from(request.urlParams),
|
|
221
|
+
hash: request.hash,
|
|
222
|
+
headers: Redactable.redact(request.headers) as Record<string, string>
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
/** @internal */
|
|
226
|
+
export const buildHttpContext = (params: {
|
|
227
|
+
readonly request: HttpClientRequest.HttpClientRequest
|
|
228
|
+
readonly response?: HttpClientResponse.HttpClientResponse
|
|
229
|
+
readonly body?: string | undefined
|
|
230
|
+
}): typeof AiError.HttpContext.Type => ({
|
|
231
|
+
request: buildHttpRequestDetails(params.request),
|
|
232
|
+
response: Predicate.isNotUndefined(params.response)
|
|
233
|
+
? {
|
|
234
|
+
status: params.response.status,
|
|
235
|
+
headers: Redactable.redact(params.response.headers) as Record<string, string>
|
|
236
|
+
}
|
|
237
|
+
: undefined,
|
|
238
|
+
body: params.body
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// =============================================================================
|
|
242
|
+
// HTTP Status Code
|
|
243
|
+
// =============================================================================
|
|
244
|
+
|
|
245
|
+
const buildInvalidRequestDescription = (params: {
|
|
246
|
+
readonly status: number
|
|
247
|
+
readonly message: string | undefined
|
|
248
|
+
readonly method: string
|
|
249
|
+
readonly url: string
|
|
250
|
+
readonly errorCode: string | number | null
|
|
251
|
+
readonly errorType: string | null
|
|
252
|
+
readonly requestId: string | null
|
|
253
|
+
readonly body: string | undefined
|
|
254
|
+
}): string => {
|
|
255
|
+
const parts: Array<string> = []
|
|
256
|
+
|
|
257
|
+
if (params.message) {
|
|
258
|
+
parts.push(params.message)
|
|
259
|
+
} else {
|
|
260
|
+
parts.push(`HTTP ${params.status}`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
parts.push(`(${params.method} ${params.url})`)
|
|
264
|
+
|
|
265
|
+
if (params.errorCode) {
|
|
266
|
+
parts.push(`[code: ${params.errorCode}]`)
|
|
267
|
+
} else if (params.errorType) {
|
|
268
|
+
parts.push(`[type: ${params.errorType}]`)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (params.requestId) {
|
|
272
|
+
parts.push(`[requestId: ${params.requestId}]`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!params.message && params.body) {
|
|
276
|
+
const truncated = params.body.length > 200
|
|
277
|
+
? params.body.slice(0, 200) + "..."
|
|
278
|
+
: params.body
|
|
279
|
+
parts.push(`Response: ${truncated}`)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return parts.join(" ")
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** @internal */
|
|
286
|
+
export const mapStatusCodeToReason = ({ status, headers, message, metadata, http }: {
|
|
287
|
+
readonly status: number
|
|
288
|
+
readonly headers: Record<string, string>
|
|
289
|
+
readonly message: string | undefined
|
|
290
|
+
readonly metadata: OpenRouterErrorMetadata
|
|
291
|
+
readonly http: typeof AiError.HttpContext.Type
|
|
292
|
+
}): AiError.AiErrorReason => {
|
|
293
|
+
const invalidRequestDescription = buildInvalidRequestDescription({
|
|
294
|
+
status,
|
|
295
|
+
message,
|
|
296
|
+
method: http.request.method,
|
|
297
|
+
url: http.request.url,
|
|
298
|
+
errorCode: metadata.errorCode,
|
|
299
|
+
errorType: metadata.errorType,
|
|
300
|
+
requestId: metadata.requestId,
|
|
301
|
+
body: http.body
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
switch (status) {
|
|
305
|
+
case 400:
|
|
306
|
+
return new AiError.InvalidRequestError({
|
|
307
|
+
description: invalidRequestDescription,
|
|
308
|
+
metadata: { openrouter: metadata },
|
|
309
|
+
http
|
|
310
|
+
})
|
|
311
|
+
case 401:
|
|
312
|
+
return new AiError.AuthenticationError({
|
|
313
|
+
kind: "InvalidKey",
|
|
314
|
+
metadata: { openrouter: metadata },
|
|
315
|
+
http
|
|
316
|
+
})
|
|
317
|
+
case 403:
|
|
318
|
+
return new AiError.AuthenticationError({
|
|
319
|
+
kind: "InsufficientPermissions",
|
|
320
|
+
metadata: { openrouter: metadata },
|
|
321
|
+
http
|
|
322
|
+
})
|
|
323
|
+
case 404:
|
|
324
|
+
case 409:
|
|
325
|
+
case 422:
|
|
326
|
+
return new AiError.InvalidRequestError({
|
|
327
|
+
description: invalidRequestDescription,
|
|
328
|
+
metadata: { openrouter: metadata },
|
|
329
|
+
http
|
|
330
|
+
})
|
|
331
|
+
case 429: {
|
|
332
|
+
if (
|
|
333
|
+
metadata.errorCode === "insufficient_quota" ||
|
|
334
|
+
metadata.errorType === "insufficient_quota"
|
|
335
|
+
) {
|
|
336
|
+
return new AiError.QuotaExhaustedError({
|
|
337
|
+
metadata: { openrouter: metadata },
|
|
338
|
+
http
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
const { retryAfter, ...rateLimitMetadata } = parseRateLimitHeaders(headers)
|
|
342
|
+
return new AiError.RateLimitError({
|
|
343
|
+
retryAfter,
|
|
344
|
+
metadata: {
|
|
345
|
+
openrouter: {
|
|
346
|
+
...metadata,
|
|
347
|
+
...rateLimitMetadata
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
http
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
case 529:
|
|
354
|
+
return new AiError.InternalProviderError({
|
|
355
|
+
description: message ?? "OpenRouter API is overloaded",
|
|
356
|
+
metadata: { openrouter: metadata },
|
|
357
|
+
http
|
|
358
|
+
})
|
|
359
|
+
default:
|
|
360
|
+
if (status >= 500) {
|
|
361
|
+
return new AiError.InternalProviderError({
|
|
362
|
+
description: message ?? "Server error",
|
|
363
|
+
metadata: { openrouter: metadata },
|
|
364
|
+
http
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
return new AiError.UnknownError({
|
|
368
|
+
description: message,
|
|
369
|
+
metadata: { openrouter: metadata },
|
|
370
|
+
http
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -1,26 +1,93 @@
|
|
|
1
|
-
import type * as Response from "@effect/ai/Response"
|
|
2
1
|
import * as Predicate from "effect/Predicate"
|
|
3
|
-
import type * as
|
|
2
|
+
import type * as Response from "effect/unstable/ai/Response"
|
|
3
|
+
import type { ReasoningDetails } from "../OpenRouterLanguageModel.ts"
|
|
4
4
|
|
|
5
5
|
const finishReasonMap: Record<string, Response.FinishReason> = {
|
|
6
6
|
content_filter: "content-filter",
|
|
7
7
|
error: "error",
|
|
8
8
|
function_call: "tool-calls",
|
|
9
|
-
tool_calls: "tool-calls",
|
|
10
9
|
length: "length",
|
|
10
|
+
tool_calls: "tool-calls",
|
|
11
11
|
stop: "stop"
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** @internal */
|
|
15
15
|
export const resolveFinishReason = (
|
|
16
|
-
finishReason:
|
|
17
|
-
): Response.FinishReason =>
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
finishReason: string | null | undefined
|
|
17
|
+
): Response.FinishReason =>
|
|
18
|
+
Predicate.isNotNullish(finishReason)
|
|
19
|
+
? finishReasonMap[finishReason]
|
|
20
|
+
: "other"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tracks ReasoningDetailUnion entries and deduplicates them based
|
|
24
|
+
* on a derived canonical key.
|
|
25
|
+
*
|
|
26
|
+
* This is used when converting messages to ensure the API request only
|
|
27
|
+
* contains unique reasoning details, preventing "Duplicate item found with id"
|
|
28
|
+
* errors in multi-turn conversations.
|
|
29
|
+
*
|
|
30
|
+
* The canonical key logic matches the OpenRouter API's deduplication exactly
|
|
31
|
+
* (see openrouter-web/packages/llm-interfaces/reasonings/duplicate-tracker.ts):
|
|
32
|
+
* - Summary: key = summary field
|
|
33
|
+
* - Encrypted: key = id field (if truthy) or data field
|
|
34
|
+
* - Text: key = text field (if truthy) or signature field (if truthy)
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export class ReasoningDetailsDuplicateTracker {
|
|
39
|
+
readonly #seenKeys = new Set<string>()
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attempts to track a detail.
|
|
43
|
+
*
|
|
44
|
+
* @returns `true` if this is a NEW detail (not seen before and has valid key),
|
|
45
|
+
* or `false` if it was skipped (no valid key) or already seen (duplicate).
|
|
46
|
+
*/
|
|
47
|
+
upsert(detail: ReasoningDetails[number]): boolean {
|
|
48
|
+
const key = this.getCanonicalKey(detail)
|
|
49
|
+
|
|
50
|
+
if (Predicate.isNull(key)) {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.#seenKeys.has(key)) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.#seenKeys.add(key)
|
|
59
|
+
|
|
60
|
+
return true
|
|
20
61
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
62
|
+
|
|
63
|
+
private getCanonicalKey(detail: ReasoningDetails[number]): string | null {
|
|
64
|
+
// This logic matches the OpenRouter API's deduplication exactly.
|
|
65
|
+
// See: openrouter-web/packages/llm-interfaces/reasonings/duplicate-tracker.ts
|
|
66
|
+
switch (detail.type) {
|
|
67
|
+
case "reasoning.summary": {
|
|
68
|
+
return detail.summary
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case "reasoning.encrypted": {
|
|
72
|
+
return Predicate.isNotNullish(detail.id) ? detail.id : detail.data
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case "reasoning.text": {
|
|
76
|
+
if (Predicate.isNotNullish(detail.text)) {
|
|
77
|
+
return detail.text
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Predicate.isNotNullish(detail.signature)) {
|
|
81
|
+
return detail.signature
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
default: {
|
|
88
|
+
// Handle unknown types gracefully
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
24
92
|
}
|
|
25
|
-
return reason
|
|
26
93
|
}
|
package/Generated/package.json
DELETED