@codilore/llm 1.15.13
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/AGENTS.md +321 -0
- package/README.md +131 -0
- package/example/call-sites.md +591 -0
- package/example/tutorial.ts +255 -0
- package/package.json +50 -0
- package/script/recording-cost-report.ts +250 -0
- package/script/setup-recording-env.ts +542 -0
- package/src/cache-policy.ts +111 -0
- package/src/index.ts +32 -0
- package/src/llm.ts +186 -0
- package/src/protocols/anthropic-messages.ts +841 -0
- package/src/protocols/bedrock-converse.ts +649 -0
- package/src/protocols/bedrock-event-stream.ts +87 -0
- package/src/protocols/gemini.ts +465 -0
- package/src/protocols/index.ts +6 -0
- package/src/protocols/openai-chat.ts +431 -0
- package/src/protocols/openai-compatible-chat.ts +24 -0
- package/src/protocols/openai-responses.ts +987 -0
- package/src/protocols/shared.ts +283 -0
- package/src/protocols/utils/bedrock-auth.ts +70 -0
- package/src/protocols/utils/bedrock-cache.ts +37 -0
- package/src/protocols/utils/bedrock-media.ts +80 -0
- package/src/protocols/utils/cache.ts +16 -0
- package/src/protocols/utils/gemini-tool-schema.ts +101 -0
- package/src/protocols/utils/lifecycle.ts +102 -0
- package/src/protocols/utils/openai-options.ts +84 -0
- package/src/protocols/utils/tool-stream.ts +218 -0
- package/src/provider.ts +37 -0
- package/src/providers/amazon-bedrock.ts +43 -0
- package/src/providers/anthropic.ts +35 -0
- package/src/providers/azure.ts +110 -0
- package/src/providers/cloudflare.ts +127 -0
- package/src/providers/github-copilot.ts +66 -0
- package/src/providers/google.ts +35 -0
- package/src/providers/index.ts +11 -0
- package/src/providers/openai-compatible-profile.ts +20 -0
- package/src/providers/openai-compatible.ts +65 -0
- package/src/providers/openai-options.ts +81 -0
- package/src/providers/openai.ts +63 -0
- package/src/providers/openrouter.ts +98 -0
- package/src/providers/xai.ts +56 -0
- package/src/route/auth-options.ts +57 -0
- package/src/route/auth.ts +156 -0
- package/src/route/client.ts +434 -0
- package/src/route/endpoint.ts +53 -0
- package/src/route/executor.ts +374 -0
- package/src/route/framing.ts +27 -0
- package/src/route/index.ts +25 -0
- package/src/route/protocol.ts +84 -0
- package/src/route/transport/http.ts +108 -0
- package/src/route/transport/index.ts +33 -0
- package/src/route/transport/websocket.ts +280 -0
- package/src/schema/errors.ts +203 -0
- package/src/schema/events.ts +370 -0
- package/src/schema/ids.ts +43 -0
- package/src/schema/index.ts +5 -0
- package/src/schema/messages.ts +404 -0
- package/src/schema/options.ts +221 -0
- package/src/tool-runtime.ts +78 -0
- package/src/tool.ts +241 -0
- package/src/utils/record.ts +3 -0
- package/sst-env.d.ts +10 -0
- package/test/adapter.test.ts +164 -0
- package/test/auth-options.types.ts +168 -0
- package/test/auth.test.ts +103 -0
- package/test/cache-policy.test.ts +262 -0
- package/test/continuation-scenarios.ts +104 -0
- package/test/endpoint.test.ts +58 -0
- package/test/executor.test.ts +418 -0
- package/test/exports.test.ts +62 -0
- package/test/fixtures/media/restroom.png +0 -0
- package/test/fixtures/recordings/anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/anthropic-opus-4-7-image-tool-result.json +43 -0
- package/test/fixtures/recordings/anthropic-messages/claude-opus-4-7-drives-a-tool-loop.json +56 -0
- package/test/fixtures/recordings/anthropic-messages/rejects-malformed-assistant-tool-order-without-patch.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/streams-text.json +29 -0
- package/test/fixtures/recordings/anthropic-messages/streams-tool-call.json +29 -0
- package/test/fixtures/recordings/anthropic-messages-cache/writes-then-reads-cache-control-on-identical-second-call.json +48 -0
- package/test/fixtures/recordings/bedrock-converse/drives-a-tool-loop.json +55 -0
- package/test/fixtures/recordings/bedrock-converse/streams-a-tool-call.json +29 -0
- package/test/fixtures/recordings/bedrock-converse/streams-text.json +29 -0
- package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
- package/test/fixtures/recordings/cloudflare-ai-gateway/cloudflare-ai-gateway-workers-ai-llama-3-1-8b-text.json +32 -0
- package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-gpt-oss-20b-tools-tool-call.json +32 -0
- package/test/fixtures/recordings/cloudflare-workers-ai/cloudflare-workers-ai-llama-3-1-8b-text.json +32 -0
- package/test/fixtures/recordings/gemini/gemini-2-5-flash-image.json +32 -0
- package/test/fixtures/recordings/gemini/streams-text.json +28 -0
- package/test/fixtures/recordings/gemini/streams-tool-call.json +28 -0
- package/test/fixtures/recordings/gemini-cache/reports-cachedcontenttokencount-on-identical-second-call.json +46 -0
- package/test/fixtures/recordings/openai-chat/continues-after-tool-result.json +28 -0
- package/test/fixtures/recordings/openai-chat/drives-a-tool-loop-end-to-end.json +46 -0
- package/test/fixtures/recordings/openai-chat/streams-text.json +28 -0
- package/test/fixtures/recordings/openai-chat/streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/deepseek-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-llama-3-3-70b-drives-a-tool-loop.json +53 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/groq-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-claude-opus-4-7-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-4o-mini-drives-a-tool-loop.json +53 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-gpt-5-5-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/openrouter-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-compatible-chat/togetherai-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-drives-a-tool-loop.json +54 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-text.json +28 -0
- package/test/fixtures/recordings/openai-responses/gpt-5-5-streams-tool-call.json +28 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-image-tool-result.json +42 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning-continuation.json +58 -0
- package/test/fixtures/recordings/openai-responses/openai-responses-gpt-5-5-reasoning.json +32 -0
- package/test/fixtures/recordings/openai-responses-cache/reports-cached-tokens-on-identical-second-call.json +46 -0
- package/test/generate-object.test.ts +184 -0
- package/test/lib/effect.ts +50 -0
- package/test/lib/http.ts +98 -0
- package/test/lib/openai-chunks.ts +27 -0
- package/test/lib/sse.ts +17 -0
- package/test/lib/tool-runtime.ts +146 -0
- package/test/llm.test.ts +167 -0
- package/test/provider/anthropic-messages-cache.recorded.test.ts +54 -0
- package/test/provider/anthropic-messages.recorded.test.ts +46 -0
- package/test/provider/anthropic-messages.test.ts +829 -0
- package/test/provider/bedrock-converse-cache.recorded.test.ts +54 -0
- package/test/provider/bedrock-converse.test.ts +707 -0
- package/test/provider/cloudflare.test.ts +230 -0
- package/test/provider/gemini-cache.recorded.test.ts +48 -0
- package/test/provider/gemini.test.ts +476 -0
- package/test/provider/golden.recorded.test.ts +219 -0
- package/test/provider/openai-chat.test.ts +446 -0
- package/test/provider/openai-compatible-chat.test.ts +238 -0
- package/test/provider/openai-responses-cache.recorded.test.ts +46 -0
- package/test/provider/openai-responses.test.ts +1322 -0
- package/test/provider/openrouter.test.ts +56 -0
- package/test/provider.types.ts +41 -0
- package/test/recorded-golden.ts +97 -0
- package/test/recorded-runner.ts +100 -0
- package/test/recorded-scenarios.ts +531 -0
- package/test/recorded-test.ts +74 -0
- package/test/recorded-utils.ts +56 -0
- package/test/recorded-websocket.ts +26 -0
- package/test/route.test.ts +43 -0
- package/test/schema.test.ts +97 -0
- package/test/tool-runtime.test.ts +802 -0
- package/test/tool-stream.test.ts +99 -0
- package/test/tool.types.ts +40 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { describe, expect } from "bun:test"
|
|
2
|
+
import { Effect, Fiber, Layer, Random, Ref } from "effect"
|
|
3
|
+
import * as TestClock from "effect/testing/TestClock"
|
|
4
|
+
import { Headers, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
|
5
|
+
import { LLM, LLMError } from "../src"
|
|
6
|
+
import { LLMClient, RequestExecutor } from "../src/route"
|
|
7
|
+
import * as OpenAIChat from "../src/protocols/openai-chat"
|
|
8
|
+
import { dynamicResponse } from "./lib/http"
|
|
9
|
+
import { deltaChunk } from "./lib/openai-chunks"
|
|
10
|
+
import { sseRaw } from "./lib/sse"
|
|
11
|
+
import { it } from "./lib/effect"
|
|
12
|
+
|
|
13
|
+
const request = HttpClientRequest.post("https://provider.test/v1/chat?api_key=secret&key=secret&debug=1").pipe(
|
|
14
|
+
HttpClientRequest.setHeaders(Headers.fromInput({ authorization: "Bearer secret", "x-safe": "visible" })),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const secretRequest = HttpClientRequest.post("https://provider.test/v1/chat?api_key=query-secret-123&debug=1").pipe(
|
|
18
|
+
HttpClientRequest.setHeaders(Headers.fromInput({ authorization: "Bearer header-secret-456" })),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const responsesLayer = (responses: ReadonlyArray<Response>) =>
|
|
22
|
+
RequestExecutor.layer.pipe(
|
|
23
|
+
Layer.provide(
|
|
24
|
+
Layer.unwrap(
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const cursor = yield* Ref.make(0)
|
|
27
|
+
return Layer.succeed(
|
|
28
|
+
HttpClient.HttpClient,
|
|
29
|
+
HttpClient.make((request) =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const index = yield* Ref.getAndUpdate(cursor, (value) => value + 1)
|
|
32
|
+
return HttpClientResponse.fromWeb(request, responses[index] ?? responses[responses.length - 1])
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const countedResponsesLayer = (attempts: Ref.Ref<number>, responses: ReadonlyArray<Response>) =>
|
|
42
|
+
RequestExecutor.layer.pipe(
|
|
43
|
+
Layer.provide(
|
|
44
|
+
Layer.unwrap(
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const cursor = yield* Ref.make(0)
|
|
47
|
+
return Layer.succeed(
|
|
48
|
+
HttpClient.HttpClient,
|
|
49
|
+
HttpClient.make((request) =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
yield* Ref.update(attempts, (value) => value + 1)
|
|
52
|
+
const index = yield* Ref.getAndUpdate(cursor, (value) => value + 1)
|
|
53
|
+
return HttpClientResponse.fromWeb(request, responses[index] ?? responses[responses.length - 1])
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const randomMidpoint = {
|
|
63
|
+
nextDoubleUnsafe: () => 0.5,
|
|
64
|
+
nextIntUnsafe: () => 0,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const expectLLMError = (error: unknown) => {
|
|
68
|
+
expect(error).toBeInstanceOf(LLMError)
|
|
69
|
+
if (!(error instanceof LLMError)) throw new Error("expected LLMError")
|
|
70
|
+
return error
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const errorHttp = (error: LLMError) => ("http" in error.reason ? error.reason.http : undefined)
|
|
74
|
+
|
|
75
|
+
describe("RequestExecutor", () => {
|
|
76
|
+
it.effect("returns redacted diagnostics for retryable rate limits", () =>
|
|
77
|
+
Effect.gen(function* () {
|
|
78
|
+
const executor = yield* RequestExecutor.Service
|
|
79
|
+
const error = yield* executor.execute(request).pipe(Effect.flip)
|
|
80
|
+
|
|
81
|
+
expectLLMError(error)
|
|
82
|
+
expect(error).toMatchObject({
|
|
83
|
+
retryable: true,
|
|
84
|
+
retryAfterMs: 0,
|
|
85
|
+
reason: {
|
|
86
|
+
_tag: "RateLimit",
|
|
87
|
+
rateLimit: { retryAfterMs: 0 },
|
|
88
|
+
http: {
|
|
89
|
+
requestId: "req_123",
|
|
90
|
+
request: {
|
|
91
|
+
method: "POST",
|
|
92
|
+
url: "https://provider.test/v1/chat?api_key=%3Credacted%3E&key=%3Credacted%3E&debug=1",
|
|
93
|
+
headers: { authorization: "<redacted>", "x-safe": "visible" },
|
|
94
|
+
},
|
|
95
|
+
response: {
|
|
96
|
+
status: 429,
|
|
97
|
+
headers: {
|
|
98
|
+
"retry-after-ms": "0",
|
|
99
|
+
"x-request-id": "req_123",
|
|
100
|
+
"x-api-key": "<redacted>",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
expect(errorHttp(error)?.body).toBe("rate limited")
|
|
107
|
+
}).pipe(
|
|
108
|
+
Effect.provide(
|
|
109
|
+
responsesLayer(
|
|
110
|
+
Array.from(
|
|
111
|
+
{ length: 3 },
|
|
112
|
+
() =>
|
|
113
|
+
new Response("rate limited", {
|
|
114
|
+
status: 429,
|
|
115
|
+
headers: { "retry-after-ms": "0", "x-request-id": "req_123", "x-api-key": "secret" },
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
it.effect("honors current redacted header names in diagnostics", () =>
|
|
124
|
+
Effect.gen(function* () {
|
|
125
|
+
const executor = yield* RequestExecutor.Service
|
|
126
|
+
const error = yield* executor.execute(request).pipe(Effect.flip)
|
|
127
|
+
|
|
128
|
+
expectLLMError(error)
|
|
129
|
+
expect(errorHttp(error)?.request.headers["x-safe"]).toBe("<redacted>")
|
|
130
|
+
expect(errorHttp(error)?.response?.headers["x-safe"]).toBe("<redacted>")
|
|
131
|
+
}).pipe(
|
|
132
|
+
Effect.provide(responsesLayer([new Response("bad", { status: 400, headers: { "x-safe": "response-secret" } })])),
|
|
133
|
+
Effect.provideService(Headers.CurrentRedactedNames, ["x-safe"]),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
it.effect("extracts OpenAI-style rate-limit diagnostics", () =>
|
|
138
|
+
Effect.gen(function* () {
|
|
139
|
+
const executor = yield* RequestExecutor.Service
|
|
140
|
+
const error = yield* executor.execute(request).pipe(Effect.flip)
|
|
141
|
+
|
|
142
|
+
expectLLMError(error)
|
|
143
|
+
expect(error.reason).toMatchObject({ _tag: "RateLimit" })
|
|
144
|
+
expect(error.reason._tag === "RateLimit" ? error.reason.rateLimit : undefined).toEqual({
|
|
145
|
+
retryAfterMs: 0,
|
|
146
|
+
limit: { requests: "500", tokens: "30000" },
|
|
147
|
+
remaining: { requests: "499", tokens: "29900" },
|
|
148
|
+
reset: { requests: "1s", tokens: "10s" },
|
|
149
|
+
})
|
|
150
|
+
}).pipe(
|
|
151
|
+
Effect.provide(
|
|
152
|
+
responsesLayer(
|
|
153
|
+
Array.from(
|
|
154
|
+
{ length: 3 },
|
|
155
|
+
() =>
|
|
156
|
+
new Response("rate limited", {
|
|
157
|
+
status: 429,
|
|
158
|
+
headers: {
|
|
159
|
+
"retry-after-ms": "0",
|
|
160
|
+
"x-ratelimit-limit-requests": "500",
|
|
161
|
+
"x-ratelimit-limit-tokens": "30000",
|
|
162
|
+
"x-ratelimit-remaining-requests": "499",
|
|
163
|
+
"x-ratelimit-remaining-tokens": "29900",
|
|
164
|
+
"x-ratelimit-reset-requests": "1s",
|
|
165
|
+
"x-ratelimit-reset-tokens": "10s",
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
it.effect("extracts Anthropic-style rate-limit diagnostics", () =>
|
|
175
|
+
Effect.gen(function* () {
|
|
176
|
+
const executor = yield* RequestExecutor.Service
|
|
177
|
+
const error = yield* executor.execute(request).pipe(Effect.flip)
|
|
178
|
+
|
|
179
|
+
expectLLMError(error)
|
|
180
|
+
expect(error.reason).toMatchObject({ _tag: "ProviderInternal" })
|
|
181
|
+
expect(errorHttp(error)?.rateLimit).toEqual({
|
|
182
|
+
retryAfterMs: 0,
|
|
183
|
+
limit: { requests: "100", "input-tokens": "10000" },
|
|
184
|
+
remaining: { requests: "12", "input-tokens": "9000" },
|
|
185
|
+
reset: { requests: "2026-05-06T12:00:00Z", "input-tokens": "2026-05-06T12:00:10Z" },
|
|
186
|
+
})
|
|
187
|
+
}).pipe(
|
|
188
|
+
Effect.provide(
|
|
189
|
+
responsesLayer(
|
|
190
|
+
Array.from(
|
|
191
|
+
{ length: 3 },
|
|
192
|
+
() =>
|
|
193
|
+
new Response("overloaded", {
|
|
194
|
+
status: 529,
|
|
195
|
+
headers: {
|
|
196
|
+
"retry-after-ms": "0",
|
|
197
|
+
"anthropic-ratelimit-requests-limit": "100",
|
|
198
|
+
"anthropic-ratelimit-requests-remaining": "12",
|
|
199
|
+
"anthropic-ratelimit-requests-reset": "2026-05-06T12:00:00Z",
|
|
200
|
+
"anthropic-ratelimit-input-tokens-limit": "10000",
|
|
201
|
+
"anthropic-ratelimit-input-tokens-remaining": "9000",
|
|
202
|
+
"anthropic-ratelimit-input-tokens-reset": "2026-05-06T12:00:10Z",
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
),
|
|
206
|
+
),
|
|
207
|
+
),
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
it.effect("retries retryable status responses before returning the stream", () =>
|
|
212
|
+
Effect.gen(function* () {
|
|
213
|
+
const executor = yield* RequestExecutor.Service
|
|
214
|
+
const response = yield* executor.execute(request)
|
|
215
|
+
|
|
216
|
+
expect(response.status).toBe(200)
|
|
217
|
+
expect(yield* response.text).toBe("ok")
|
|
218
|
+
}).pipe(
|
|
219
|
+
Effect.provide(
|
|
220
|
+
responsesLayer([
|
|
221
|
+
new Response("busy", { status: 503, headers: { "retry-after-ms": "0" } }),
|
|
222
|
+
new Response("ok", { status: 200 }),
|
|
223
|
+
]),
|
|
224
|
+
),
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
it.effect("marks 504 and 529 status responses retryable", () =>
|
|
229
|
+
Effect.gen(function* () {
|
|
230
|
+
const failWith = (status: number) =>
|
|
231
|
+
Effect.gen(function* () {
|
|
232
|
+
const executor = yield* RequestExecutor.Service
|
|
233
|
+
const error = yield* executor.execute(request).pipe(Effect.flip)
|
|
234
|
+
|
|
235
|
+
expectLLMError(error)
|
|
236
|
+
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", status })
|
|
237
|
+
expect(error.retryable).toBe(true)
|
|
238
|
+
}).pipe(
|
|
239
|
+
Effect.provide(
|
|
240
|
+
responsesLayer(
|
|
241
|
+
Array.from(
|
|
242
|
+
{ length: 3 },
|
|
243
|
+
() =>
|
|
244
|
+
new Response("retry", {
|
|
245
|
+
status,
|
|
246
|
+
headers: { "retry-after-ms": "0" },
|
|
247
|
+
}),
|
|
248
|
+
),
|
|
249
|
+
),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
yield* failWith(504)
|
|
254
|
+
yield* failWith(529)
|
|
255
|
+
}),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
it.effect("does not retry non-retryable status responses and truncates large bodies", () =>
|
|
259
|
+
Effect.gen(function* () {
|
|
260
|
+
const executor = yield* RequestExecutor.Service
|
|
261
|
+
const error = yield* executor.execute(request).pipe(Effect.flip)
|
|
262
|
+
|
|
263
|
+
expectLLMError(error)
|
|
264
|
+
expect(error.reason).toMatchObject({ _tag: "Authentication" })
|
|
265
|
+
expect(error.retryable).toBe(false)
|
|
266
|
+
expect(errorHttp(error)?.bodyTruncated).toBe(true)
|
|
267
|
+
expect(errorHttp(error)?.body).toHaveLength(16_384)
|
|
268
|
+
}).pipe(
|
|
269
|
+
Effect.provide(
|
|
270
|
+
responsesLayer([
|
|
271
|
+
new Response("x".repeat(20_000), { status: 401 }),
|
|
272
|
+
new Response("should not retry", { status: 200 }),
|
|
273
|
+
]),
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
it.effect("redacts common secret fields in response bodies", () =>
|
|
279
|
+
Effect.gen(function* () {
|
|
280
|
+
const executor = yield* RequestExecutor.Service
|
|
281
|
+
const error = yield* executor.execute(request).pipe(Effect.flip)
|
|
282
|
+
|
|
283
|
+
expectLLMError(error)
|
|
284
|
+
expect(errorHttp(error)?.body).toContain('"key":"<redacted>"')
|
|
285
|
+
expect(errorHttp(error)?.body).toContain("api_key=<redacted>")
|
|
286
|
+
expect(errorHttp(error)?.body).not.toContain("body-secret")
|
|
287
|
+
expect(errorHttp(error)?.body).not.toContain("query-secret")
|
|
288
|
+
}).pipe(
|
|
289
|
+
Effect.provide(
|
|
290
|
+
responsesLayer([
|
|
291
|
+
new Response('{"error":{"message":"bad","key":"body-secret","detail":"api_key=query-secret"}}', {
|
|
292
|
+
status: 400,
|
|
293
|
+
}),
|
|
294
|
+
]),
|
|
295
|
+
),
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
it.effect("redacts echoed request secret values in response bodies", () =>
|
|
300
|
+
Effect.gen(function* () {
|
|
301
|
+
const executor = yield* RequestExecutor.Service
|
|
302
|
+
const error = yield* executor.execute(secretRequest).pipe(Effect.flip)
|
|
303
|
+
|
|
304
|
+
expectLLMError(error)
|
|
305
|
+
expect(errorHttp(error)?.body).toContain("provider echoed <redacted>")
|
|
306
|
+
expect(errorHttp(error)?.body).toContain("authorization <redacted>")
|
|
307
|
+
expect(errorHttp(error)?.body).not.toContain("query-secret-123")
|
|
308
|
+
expect(errorHttp(error)?.body).not.toContain("header-secret-456")
|
|
309
|
+
}).pipe(
|
|
310
|
+
Effect.provide(
|
|
311
|
+
responsesLayer([
|
|
312
|
+
new Response("provider echoed query-secret-123 and authorization header-secret-456", { status: 400 }),
|
|
313
|
+
]),
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
it.effect("honors Retry-After delta seconds before retrying", () =>
|
|
319
|
+
Effect.gen(function* () {
|
|
320
|
+
const attempts = yield* Ref.make(0)
|
|
321
|
+
return yield* Effect.gen(function* () {
|
|
322
|
+
const executor = yield* RequestExecutor.Service
|
|
323
|
+
const fiber = yield* executor.execute(request).pipe(Effect.forkChild)
|
|
324
|
+
|
|
325
|
+
yield* Effect.yieldNow
|
|
326
|
+
expect(yield* Ref.get(attempts)).toBe(1)
|
|
327
|
+
|
|
328
|
+
yield* TestClock.adjust(1_999)
|
|
329
|
+
yield* Effect.yieldNow
|
|
330
|
+
expect(yield* Ref.get(attempts)).toBe(1)
|
|
331
|
+
|
|
332
|
+
yield* TestClock.adjust(1)
|
|
333
|
+
const response = yield* Fiber.join(fiber)
|
|
334
|
+
|
|
335
|
+
expect(response.status).toBe(200)
|
|
336
|
+
expect(yield* Ref.get(attempts)).toBe(2)
|
|
337
|
+
}).pipe(
|
|
338
|
+
Effect.provide(
|
|
339
|
+
countedResponsesLayer(attempts, [
|
|
340
|
+
new Response("busy", { status: 503, headers: { "retry-after": "2" } }),
|
|
341
|
+
new Response("ok", { status: 200 }),
|
|
342
|
+
]),
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
}),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
it.effect("uses exponential jittered delay when retry-after is absent", () =>
|
|
349
|
+
Effect.gen(function* () {
|
|
350
|
+
const attempts = yield* Ref.make(0)
|
|
351
|
+
return yield* Effect.gen(function* () {
|
|
352
|
+
const executor = yield* RequestExecutor.Service
|
|
353
|
+
const fiber = yield* executor.execute(request).pipe(Effect.flip, Effect.forkChild)
|
|
354
|
+
|
|
355
|
+
yield* Effect.yieldNow
|
|
356
|
+
expect(yield* Ref.get(attempts)).toBe(1)
|
|
357
|
+
|
|
358
|
+
yield* TestClock.adjust(499)
|
|
359
|
+
yield* Effect.yieldNow
|
|
360
|
+
expect(yield* Ref.get(attempts)).toBe(1)
|
|
361
|
+
|
|
362
|
+
yield* TestClock.adjust(1)
|
|
363
|
+
yield* Effect.yieldNow
|
|
364
|
+
expect(yield* Ref.get(attempts)).toBe(2)
|
|
365
|
+
|
|
366
|
+
yield* TestClock.adjust(999)
|
|
367
|
+
yield* Effect.yieldNow
|
|
368
|
+
expect(yield* Ref.get(attempts)).toBe(2)
|
|
369
|
+
|
|
370
|
+
yield* TestClock.adjust(1)
|
|
371
|
+
const error = yield* Fiber.join(fiber)
|
|
372
|
+
|
|
373
|
+
expectLLMError(error)
|
|
374
|
+
expect(error.reason).toMatchObject({ _tag: "ProviderInternal" })
|
|
375
|
+
expect(yield* Ref.get(attempts)).toBe(3)
|
|
376
|
+
}).pipe(
|
|
377
|
+
Effect.provide(
|
|
378
|
+
countedResponsesLayer(attempts, [
|
|
379
|
+
new Response("busy", { status: 503 }),
|
|
380
|
+
new Response("still busy", { status: 503 }),
|
|
381
|
+
new Response("done retrying", { status: 503 }),
|
|
382
|
+
]),
|
|
383
|
+
),
|
|
384
|
+
)
|
|
385
|
+
}).pipe(Effect.provideService(Random.Random, randomMidpoint)),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
it.effect("does not retry after a successful response reaches stream parsing", () =>
|
|
389
|
+
Effect.gen(function* () {
|
|
390
|
+
const attempts = yield* Ref.make(0)
|
|
391
|
+
const model = OpenAIChat.route
|
|
392
|
+
.with({ endpoint: { baseURL: "https://api.openai.test/v1" } })
|
|
393
|
+
.model({ id: "gpt-4o-mini" })
|
|
394
|
+
const error = yield* LLMClient.generate(LLM.request({ model, prompt: "Say hello." })).pipe(
|
|
395
|
+
Effect.provide(
|
|
396
|
+
dynamicResponse((input) =>
|
|
397
|
+
Ref.update(attempts, (value) => value + 1).pipe(
|
|
398
|
+
Effect.as(
|
|
399
|
+
input.respond(
|
|
400
|
+
sseRaw(
|
|
401
|
+
`data: ${JSON.stringify(deltaChunk({ role: "assistant", content: "Hello" }))}`,
|
|
402
|
+
"data: not-json",
|
|
403
|
+
),
|
|
404
|
+
{ headers: { "content-type": "text/event-stream" } },
|
|
405
|
+
),
|
|
406
|
+
),
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
),
|
|
410
|
+
Effect.flip,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
expectLLMError(error)
|
|
414
|
+
expect(error.reason).toMatchObject({ _tag: "InvalidProviderOutput" })
|
|
415
|
+
expect(yield* Ref.get(attempts)).toBe(1)
|
|
416
|
+
}),
|
|
417
|
+
)
|
|
418
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { LLM, LLMClient, Provider } from "@codilore/llm"
|
|
3
|
+
import { Route, Protocol } from "@codilore/llm/route"
|
|
4
|
+
import { Provider as ProviderSubpath } from "@codilore/llm/provider"
|
|
5
|
+
import {
|
|
6
|
+
CloudflareAIGateway,
|
|
7
|
+
CloudflareWorkersAI,
|
|
8
|
+
OpenAI,
|
|
9
|
+
OpenAICompatible,
|
|
10
|
+
OpenRouter,
|
|
11
|
+
XAI,
|
|
12
|
+
} from "@codilore/llm/providers"
|
|
13
|
+
import * as GitHubCopilot from "@codilore/llm/providers/github-copilot"
|
|
14
|
+
import { OpenAIChat, OpenAICompatibleChat, OpenAIResponses } from "@codilore/llm/protocols"
|
|
15
|
+
import * as AnthropicMessages from "@codilore/llm/protocols/anthropic-messages"
|
|
16
|
+
|
|
17
|
+
describe("public exports", () => {
|
|
18
|
+
test("root exposes app-facing runtime APIs", () => {
|
|
19
|
+
expect(LLM.request).toBeFunction()
|
|
20
|
+
expect(LLMClient.Service).toBeFunction()
|
|
21
|
+
expect(LLMClient.layer).toBeDefined()
|
|
22
|
+
expect(Provider.make).toBeFunction()
|
|
23
|
+
expect(ProviderSubpath.make).toBe(Provider.make)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("route barrel exposes route-authoring APIs", () => {
|
|
27
|
+
expect(Route.make).toBeFunction()
|
|
28
|
+
expect(Protocol.make).toBeFunction()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("provider barrels expose user-facing facades", () => {
|
|
32
|
+
expect(OpenAI.model).toBeFunction()
|
|
33
|
+
expect(OpenAI.provider.model).toBe(OpenAI.model)
|
|
34
|
+
expect(OpenAI.provider.responses).toBe(OpenAI.responses)
|
|
35
|
+
expect(OpenAI.provider.responsesWebSocket).toBe(OpenAI.responsesWebSocket)
|
|
36
|
+
expect(OpenAI.configure({ apiKey: "fixture" }).responses).toBeFunction()
|
|
37
|
+
expect(OpenAICompatible.deepseek.model).toBeFunction()
|
|
38
|
+
expect(CloudflareAIGateway.configure).toBeFunction()
|
|
39
|
+
expect(CloudflareAIGateway.configure({ accountId: "fixture", gatewayApiKey: "fixture" }).model).toBeFunction()
|
|
40
|
+
expect(CloudflareWorkersAI.configure).toBeFunction()
|
|
41
|
+
expect(CloudflareWorkersAI.configure({ accountId: "fixture", apiKey: "fixture" }).model).toBeFunction()
|
|
42
|
+
expect(OpenRouter.model).toBeFunction()
|
|
43
|
+
expect(OpenRouter.provider.model).toBe(OpenRouter.model)
|
|
44
|
+
expect(XAI.model).toBeFunction()
|
|
45
|
+
expect(XAI.provider.model).toBe(XAI.model)
|
|
46
|
+
expect(XAI.provider.responses).toBe(XAI.responses)
|
|
47
|
+
expect(XAI.provider.chat).toBe(XAI.chat)
|
|
48
|
+
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses")
|
|
49
|
+
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat")
|
|
50
|
+
expect(
|
|
51
|
+
GitHubCopilot.configure({ baseURL: "https://api.githubcopilot.test", apiKey: "fixture" }).model,
|
|
52
|
+
).toBeFunction()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("protocol barrels expose supported low-level routes", () => {
|
|
56
|
+
expect(OpenAIChat.route.id).toBe("openai-chat")
|
|
57
|
+
expect(OpenAICompatibleChat.route.id).toBe("openai-compatible-chat")
|
|
58
|
+
expect(OpenAIResponses.route.id).toBe("openai-responses")
|
|
59
|
+
expect(OpenAIResponses.webSocketRoute.id).toBe("openai-responses-websocket")
|
|
60
|
+
expect(AnthropicMessages.route.id).toBe("anthropic-messages")
|
|
61
|
+
})
|
|
62
|
+
})
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"metadata": {
|
|
4
|
+
"name": "anthropic-messages/accepts-malformed-assistant-tool-order-with-default-patch",
|
|
5
|
+
"recordedAt": "2026-05-05T20:09:16.245Z",
|
|
6
|
+
"tags": ["prefix:anthropic-messages", "provider:anthropic", "protocol:anthropic-messages", "tool"]
|
|
7
|
+
},
|
|
8
|
+
"interactions": [
|
|
9
|
+
{
|
|
10
|
+
"transport": "http",
|
|
11
|
+
"request": {
|
|
12
|
+
"method": "POST",
|
|
13
|
+
"url": "https://api.anthropic.com/v1/messages",
|
|
14
|
+
"headers": {
|
|
15
|
+
"anthropic-version": "2023-06-01",
|
|
16
|
+
"content-type": "application/json"
|
|
17
|
+
},
|
|
18
|
+
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"messages\":[{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I will check the weather.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_1\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_1\",\"content\":\"{\\\"temperature\\\":\\\"72F\\\"}\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use that result to answer briefly.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather\",\"input_schema\":{\"type\":\"object\",\"properties\":{}}}],\"stream\":true,\"max_tokens\":4096}"
|
|
19
|
+
},
|
|
20
|
+
"response": {
|
|
21
|
+
"status": 200,
|
|
22
|
+
"headers": {
|
|
23
|
+
"content-type": "text/event-stream; charset=utf-8"
|
|
24
|
+
},
|
|
25
|
+
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01SikJVFaMR1XLMtavUhvuog\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":638,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" weather in Paris is currently 72°F.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":638,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":14} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"metadata": {
|
|
4
|
+
"name": "anthropic-messages/anthropic-opus-4-7-image-tool-result",
|
|
5
|
+
"recordedAt": "2026-05-22T01:57:05.693Z",
|
|
6
|
+
"provider": "anthropic",
|
|
7
|
+
"route": "anthropic-messages",
|
|
8
|
+
"transport": "http",
|
|
9
|
+
"model": "claude-opus-4-7",
|
|
10
|
+
"tags": [
|
|
11
|
+
"prefix:anthropic-messages",
|
|
12
|
+
"provider:anthropic",
|
|
13
|
+
"flagship",
|
|
14
|
+
"media",
|
|
15
|
+
"image",
|
|
16
|
+
"vision",
|
|
17
|
+
"tool",
|
|
18
|
+
"tool-result",
|
|
19
|
+
"golden"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"interactions": [
|
|
23
|
+
{
|
|
24
|
+
"transport": "http",
|
|
25
|
+
"request": {
|
|
26
|
+
"method": "POST",
|
|
27
|
+
"url": "https://api.anthropic.com/v1/messages",
|
|
28
|
+
"headers": {
|
|
29
|
+
"anthropic-version": "2023-06-01",
|
|
30
|
+
"content-type": "application/json"
|
|
31
|
+
},
|
|
32
|
+
"body": "{\"model\":\"claude-opus-4-7\",\"system\":[{\"type\":\"text\",\"text\":\"Read images carefully. Reply only with the visible text, lowercase, no punctuation.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Use the read_screenshot tool, then reply with the words shown.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_screenshot_1\",\"name\":\"read_screenshot\",\"input\":{}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_screenshot_1\",\"content\":[{\"type\":\"text\",\"text\":\"Image read successfully\"},{\"type\":\"image\",\"source\":{\"type\":\"base64\",\"media_type\":\"image/png\",\"data\":\"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\"}}]}]}],\"tools\":[{\"name\":\"read_screenshot\",\"description\":\"Capture a screenshot of the current screen.\",\"input_schema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":40}"
|
|
33
|
+
},
|
|
34
|
+
"response": {
|
|
35
|
+
"status": 200,
|
|
36
|
+
"headers": {
|
|
37
|
+
"content-type": "text/event-stream; charset=utf-8"
|
|
38
|
+
},
|
|
39
|
+
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-opus-4-7\",\"id\":\"msg_017z3Dpfd8nim5vfCmcAQMFS\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1005,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"j\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"iggling restroom prison\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":1005,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":13} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|