1import { type Generated, OpenAiClient, OpenAiLanguageModel, OpenAiSchema, OpenAiTool } from "@effect/ai-openai"
2import { assert, describe, it } from "@effect/vitest"
3import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils"
4import { Array, Context, Effect, Layer, Redacted, Ref, Schema, SchemaGetter, Stream } from "effect"
5import { AiError, LanguageModel, Prompt, Response as AiResponse, Tool, Toolkit } from "effect/ai"
6import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"
7
8const webSearchFailures = [
9 { tool: OpenAiTool.WebSearch({}), status: "failed" },
10 { tool: OpenAiTool.WebSearch({}), status: "incomplete" },
11 { tool: OpenAiTool.WebSearchPreview({}), status: "searching" }
12] as const
13
14const fileSearchOutcomes = [
15 { status: "completed", isFailure: false, results: undefined },
16 { status: "failed", isFailure: true, results: null },
17 { status: "incomplete", isFailure: true, results: [{ file_id: "file_123", text: "Matching text" }] }
18] as const
19
20describe("OpenAiLanguageModel", () => {
21 describe("make", () => {
22 it.effect("sends correct model in request", () =>
23 Effect.gen(function*() {
24 const result = yield* LanguageModel.generateText({ prompt: "test" }).pipe(
25 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))
26 )
27
28 const metadata = result.content.find((part) => part.type === "response-metadata")
29
30 strictEqual(metadata?.modelId, "gpt-4o-mini")
31 }).pipe(Effect.provide(makeTestLayer())))
32
33 it.effect("sends custom model string in request", () =>
34 Effect.gen(function*() {
35 const result = yield* LanguageModel.generateText({ prompt: "test" }).pipe(
36 Effect.provide(OpenAiLanguageModel.model("ft:gpt-4o-mini:custom"))
37 )
38
39 const metadata = result.content.find((part) => part.type === "response-metadata")
40 strictEqual(metadata?.modelId, "ft:gpt-4o-mini:custom")
41 }).pipe(Effect.provide(makeTestLayer({ body: { model: "ft:gpt-4o-mini:custom" as any } }))))
42 })
43
44 describe("web search sources", () => {
45 it.effect.each(["generateText", "streamText"] as const)(
46 "preserves URL and API sources with %s",
47 (method) =>
48 Effect.gen(function*() {
49 const sources = [
50 { type: "url", url: "https://example.com/weather" },
51 { type: "api", name: "oai-weather" }
52 ] as const
53 const action = { type: "search", query: "weather in London", sources } as const
54 const item = makeWebSearchCall({ action })
55 const event = { type: "response.output_item.done", sequence_number: 1, output_index: 0, item }
56 const body = method === "generateText"
57 ? JSON.stringify(makeDefaultResponse({ output: [item] }))
58 : `data: ${JSON.stringify(event)}\n\n`
59 const client = makeRawResponseClient(
60 body,
61 method === "generateText" ? "application/json" : "text/event-stream"
62 )
63 const options = { prompt: "Weather in London", toolkit: Toolkit.make(OpenAiTool.WebSearch({})) }
64 const parts = yield* Effect.gen(function*() {
65 if (method === "generateText") {
66 const response = yield* LanguageModel.generateText(options)
67 return response.content
68 }
69 return yield* LanguageModel.streamText(options).pipe(Stream.runCollect)
70 }).pipe(
71 Effect.provide(OpenAiLanguageModel.model("gpt-5.6")),
72 Effect.provide(OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") })),
73 Effect.provideService(HttpClient.HttpClient, client)
74 )
75
76 const calls = parts.filter((part) => part.type === "tool-call")
77 deepStrictEqual(calls.map((part) => part.params), [{ action }])
78
79 const results = parts.filter((part) => part.type === "tool-result")
80 deepStrictEqual(results.map((part) => part.result), [{ action, status: "completed" }])
81 })
82 )
83 })
84
85 describe("web search without an action", () => {
86 it.effect.each(
87 [
88 { method: "streamText", status: "incomplete", isFailure: true },
89 { method: "generateText", status: "incomplete", isFailure: true },
90 { method: "generateText", status: "completed", isFailure: false }
91 ] as const
92 )("preserves omitted actions for $status with $method", ({ method, status, isFailure }) =>
93 Effect.gen(function*() {
94 const item: Generated.WebSearchToolCall = { type: "web_search_call", id: "ws_123", status }
95 const event = { type: "response.output_item.done", sequence_number: 1, output_index: 0, item }
96 const body = method === "generateText"
97 ? JSON.stringify(makeDefaultResponse({ output: [item] }))
98 : `data: ${JSON.stringify(event)}\n\n`
99 const client = makeRawResponseClient(body, method === "generateText" ? "application/json" : "text/event-stream")
100 const options = { prompt: "Search the web", toolkit: Toolkit.make(OpenAiTool.WebSearch({})) }
101 const parts = yield* Effect.gen(function*() {
102 if (method === "generateText") return (yield* LanguageModel.generateText(options)).content
103 return yield* LanguageModel.streamText(options).pipe(Stream.runCollect)
104 }).pipe(
105 Effect.provide(OpenAiLanguageModel.model("gpt-6-luna")),
106 Effect.provide(OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") })),
107 Effect.provideService(HttpClient.HttpClient, client)
108 )
109
110 deepStrictEqual(
111 parts.filter((part) => part.type === "tool-call" || part.type === "tool-result")
112 .map((part) => ({
113 type: part.type,
114 id: part.id,
115 providerExecuted: part.providerExecuted,
116 ...(part.type === "tool-call"
117 ? { params: part.params }
118 : { result: part.result, isFailure: part.isFailure })
119 })),
120 [
121 { type: "tool-call", id: "ws_123", providerExecuted: true, params: {} },
122 { type: "tool-result", id: "ws_123", providerExecuted: true, result: { status }, isFailure }
123 ]
124 )
125 }))
126
127 it.effect("rejects null and malformed actions when present", () =>
128 Effect.forEach([null, { type: "find_in_page", url: "https://example.com" }], (action) => {
129 const event = {
130 type: "response.output_item.done",
131 sequence_number: 1,
132 output_index: 0,
133 item: { type: "web_search_call", id: "ws_123", status: "completed", action }
134 }
135 const client = makeRawResponseClient(`data: ${JSON.stringify(event)}\n\n`, "text/event-stream")
136 return LanguageModel.streamText({ prompt: "Search", toolkit: Toolkit.make(OpenAiTool.WebSearch({})) }).pipe(
137 Stream.runCollect,
138 Effect.flip,
139 Effect.tap((error) => {
140 assert(AiError.isAiError(error))
141 strictEqual(error.reason._tag, "InvalidOutputError")
142 return Effect.void
143 }),
144 Effect.provide(OpenAiLanguageModel.model("gpt-6-luna")),
145 Effect.provide(OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") })),
146 Effect.provideService(HttpClient.HttpClient, client)
147 )
148 }))
149 })
150
151 describe("generateText", () => {
152 describe("message preparation", () => {
153 it.effect("forwards prompt cache configuration and text breakpoints", () =>
154 Effect.gen(function*() {
155 const breakpoint = { mode: "explicit" } as const
156 yield* LanguageModel.generateText({
157 prompt: Prompt.make([
158 Prompt.systemMessage({
159 content: "Stable instructions",
160 options: { openai: { promptCacheBreakpoint: breakpoint } }
161 }),
162 Prompt.userMessage({
163 content: [Prompt.textPart({
164 text: "Stable context",
165 options: { openai: { promptCacheBreakpoint: breakpoint } }
166 })]
167 })
168 ])
169 }).pipe(
170 Effect.provide(OpenAiLanguageModel.model("gpt-5.6", {
171 prompt_cache_key: "assistant:v1",
172 prompt_cache_options: { mode: "explicit", ttl: "30m", comparison_response_id: "resp_baseline" }
173 }))
174 )
175
176 const requests = yield* MockHttpClient.requests
177 const body = yield* getRequestBody(requests[0])
178
179 strictEqual(body.prompt_cache_key, "assistant:v1")
180 deepStrictEqual(body.prompt_cache_options, {
181 mode: "explicit",
182 ttl: "30m",
183 comparison_response_id: "resp_baseline"
184 })
185 deepStrictEqual(body.input, [{
186 role: "developer",
187 content: [{
188 type: "input_text",
189 text: "Stable instructions",
190 prompt_cache_breakpoint: breakpoint
191 }]
192 }, {
193 role: "user",
194 content: [{
195 type: "input_text",
196 text: "Stable context",
197 prompt_cache_breakpoint: breakpoint
198 }]
199 }])
200 }).pipe(Effect.provide(makeTestLayer({ body: { model: "gpt-5.6" as any } }))))
201
202 it.effect("forwards implicit prompt cache mode without text breakpoints", () =>
203 Effect.gen(function*() {
204 yield* LanguageModel.generateText({ prompt: "Stable context" }).pipe(
205 Effect.provide(OpenAiLanguageModel.model("gpt-5.6", {
206 prompt_cache_options: { mode: "implicit" }
207 }))
208 )
209
210 const requests = yield* MockHttpClient.requests
211 const body = yield* getRequestBody(requests[0])
212
213 deepStrictEqual(body.prompt_cache_options, { mode: "implicit" })
214 deepStrictEqual(body.input, [{
215 role: "user",
216 content: [{ type: "input_text", text: "Stable context" }]
217 }])
218 }).pipe(Effect.provide(makeTestLayer({ body: { model: "gpt-5.6" as any } }))))
219
220 describe("system messages", () => {
221 it.effect("uses system role for standard models", () =>
222 Effect.gen(function*() {
223 yield* LanguageModel.generateText({
224 prompt: Prompt.make([
225 { role: "system", content: "You are a helpful assistant" },
226 { role: "user", content: "Hello" }
227 ])
228 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
229
230 const requests = yield* MockHttpClient.requests
231 const body = yield* getRequestBody(requests[0])
232
233 const systemMessage = body.input.find((m: any) => m.role === "system")
234 assert.isDefined(systemMessage)
235 deepStrictEqual(systemMessage.content, [{
236 type: "input_text",
237 text: "You are a helpful assistant"
238 }])
239 }).pipe(Effect.provide(makeTestLayer())))
240
241 it.effect("uses developer role for reasoning models", () =>
242 Effect.gen(function*() {
243 yield* LanguageModel.generateText({
244 prompt: Prompt.make([
245 { role: "system", content: "You are a helpful assistant" },
246 { role: "user", content: "Hello" }
247 ])
248 }).pipe(Effect.provide(OpenAiLanguageModel.model("o1")))
249
250 const requests = yield* MockHttpClient.requests
251 const body = yield* getRequestBody(requests[0])
252
253 const devMessage = body.input.find((m: any) => m.role === "developer")
254 assert.isDefined(devMessage)
255 deepStrictEqual(devMessage.content, [{
256 type: "input_text",
257 text: "You are a helpful assistant"
258 }])
259 }).pipe(Effect.provide(makeTestLayer({ body: { model: "o1" } }))))
260
261 it.effect("uses developer role for gpt-5 models", () =>
262 Effect.gen(function*() {
263 yield* LanguageModel.generateText({
264 prompt: Prompt.make([
265 { role: "system", content: "You are a helpful assistant" },
266 { role: "user", content: "Hello" }
267 ])
268 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-5")))
269
270 const requests = yield* MockHttpClient.requests
271 const body = yield* getRequestBody(requests[0])
272
273 const devMessage = body.input.find((m: any) => m.role === "developer")
274 assert.isDefined(devMessage)
275 }).pipe(Effect.provide(makeTestLayer({ body: { model: "gpt-5" } }))))
276
277 it.effect("uses developer role for o3 models", () =>
278 Effect.gen(function*() {
279 yield* LanguageModel.generateText({
280 prompt: Prompt.make([
281 { role: "system", content: "You are a helpful assistant" },
282 { role: "user", content: "Hello" }
283 ])
284 }).pipe(Effect.provide(OpenAiLanguageModel.model("o3-mini")))
285
286 const requests = yield* MockHttpClient.requests
287 const body = yield* getRequestBody(requests[0])
288
289 const devMessage = body.input.find((m: any) => m.role === "developer")
290 assert.isDefined(devMessage)
291 }).pipe(Effect.provide(makeTestLayer({ body: { model: "o3-mini" } }))))
292 })
293
294 describe("user messages", () => {
295 it.effect("converts text parts to input_text", () =>
296 Effect.gen(function*() {
297 yield* LanguageModel.generateText({
298 prompt: "Hello world"
299 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
300
301 const requests = yield* MockHttpClient.requests
302 const body = yield* getRequestBody(requests[0])
303
304 const userMessage = body.input.find((m: any) => m.role === "user")
305 assert.isDefined(userMessage)
306 deepStrictEqual(userMessage.content, [{ type: "input_text", text: "Hello world" }])
307 }).pipe(Effect.provide(makeTestLayer())))
308
309 it.effect("handles image URLs", () =>
310 Effect.gen(function*() {
311 yield* LanguageModel.generateText({
312 prompt: Prompt.make([{
313 role: "user",
314 content: [
315 Prompt.filePart({
316 mediaType: "image/png",
317 data: new URL("https://example.com/image.png")
318 })
319 ]
320 }])
321 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
322
323 const requests = yield* MockHttpClient.requests
324 const body = yield* getRequestBody(requests[0])
325
326 const userMessage = body.input.find((m: any) => m.role === "user")
327 deepStrictEqual(userMessage.content, [{
328 type: "input_image",
329 image_url: "https://example.com/image.png",
330 detail: "auto"
331 }])
332 }).pipe(Effect.provide(makeTestLayer())))
333
334 it.effect("handles image strings", () =>
335 Effect.gen(function*() {
336 const base64 = "iVBORw0KGgo="
337 const dataUrl = `data:image/png;base64,${base64}`
338 const upperCaseDataUrl = `DATA:image/png;base64,${base64}`
339 const url = "https://example.com/image.png"
340
341 yield* LanguageModel.generateText({
342 prompt: Prompt.make([Prompt.userMessage({
343 content: [base64, dataUrl, upperCaseDataUrl, url].map((data) =>
344 Prompt.filePart({ mediaType: "image/png", data })
345 )
346 })])
347 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
348
349 const requests = yield* MockHttpClient.requests
350 const body = yield* getRequestBody(requests[0])
351
352 assert.deepStrictEqual(body.input, [{
353 role: "user",
354 content: [
355 { type: "input_image", image_url: `data:image/png;base64,${base64}`, detail: "auto" },
356 { type: "input_image", image_url: dataUrl, detail: "auto" },
357 { type: "input_image", image_url: upperCaseDataUrl, detail: "auto" },
358 { type: "input_image", image_url: url, detail: "auto" }
359 ]
360 }])
361 }).pipe(Effect.provide(makeTestLayer())))
362
363 it.effect("handles image with custom detail level", () =>
364 Effect.gen(function*() {
365 yield* LanguageModel.generateText({
366 prompt: Prompt.make([{
367 role: "user",
368 content: [
369 Prompt.filePart({
370 mediaType: "image/png",
371 data: new URL("https://example.com/image.png"),
372 options: { openai: { imageDetail: "high" } }
373 })
374 ]
375 }])
376 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
377
378 const requests = yield* MockHttpClient.requests
379 const body = yield* getRequestBody(requests[0])
380
381 const userMessage = body.input.find((m: any) => m.role === "user")
382 strictEqual(userMessage.content[0].detail, "high")
383 }).pipe(Effect.provide(makeTestLayer())))
384
385 it.effect("handles image file IDs with configured prefixes", () =>
386 Effect.gen(function*() {
387 yield* LanguageModel.generateText({
388 prompt: Prompt.make([{
389 role: "user",
390 content: [
391 Prompt.filePart({
392 mediaType: "image/png",
393 data: "file-abc123"
394 })
395 ]
396 }])
397 }).pipe(
398 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", {
399 fileIdPrefixes: ["file-"]
400 }))
401 )
402
403 const requests = yield* MockHttpClient.requests
404 const body = yield* getRequestBody(requests[0])
405
406 const userMessage = body.input.find((m: any) => m.role === "user")
407 deepStrictEqual(userMessage.content, [{
408 type: "input_image",
409 file_id: "file-abc123",
410 detail: "auto"
411 }])
412 }).pipe(Effect.provide(makeTestLayer())))
413
414 it.effect("handles image base64 data", () =>
415 Effect.gen(function*() {
416 const imageData = new Uint8Array([137, 80, 78, 71]) // PNG magic bytes
417
418 yield* LanguageModel.generateText({
419 prompt: Prompt.make([{
420 role: "user",
421 content: [
422 Prompt.filePart({
423 mediaType: "image/png",
424 data: imageData
425 })
426 ]
427 }])
428 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
429
430 const requests = yield* MockHttpClient.requests
431 const body = yield* getRequestBody(requests[0])
432
433 const userMessage = body.input.find((m: any) => m.role === "user")
434 assert.isTrue(userMessage.content[0].image_url.startsWith("data:image/png;base64,"))
435 }).pipe(Effect.provide(makeTestLayer())))
436
437 it.effect("handles PDF URLs", () =>
438 Effect.gen(function*() {
439 yield* LanguageModel.generateText({
440 prompt: Prompt.make([{
441 role: "user",
442 content: [
443 Prompt.filePart({
444 mediaType: "application/pdf",
445 data: new URL("https://example.com/document.pdf")
446 })
447 ]
448 }])
449 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
450
451 const requests = yield* MockHttpClient.requests
452 const body = yield* getRequestBody(requests[0])
453
454 const userMessage = body.input.find((m: any) => m.role === "user")
455 deepStrictEqual(userMessage.content, [{
456 type: "input_file",
457 file_url: "https://example.com/document.pdf"
458 }])
459 }).pipe(Effect.provide(makeTestLayer())))
460
461 it.effect("handles PDF base64 data with filename", () =>
462 Effect.gen(function*() {
463 const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46]) // %PDF
464
465 yield* LanguageModel.generateText({
466 prompt: Prompt.make([{
467 role: "user",
468 content: [
469 Prompt.filePart({
470 mediaType: "application/pdf",
471 data: pdfData,
472 fileName: "document.pdf"
473 })
474 ]
475 }])
476 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
477
478 const requests = yield* MockHttpClient.requests
479 const body = yield* getRequestBody(requests[0])
480
481 const userMessage = body.input.find((m: any) => m.role === "user")
482 strictEqual(userMessage.content[0].type, "input_file")
483 strictEqual(userMessage.content[0].filename, "document.pdf")
484 assert.isTrue(userMessage.content[0].file_data.startsWith("data:application/pdf;base64,"))
485 }).pipe(Effect.provide(makeTestLayer())))
486 })
487
488 describe("assistant messages", () => {
489 it.effect("converts text parts to message output", () =>
490 Effect.gen(function*() {
491 yield* LanguageModel.generateText({
492 prompt: Prompt.make([
493 { role: "user", content: "Hello" },
494 {
495 role: "assistant",
496 content: [Prompt.textPart({ text: "Hi there!" })]
497 },
498 { role: "user", content: "How are you?" }
499 ])
500 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
501
502 const requests = yield* MockHttpClient.requests
503 const body = yield* getRequestBody(requests[0])
504
505 const assistantMessage = body.input.find((m: any) => m.type === "message" && m.role === "assistant")
506 assert.isDefined(assistantMessage)
507 strictEqual(assistantMessage.content[0].type, "output_text")
508 strictEqual(assistantMessage.content[0].text, "Hi there!")
509 }).pipe(Effect.provide(makeTestLayer())))
510
511 it.effect("converts reasoning parts", () =>
512 Effect.gen(function*() {
513 yield* LanguageModel.generateText({
514 prompt: Prompt.make([
515 { role: "user", content: "Think step by step" },
516 {
517 role: "assistant",
518 content: [
519 Prompt.reasoningPart({
520 text: "Let me think...",
521 options: { openai: { itemId: "reasoning_123" } }
522 })
523 ]
524 },
525 { role: "user", content: "Continue" }
526 ])
527 }).pipe(Effect.provide(OpenAiLanguageModel.model("o1")))
528
529 const requests = yield* MockHttpClient.requests
530 const body = yield* getRequestBody(requests[0])
531
532 const reasoningItem = body.input.find((m: any) => m.type === "reasoning")
533 assert.isDefined(reasoningItem)
534 strictEqual(reasoningItem.id, "reasoning_123")
535 }).pipe(Effect.provide(makeTestLayer({ body: { model: "o1" } }))))
536
537 it.effect("replays encrypted reasoning from response parts", () =>
538 Effect.gen(function*() {
539 const history = Prompt.fromResponseParts([
540 AiResponse.makePart("reasoning-start", {
541 id: "reasoning_123:0",
542 metadata: { openai: { itemId: "reasoning_123" } }
543 }),
544 AiResponse.makePart("reasoning-delta", {
545 id: "reasoning_123:0",
546 delta: "Let me think..."
547 }),
548 AiResponse.makePart("reasoning-end", {
549 id: "reasoning_123:0",
550 metadata: {
551 openai: {
552 itemId: "reasoning_123",
553 encryptedContent: "encrypted-reasoning"
554 }
555 }
556 })
557 ])
558
559 yield* LanguageModel.generateText({
560 prompt: Prompt.concat(history, Prompt.make("Continue"))
561 }).pipe(Effect.provide(OpenAiLanguageModel.model("o1")))
562
563 const requests = yield* MockHttpClient.requests
564 const body = yield* getRequestBody(requests[0])
565 const reasoningItem = body.input.find((item: any) => item.type === "reasoning")
566
567 assert.isDefined(reasoningItem)
568 deepStrictEqual(reasoningItem, {
569 type: "reasoning",
570 id: "reasoning_123",
571 summary: [{ type: "summary_text", text: "Let me think..." }],
572 encrypted_content: "encrypted-reasoning"
573 })
574 }).pipe(Effect.provide(makeTestLayer({ body: { model: "o1" } }))))
575
576 it.effect("serializes stored assistant history according to item reference config", () =>
577 Effect.gen(function*() {
578 yield* LanguageModel.generateText({
579 prompt: storedHistoryPrompt,
580 toolkit: TestToolkit,
581 disableToolCallResolution: true
582 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", { store: true })))
583
584 yield* LanguageModel.generateText({
585 prompt: storedHistoryPrompt,
586 toolkit: TestToolkit,
587 disableToolCallResolution: true
588 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", {
589 store: true,
590 useItemReferences: false
591 })))
592
593 const requests = yield* MockHttpClient.requests
594 const referencedBody = yield* getRequestBody(requests[0])
595 const inlineBody = yield* getRequestBody(requests[1])
596
597 assert.deepStrictEqual(referencedBody.input, [
598 { role: "user", content: [{ type: "input_text", text: "Question" }] },
599 { type: "item_reference", id: "msg_1" },
600 { type: "item_reference", id: "rs_1" },
601 { type: "item_reference", id: "fc_1" },
602 { type: "function_call_output", call_id: "call_1", output: "{\"output\":\"result\"}" },
603 { role: "user", content: [{ type: "input_text", text: "Continue" }] }
604 ])
605
606 assert.deepStrictEqual(inlineBody.input, [
607 { role: "user", content: [{ type: "input_text", text: "Question" }] },
608 {
609 id: "msg_1",
610 type: "message",
611 role: "assistant",
612 status: "completed",
613 content: [{
614 type: "output_text",
615 text: "Answer",
616 annotations: [],
617 logprobs: []
618 }]
619 },
620 {
621 type: "reasoning",
622 id: "rs_1",
623 summary: [{ type: "summary_text", text: "Thinking" }],
624 encrypted_content: "encrypted-reasoning"
625 },
626 {
627 type: "function_call",
628 name: "TestTool",
629 call_id: "call_1",
630 arguments: "{\"input\":\"value\"}",
631 id: "fc_1"
632 },
633 { type: "function_call_output", call_id: "call_1", output: "{\"output\":\"result\"}" },
634 { role: "user", content: [{ type: "input_text", text: "Continue" }] }
635 ])
636 strictEqual(inlineBody.useItemReferences, undefined)
637 }).pipe(Effect.provide(makeTestLayer())))
638
639 it.effect("replays matched provider-executed history inline", () =>
640 Effect.gen(function*() {
641 yield* LanguageModel.generateText({
642 prompt: providerExecutedHistoryPrompt,
643 toolkit: Toolkit.make(OpenAiTool.WebSearch({})),
644 disableToolCallResolution: true
645 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", {
646 store: true,
647 useItemReferences: false
648 })))
649
650 const requests = yield* MockHttpClient.requests
651 const body = yield* getRequestBody(requests[0])
652
653 assert.deepStrictEqual(body.input, [
654 { role: "user", content: [{ type: "input_text", text: "Search" }] },
655 {
656 type: "web_search_call",
657 id: "ws_1",
658 status: "completed",
659 action: { type: "search", query: "Effect TypeScript" }
660 },
661 { role: "user", content: [{ type: "input_text", text: "Continue" }] }
662 ])
663 }).pipe(Effect.provide(makeTestLayer())))
664
665 it.effect("replays web search history with an omitted action", () =>
666 Effect.gen(function*() {
667 yield* LanguageModel.generateText({
668 prompt: Prompt.make([
669 { role: "user", content: "Search" },
670 {
671 role: "assistant",
672 content: [
673 Prompt.toolCallPart({ id: "ws_1", name: "OpenAiWebSearch", params: {}, providerExecuted: true }),
674 Prompt.toolResultPart({
675 id: "ws_1",
676 name: "OpenAiWebSearch",
677 providerExecuted: true,
678 isFailure: true,
679 result: { status: "incomplete" }
680 })
681 ]
682 },
683 { role: "user", content: "Continue" }
684 ]),
685 toolkit: Toolkit.make(OpenAiTool.WebSearch({})),
686 disableToolCallResolution: true
687 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-6-luna", {
688 store: true,
689 useItemReferences: false
690 })))
691
692 const body = yield* getRequestBody((yield* MockHttpClient.requests)[0])
693 deepStrictEqual(body.input?.[1], { type: "web_search_call", id: "ws_1", status: "incomplete" })
694 }).pipe(Effect.provide(makeTestLayer())))
695
696 it.effect("fails locally when provider-executed history cannot be replayed inline", () =>
697 Effect.gen(function*() {
698 const cases = [
699 providerExecutedCall,
700 providerExecutedResult
701 ] as const
702 yield* Effect.forEach(cases, (part) =>
703 LanguageModel.generateText({
704 prompt: Prompt.make([
705 { role: "user", content: "Search" },
706 {
707 role: "assistant",
708 content: [part]
709 },
710 { role: "user", content: "Continue" }
711 ]),
712 toolkit: Toolkit.make(OpenAiTool.WebSearch({})),
713 disableToolCallResolution: true
714 }).pipe(
715 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", {
716 store: true,
717 useItemReferences: false
718 })),
719 Effect.flip,
720 Effect.tap((error) =>
721 Effect.sync(() => {
722 strictEqual(error.reason._tag, "InvalidRequestError")
723 if (error.reason._tag === "InvalidRequestError") {
724 assert.include(error.reason.description, "OpenAiWebSearch")
725 assert.include(error.reason.description, "ws_1")
726 }
727 })
728 )
729 ))
730
731 strictEqual((yield* MockHttpClient.requests).length, 0)
732 }).pipe(Effect.provide(makeTestLayer())))
733
734 it.effect("converts tool call parts to function_call", () =>
735 Effect.gen(function*() {
736 yield* LanguageModel.generateText({
737 prompt: Prompt.make([
738 { role: "user", content: "Use the tool" },
739 {
740 role: "assistant",
741 content: [
742 Prompt.toolCallPart({
743 id: "call_abc",
744 name: "TestTool",
745 params: { input: "test" },
746 providerExecuted: false
747 })
748 ]
749 },
750 {
751 role: "tool",
752 content: [
753 Prompt.toolResultPart({
754 id: "call_abc",
755 name: "TestTool",
756 isFailure: false,
757 result: { output: "result" },
758 providerExecuted: false
759 })
760 ]
761 }
762 ]),
763 toolkit: TestToolkit
764 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
765
766 const requests = yield* MockHttpClient.requests
767 const body = yield* getRequestBody(requests[0])
768
769 const functionCall = body.input.find((m: any) => m.type === "function_call")
770 assert.isDefined(functionCall)
771 strictEqual(functionCall.name, "TestTool")
772 strictEqual(functionCall.call_id, "call_abc")
773 }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))
774 })
775
776 describe("tool messages", () => {
777 it.effect("converts tool results to function_call_output", () =>
778 Effect.gen(function*() {
779 yield* LanguageModel.generateText({
780 prompt: Prompt.make([
781 { role: "user", content: "Use the tool" },
782 {
783 role: "assistant",
784 content: [
785 Prompt.toolCallPart({
786 id: "call_abc",
787 name: "TestTool",
788 params: { input: "test" },
789 providerExecuted: false
790 })
791 ]
792 },
793 {
794 role: "tool",
795 content: [
796 Prompt.toolResultPart({
797 id: "call_abc",
798 name: "TestTool",
799 isFailure: false,
800 result: { output: "result" },
801 providerExecuted: false
802 })
803 ]
804 }
805 ]),
806 toolkit: TestToolkit
807 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
808
809 const requests = yield* MockHttpClient.requests
810 const body = yield* getRequestBody(requests[0])
811
812 const toolOutput = body.input.find((m: any) => m.type === "function_call_output")
813 assert.isDefined(toolOutput)
814 strictEqual(toolOutput.call_id, "call_abc")
815 strictEqual(toolOutput.output, JSON.stringify({ output: "result" }))
816 }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))
817
818 it.effect("preserves string tool results", () =>
819 Effect.gen(function*() {
820 yield* LanguageModel.generateText({
821 prompt: Prompt.make([
822 { role: "user", content: "Use the tool" },
823 {
824 role: "assistant",
825 content: [
826 Prompt.toolCallPart({
827 id: "call_text",
828 name: "TestTool",
829 params: { input: "test" },
830 providerExecuted: false
831 })
832 ]
833 },
834 {
835 role: "tool",
836 content: [
837 Prompt.toolResultPart({
838 id: "call_text",
839 name: "TestTool",
840 isFailure: false,
841 result: "PLAIN_TEXT_SENTINEL\n",
842 providerExecuted: false
843 })
844 ]
845 }
846 ]),
847 toolkit: TestToolkit
848 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
849
850 const requests = yield* MockHttpClient.requests
851 const body = yield* getRequestBody(requests[0])
852 const toolOutput = body.input.find((item: any) => item.type === "function_call_output")
853
854 assert.isDefined(toolOutput)
855 strictEqual(toolOutput.output, "PLAIN_TEXT_SENTINEL\n")
856 }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))
857
858 it.effect("emits only the specialized output for apply_patch results", () =>
859 Effect.gen(function*() {
860 const toolkit = Toolkit.make(OpenAiTool.ApplyPatch({}))
861 yield* LanguageModel.generateText({
862 prompt: Prompt.make([
863 { role: "user", content: "Apply a patch" },
864 {
865 role: "assistant",
866 content: [Prompt.toolCallPart({
867 id: "call_apply_patch",
868 name: "OpenAiApplyPatch",
869 params: {
870 call_id: "call_apply_patch",
871 operation: { type: "delete_file", path: "old.ts" }
872 },
873 providerExecuted: false
874 })]
875 },
876 {
877 role: "tool",
878 content: [Prompt.toolResultPart({
879 id: "call_apply_patch",
880 name: "OpenAiApplyPatch",
881 isFailure: false,
882 result: { status: "completed", output: "deleted" },
883 providerExecuted: false
884 })]
885 }
886 ]),
887 toolkit,
888 disableToolCallResolution: true
889 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
890
891 const requests = yield* MockHttpClient.requests
892 const body = yield* getRequestBody(requests[0])
893 const outputs = body.input.filter((item: any) =>
894 item.call_id === "call_apply_patch" && item.type.endsWith("_output")
895 )
896
897 deepStrictEqual(outputs.map((item: any) => item.type), ["apply_patch_call_output"])
898 }).pipe(Effect.provide(makeTestLayer())))
899
900 it.effect("emits only the specialized output for shell results", () =>
901 Effect.gen(function*() {
902 const toolkit = Toolkit.make(OpenAiTool.Shell({}))
903 yield* LanguageModel.generateText({
904 prompt: Prompt.make([
905 { role: "user", content: "Run a shell command" },
906 {
907 role: "assistant",
908 content: [Prompt.toolCallPart({
909 id: "call_shell",
910 name: "OpenAiShell",
911 params: {
912 action: {
913 commands: ["echo hello"],
914 timeout_ms: null,
915 max_output_length: null
916 }
917 },
918 providerExecuted: false
919 })]
920 },
921 {
922 role: "tool",
923 content: [Prompt.toolResultPart({
924 id: "call_shell",
925 name: "OpenAiShell",
926 isFailure: false,
927 result: {
928 output: [{
929 stdout: "hello\n",
930 stderr: "",
931 outcome: { type: "exit", exit_code: 0 }
932 }]
933 },
934 providerExecuted: false
935 })]
936 }
937 ]),
938 toolkit,
939 disableToolCallResolution: true
940 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
941
942 const requests = yield* MockHttpClient.requests
943 const body = yield* getRequestBody(requests[0])
944 const outputs = body.input.filter((item: any) =>
945 item.call_id === "call_shell" && item.type.endsWith("_output")
946 )
947
948 deepStrictEqual(outputs.map((item: any) => item.type), ["shell_call_output"])
949 }).pipe(Effect.provide(makeTestLayer())))
950
951 it.effect("emits only the specialized output for local_shell results", () =>
952 Effect.gen(function*() {
953 const toolkit = Toolkit.make(OpenAiTool.LocalShell({}))
954 yield* LanguageModel.generateText({
955 prompt: Prompt.make([
956 { role: "user", content: "Run a local shell command" },
957 {
958 role: "assistant",
959 content: [Prompt.toolCallPart({
960 id: "call_local_shell",
961 name: "OpenAiLocalShell",
962 params: {
963 action: {
964 type: "exec",
965 command: ["echo", "hello"],
966 env: {}
967 }
968 },
969 providerExecuted: false
970 })]
971 },
972 {
973 role: "tool",
974 content: [Prompt.toolResultPart({
975 id: "call_local_shell",
976 name: "OpenAiLocalShell",
977 isFailure: false,
978 result: { output: "hello\n" },
979 providerExecuted: false
980 })]
981 }
982 ]),
983 toolkit,
984 disableToolCallResolution: true
985 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
986
987 const requests = yield* MockHttpClient.requests
988 const body = yield* getRequestBody(requests[0])
989 const outputs = body.input.filter((item: any) =>
990 item.call_id === "call_local_shell" && item.type.endsWith("_output")
991 )
992
993 deepStrictEqual(outputs.map((item: any) => item.type), ["local_shell_call_output"])
994 }).pipe(Effect.provide(makeTestLayer())))
995 })
996 })
997
998 describe("tool preparation", () => {
999 it.effect("converts user-defined tools to function type", () =>
1000 Effect.gen(function*() {
1001 yield* LanguageModel.generateText({
1002 prompt: "Use the tool",
1003 toolkit: TestToolkit
1004 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1005
1006 const requests = yield* MockHttpClient.requests
1007 const body = yield* getRequestBody(requests[0])
1008
1009 const tool = body.tools?.find((t: any) => t.type === "function")
1010 assert.isDefined(tool)
1011 strictEqual(tool.name, "TestTool")
1012 strictEqual(tool.description, "A test tool")
1013 strictEqual(tool.strict, true)
1014 }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))
1015
1016 it.effect("empty object on properties for empty parameters", () =>
1017 Effect.gen(function*() {
1018 const EmptyTool = Tool.make("EmptyParamsTool", {
1019 description: "Empty params tool",
1020 parameters: Tool.EmptyParams,
1021 success: Schema.String
1022 })
1023 const toolkit = Toolkit.make(EmptyTool)
1024 const toolkitLayer = toolkit.toLayer({
1025 EmptyParamsTool: () => Effect.succeed("ok")
1026 })
1027
1028 yield* LanguageModel.generateText({
1029 prompt: "Use the tool",
1030 toolkit
1031 }).pipe(Effect.provide([OpenAiLanguageModel.model("gpt-4o-mini"), toolkitLayer]))
1032
1033 const requests = yield* MockHttpClient.requests
1034 const body = yield* getRequestBody(requests[0])
1035
1036 const tool = body.tools?.find((t: any) => t.type === "function" && t.name === "EmptyParamsTool")
1037 assert.isDefined(tool)
1038 deepStrictEqual(tool.parameters, {
1039 type: "object",
1040 properties: {},
1041 additionalProperties: false
1042 })
1043 }).pipe(Effect.provide(makeTestLayer())))
1044
1045 it.effect("converts dynamic tools to function type", () =>
1046 Effect.gen(function*() {
1047 const inputSchema = {
1048 type: "object",
1049 properties: {
1050 query: { type: "string" },
1051 limit: { type: "number" }
1052 },
1053 required: ["query"],
1054 additionalProperties: false
1055 } as const
1056
1057 const DynamicTool = Tool.dynamic("DynamicTool", {
1058 description: "A dynamic tool",
1059 parameters: inputSchema
1060 })
1061
1062 yield* LanguageModel.generateText({
1063 prompt: "Use the dynamic tool",
1064 toolkit: Toolkit.make(DynamicTool),
1065 disableToolCallResolution: true
1066 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1067
1068 const requests = yield* MockHttpClient.requests
1069 const body = yield* getRequestBody(requests[0])
1070
1071 const tool = body.tools?.find((entry: any) => entry.type === "function" && entry.name === "DynamicTool")
1072 assert.isDefined(tool)
1073 strictEqual(tool.description, "A dynamic tool")
1074 deepStrictEqual(tool.parameters, inputSchema)
1075 }).pipe(Effect.provide(makeTestLayer())))
1076
1077 it.effect("empty object on properties for empty parameters", () =>
1078 Effect.gen(function*() {
1079 const EmptyTool = Tool.make("EmptyParamsTool", {
1080 description: "Empty params tool",
1081 parameters: Tool.EmptyParams,
1082 success: Schema.String
1083 })
1084 const toolkit = Toolkit.make(EmptyTool)
1085 const toolkitLayer = toolkit.toLayer({
1086 EmptyParamsTool: () => Effect.succeed("ok")
1087 })
1088
1089 yield* LanguageModel.generateText({
1090 prompt: "Use the tool",
1091 toolkit
1092 }).pipe(Effect.provide([OpenAiLanguageModel.model("gpt-4o-mini"), toolkitLayer]))
1093
1094 const requests = yield* MockHttpClient.requests
1095 const body = yield* getRequestBody(requests[0])
1096
1097 const tool = body.tools?.find((t: any) => t.type === "function" && t.name === "EmptyParamsTool")
1098 assert.isDefined(tool)
1099 deepStrictEqual(tool.parameters, {
1100 type: "object",
1101 properties: {},
1102 additionalProperties: false
1103 })
1104 }).pipe(Effect.provide(makeTestLayer())))
1105
1106 it.effect("handles tool choice auto", () =>
1107 Effect.gen(function*() {
1108 yield* LanguageModel.generateText({
1109 prompt: "Use the tool",
1110 toolkit: TestToolkit,
1111 toolChoice: "auto"
1112 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1113
1114 const requests = yield* MockHttpClient.requests
1115 const body = yield* getRequestBody(requests[0])
1116
1117 strictEqual(body.tool_choice, "auto")
1118 }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))
1119
1120 it.effect("handles tool choice none", () =>
1121 Effect.gen(function*() {
1122 yield* LanguageModel.generateText({
1123 prompt: "Use the tool",
1124 toolkit: TestToolkit,
1125 toolChoice: "none"
1126 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1127
1128 const requests = yield* MockHttpClient.requests
1129 const body = yield* getRequestBody(requests[0])
1130
1131 strictEqual(body.tool_choice, "none")
1132 }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))
1133
1134 it.effect("handles tool choice required", () =>
1135 Effect.gen(function*() {
1136 yield* LanguageModel.generateText({
1137 prompt: "Use the tool",
1138 toolkit: TestToolkit,
1139 toolChoice: "required"
1140 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1141
1142 const requests = yield* MockHttpClient.requests
1143 const body = yield* getRequestBody(requests[0])
1144
1145 strictEqual(body.tool_choice, "required")
1146 }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))
1147
1148 it.effect("handles specific tool choice", () =>
1149 Effect.gen(function*() {
1150 yield* LanguageModel.generateText({
1151 prompt: "Use the tool",
1152 toolkit: TestToolkit,
1153 toolChoice: { tool: "TestTool" }
1154 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1155
1156 const requests = yield* MockHttpClient.requests
1157 const body = yield* getRequestBody(requests[0])
1158
1159 deepStrictEqual(body.tool_choice, { type: "function", name: "TestTool" })
1160 }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer])))
1161
1162 it.effect("adds code_interpreter tool", () =>
1163 Effect.gen(function*() {
1164 const toolkit = Toolkit.make(OpenAiTool.CodeInterpreter({ container: { type: "auto" } }))
1165
1166 yield* LanguageModel.generateText({
1167 prompt: "Run some code",
1168 toolkit
1169 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1170
1171 const requests = yield* MockHttpClient.requests
1172 const body = yield* getRequestBody(requests[0])
1173
1174 const tool = body.tools?.find((t: any) => t.type === "code_interpreter")
1175 assert.isDefined(tool)
1176 }).pipe(Effect.provide(makeTestLayer())))
1177
1178 it.effect("adds web_search tool", () =>
1179 Effect.gen(function*() {
1180 const toolkit = Toolkit.make(OpenAiTool.WebSearch({}))
1181
1182 yield* LanguageModel.generateText({
1183 prompt: "Search the web",
1184 toolkit
1185 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1186
1187 const requests = yield* MockHttpClient.requests
1188 const body = yield* getRequestBody(requests[0])
1189
1190 const tool = body.tools?.find((t: any) => t.type === "web_search")
1191 assert.isDefined(tool)
1192 }).pipe(Effect.provide(makeTestLayer())))
1193
1194 it.effect("adds file_search tool with vector store IDs", () =>
1195 Effect.gen(function*() {
1196 const toolkit = Toolkit.make(OpenAiTool.FileSearch({
1197 vector_store_ids: ["vs_123"]
1198 }))
1199
1200 yield* LanguageModel.generateText({
1201 prompt: "Search files",
1202 toolkit
1203 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1204
1205 const requests = yield* MockHttpClient.requests
1206 const body = yield* getRequestBody(requests[0])
1207
1208 const tool = body.tools?.find((t: any) => t.type === "file_search")
1209 assert.isDefined(tool)
1210 deepStrictEqual(tool.vector_store_ids, ["vs_123"])
1211 }).pipe(Effect.provide(makeTestLayer())))
1212 })
1213
1214 describe("response format", () => {
1215 it.effect("uses text format by default", () =>
1216 Effect.gen(function*() {
1217 yield* LanguageModel.generateText({
1218 prompt: "Hello"
1219 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1220
1221 const requests = yield* MockHttpClient.requests
1222 const body = yield* getRequestBody(requests[0])
1223
1224 strictEqual(body.text?.format?.type, "text")
1225 }).pipe(Effect.provide(makeTestLayer())))
1226
1227 it.effect("uses json_schema format for structured output", () =>
1228 Effect.gen(function*() {
1229 yield* LanguageModel.generateObject({
1230 prompt: "Give me a person",
1231 schema: Schema.Struct({
1232 name: Schema.String,
1233 age: Schema.Number
1234 })
1235 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1236
1237 const requests = yield* MockHttpClient.requests
1238 const body = yield* getRequestBody(requests[0])
1239
1240 strictEqual(body.text?.format?.type, "json_schema")
1241 strictEqual(body.text?.format?.strict, true)
1242 }).pipe(Effect.provide(makeTestLayer({
1243 body: {
1244 output: [makeTextOutput(JSON.stringify({ name: "John", age: 30 }))]
1245 }
1246 }))))
1247 })
1248
1249 describe("response handling", () => {
1250 it.effect("preserves prompt cache diagnostics alongside the service tier", () =>
1251 Effect.gen(function*() {
1252 const diagnostics = {
1253 type: "cache_miss",
1254 reason: "tools_changed",
1255 cache_missed_tokens: 1024,
1256 comparison_reusable_tokens: 2048
1257 } as const
1258 const body = JSON.stringify({
1259 ...makeDefaultResponse({ service_tier: "priority" }),
1260 prompt_cache_diagnostics: diagnostics
1261 })
1262 const result = yield* LanguageModel.generateText({ prompt: "test" }).pipe(
1263 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1264 Effect.provide(OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") })),
1265 Effect.provideService(HttpClient.HttpClient, makeRawResponseClient(body, "application/json"))
1266 )
1267
1268 const finish = result.content.find((part) => part.type === "finish")
1269 assert.isDefined(finish)
1270 deepStrictEqual(finish.metadata.openai, {
1271 serviceTier: "priority",
1272 promptCacheDiagnostics: diagnostics
1273 })
1274 }))
1275
1276 it.effect("extracts text from output_text", () =>
1277 Effect.gen(function*() {
1278 const result = yield* LanguageModel.generateText({
1279 prompt: "Hello"
1280 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1281
1282 strictEqual(result.text, "Hello, world!")
1283 }).pipe(Effect.provide(makeTestLayer({
1284 body: { output: [makeTextOutput("Hello, world!")] }
1285 }))))
1286
1287 it.effect("extracts multiple text parts", () =>
1288 Effect.gen(function*() {
1289 const result = yield* LanguageModel.generateText({
1290 prompt: "Hello"
1291 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1292
1293 const textParts = result.content.filter((p) => p.type === "text")
1294 strictEqual(textParts.length, 2)
1295 }).pipe(Effect.provide(makeTestLayer({
1296 body: {
1297 output: [
1298 makeTextOutput("First"),
1299 makeTextOutput("Second", { id: "msg_456" })
1300 ]
1301 }
1302 }))))
1303
1304 it.effect("handles refusal content", () =>
1305 Effect.gen(function*() {
1306 const result = yield* LanguageModel.generateText({
1307 prompt: "Do something bad"
1308 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1309
1310 const textPart = result.content.find((p) => p.type === "text")
1311 strictEqual(textPart?.text, "")
1312 strictEqual(textPart?.metadata?.openai?.refusal, "I cannot do that")
1313 }).pipe(Effect.provide(makeTestLayer({
1314 body: {
1315 output: [{
1316 type: "message",
1317 id: "msg_123",
1318 role: "assistant",
1319 status: "completed",
1320 content: [{ type: "refusal", refusal: "I cannot do that" }]
1321 }]
1322 }
1323 }))))
1324
1325 it.effect("parses function call arguments", () =>
1326 Effect.gen(function*() {
1327 const result = yield* LanguageModel.generateText({
1328 prompt: "Use the tool",
1329 toolkit: TestToolkit
1330 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1331
1332 const toolCall = result.content.find((p) => p.type === "tool-call")
1333 assert.isDefined(toolCall)
1334 if (toolCall?.type === "tool-call") {
1335 strictEqual(toolCall.name, "TestTool")
1336 deepStrictEqual(toolCall.params, { input: "hello" })
1337 }
1338 }).pipe(
1339 Effect.provide([
1340 makeTestLayer({
1341 body: { output: [makeFunctionCall("TestTool", { input: "hello" })] }
1342 }),
1343 TestToolkitLayer
1344 ])
1345 ))
1346
1347 it.effect("preserves raw JSON Schema dynamic tool call params", () =>
1348 Effect.gen(function*() {
1349 const DynamicTool = Tool.dynamic("DynamicTool", {
1350 parameters: {
1351 type: "object",
1352 properties: { query: { type: "string" } },
1353 required: ["query"],
1354 additionalProperties: false
1355 } as const
1356 })
1357 const params = { query: "effect" }
1358 const result = yield* LanguageModel.generateText({
1359 prompt: "Use the dynamic tool",
1360 toolkit: Toolkit.make(DynamicTool),
1361 disableToolCallResolution: true
1362 }).pipe(
1363 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1364 Effect.provide(makeTestLayer({
1365 body: { output: [makeFunctionCall("DynamicTool", params)] }
1366 }))
1367 )
1368
1369 deepStrictEqual(result.toolCalls[0]?.params, params)
1370 }))
1371
1372 it.effect("decodes Effect Schema dynamic tool call params with the OpenAI codec", () =>
1373 Effect.gen(function*() {
1374 const DynamicTool = Tool.dynamic("DynamicTool", {
1375 parameters: Schema.Struct({
1376 env: Schema.Record(Schema.String, Schema.String)
1377 })
1378 })
1379 const result = yield* LanguageModel.generateText({
1380 prompt: "Use the dynamic tool",
1381 toolkit: Toolkit.make(DynamicTool),
1382 disableToolCallResolution: true
1383 }).pipe(
1384 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1385 Effect.provide(makeTestLayer({
1386 body: {
1387 output: [makeFunctionCall("DynamicTool", { env: [{ 0: "PATH", 1: "/usr/bin" }] })]
1388 }
1389 }))
1390 )
1391
1392 deepStrictEqual(result.toolCalls[0]?.params, { env: { PATH: "/usr/bin" } })
1393 }))
1394
1395 it.effect("routes invalid tool call params through failureMode: return without failing the effect", () =>
1396 Effect.gen(function*() {
1397 const toolkit = Toolkit.make(ReturnModeTool)
1398 const handlers = toolkit.toLayer({
1399 ReturnModeTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` })
1400 })
1401
1402 const result = yield* LanguageModel.generateText({
1403 prompt: "Use the tool",
1404 toolkit
1405 }).pipe(
1406 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1407 Effect.provide(handlers)
1408 )
1409
1410 strictEqual(result.toolResults.length, 1)
1411 const toolResult = result.toolResults[0]!
1412 strictEqual(toolResult.isFailure, true)
1413 const failure = toolResult.result as AiError.AiError
1414 strictEqual(failure._tag, "AiError")
1415 strictEqual(failure.reason._tag, "ToolParameterValidationError")
1416 }).pipe(
1417 Effect.provide(makeTestLayer({
1418 body: { output: [makeFunctionCall("ReturnModeTool", { input: 123 })] }
1419 }))
1420 ))
1421
1422 it.effect("converts transformed tool call params to the tool's standard encoded form", () =>
1423 Effect.gen(function*() {
1424 const received = yield* Ref.make<number | undefined>(undefined)
1425 const toolkit = Toolkit.make(TransformParamsTool)
1426 const handlers = toolkit.toLayer({
1427 TransformParamsTool: ({ input }) =>
1428 Ref.set(received, input).pipe(
1429 Effect.as({ output: input * 2 })
1430 )
1431 })
1432
1433 const result = yield* LanguageModel.generateText({
1434 prompt: "Use the tool",
1435 toolkit
1436 }).pipe(
1437 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1438 Effect.provide(handlers)
1439 )
1440
1441 const toolCall = result.toolCalls[0]!
1442 deepStrictEqual(toolCall.params, { input: "21" })
1443 strictEqual(yield* Ref.get(received), 21)
1444 strictEqual(result.toolResults[0]!.isFailure, false)
1445 deepStrictEqual(result.toolResults[0]!.result, { output: 42 })
1446 }).pipe(
1447 Effect.provide(makeTestLayer({
1448 body: { output: [makeFunctionCall("TransformParamsTool", { input: "21" })] }
1449 }))
1450 ))
1451
1452 it.effect("provides parameter encoding services to provider normalization", () =>
1453 Effect.gen(function*() {
1454 const used = yield* Ref.make(false)
1455 const toolkit = Toolkit.make(AsymmetricParamsTool)
1456 const handlers = toolkit.toLayer({
1457 AsymmetricParamsTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` })
1458 })
1459
1460 const result = yield* LanguageModel.generateText({
1461 prompt: "Use the tool",
1462 toolkit
1463 }).pipe(
1464 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1465 Effect.provide(handlers),
1466 Effect.provideService(ParamEncodeService, { use: Ref.set(used, true) })
1467 )
1468
1469 strictEqual(yield* Ref.get(used), true)
1470 strictEqual(result.toolResults[0]!.isFailure, false)
1471 deepStrictEqual(result.toolResults[0]!.result, { output: "processed: hello" })
1472 }).pipe(
1473 Effect.provide(makeTestLayer({
1474 body: { output: [makeFunctionCall("AsymmetricParamsTool", { input: "hello" })] }
1475 }))
1476 ))
1477
1478 it.effect("provides parameter encoding services to provider normalization when tool call resolution is disabled", () =>
1479 Effect.gen(function*() {
1480 const used = yield* Ref.make(false)
1481 const toolkit = Toolkit.make(AsymmetricParamsTool)
1482
1483 const result = yield* LanguageModel.generateText({
1484 prompt: "Use the tool",
1485 toolkit,
1486 disableToolCallResolution: true
1487 }).pipe(
1488 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1489 Effect.provideService(ParamEncodeService, { use: Ref.set(used, true) })
1490 )
1491
1492 strictEqual(yield* Ref.get(used), true)
1493 const toolCall = result.toolCalls[0]!
1494 deepStrictEqual(toolCall.params, { input: "hello" })
1495 }).pipe(
1496 Effect.provide(makeTestLayer({
1497 body: { output: [makeFunctionCall("AsymmetricParamsTool", { input: "hello" })] }
1498 }))
1499 ))
1500
1501 it.effect("uses canonical OpenAiMcp name for mcp_call", () =>
1502 Effect.gen(function*() {
1503 const result = yield* LanguageModel.generateText({
1504 prompt: "Use MCP",
1505 toolkit: McpToolkit
1506 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1507
1508 const toolCall = result.content.find((part) => part.type === "tool-call")
1509 assert.isDefined(toolCall)
1510 if (toolCall?.type === "tool-call") {
1511 strictEqual(toolCall.name, "OpenAiMcp")
1512 deepStrictEqual(toolCall.params, { packageName: "effect" })
1513 }
1514
1515 const toolResult = result.content.find((part) => part.type === "tool-result")
1516 assert.isDefined(toolResult)
1517 if (toolResult?.type === "tool-result") {
1518 strictEqual(toolResult.name, "OpenAiMcp")
1519 assertTrue(!toolResult.isFailure, "expected a successful MCP result")
1520 strictEqual(toolResult.result.name, "CheckPackage")
1521 }
1522 }).pipe(Effect.provide(makeTestLayer({
1523 body: {
1524 output: [makeMcpCall("CheckPackage", { packageName: "effect" })]
1525 }
1526 }))))
1527
1528 it.each(["gpt-4.1", "gpt-5.6"] as const)(
1529 "maps stable web search action to tool call parameters with %s",
1530 (model) =>
1531 Effect.runPromise(
1532 Effect.gen(function*() {
1533 const toolkit = Toolkit.make(OpenAiTool.WebSearch({}))
1534 const result = yield* LanguageModel.generateText({
1535 prompt: "Search the web",
1536 toolkit
1537 }).pipe(Effect.provide(OpenAiLanguageModel.model(model)))
1538
1539 const toolCall = result.content.find((part) => part.type === "tool-call")
1540 assert.isDefined(toolCall)
1541 assert.deepStrictEqual(toolCall.params, {
1542 action: { type: "search", query: "Effect TypeScript" }
1543 })
1544
1545 const toolResult = result.content.find((part) => part.type === "tool-result")
1546 assert.isDefined(toolResult)
1547 assert.deepStrictEqual(toolResult.result, {
1548 action: { type: "search", query: "Effect TypeScript" },
1549 status: "completed"
1550 })
1551 }).pipe(Effect.provide(makeTestLayer({
1552 body: {
1553 model,
1554 output: [makeWebSearchCall()]
1555 }
1556 })))
1557 )
1558 )
1559
1560 for (const { tool, status } of webSearchFailures) {
1561 it.effect(
1562 `${tool.name} preserves ${status} results`,
1563 () =>
1564 Effect.gen(function*() {
1565 const result = yield* LanguageModel.generateText({
1566 prompt: "Search the web",
1567 toolkit: Toolkit.make(tool)
1568 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1569 const toolResult = result.toolResults[0]!
1570 strictEqual(toolResult.isFailure, true)
1571 deepStrictEqual(toolResult.result, {
1572 action: { type: "search", query: "Effect TypeScript" },
1573 status
1574 })
1575 }).pipe(Effect.provide(makeTestLayer({
1576 body: { output: [makeWebSearchCall({ status })] }
1577 })))
1578 )
1579 }
1580
1581 it.effect.each(fileSearchOutcomes)(
1582 "OpenAiFileSearch reports $status results",
1583 ({ status, isFailure, results }) =>
1584 Effect.gen(function*() {
1585 const result = yield* LanguageModel.generateText({
1586 prompt: "Search the files",
1587 toolkit: Toolkit.make(OpenAiTool.FileSearch({ vector_store_ids: ["vs_123"] }))
1588 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1589 const toolResult = result.toolResults[0]!
1590 strictEqual(toolResult.isFailure, isFailure)
1591 deepStrictEqual(toolResult.result, {
1592 status,
1593 queries: ["Effect TypeScript"],
1594 results: results ?? null
1595 })
1596 }).pipe(Effect.provide(makeTestLayer({
1597 body: { output: [makeFileSearchCall({ status, ...(results === undefined ? {} : { results }) })] }
1598 })))
1599 )
1600
1601 it.effect("uses canonical OpenAiMcp name for mcp_approval_request", () =>
1602 Effect.gen(function*() {
1603 const result = yield* LanguageModel.generateText({
1604 prompt: "Use MCP",
1605 toolkit: McpToolkit
1606 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1607
1608 const toolCall = result.content.find((part) => part.type === "tool-call")
1609 assert.isDefined(toolCall)
1610 if (toolCall?.type === "tool-call") {
1611 strictEqual(toolCall.name, "OpenAiMcp")
1612 deepStrictEqual(toolCall.params, { packageName: "effect" })
1613 }
1614
1615 const approvalRequest = result.content.find((part) => part.type === "tool-approval-request")
1616 assert.isDefined(approvalRequest)
1617 if (toolCall?.type === "tool-call" && approvalRequest?.type === "tool-approval-request") {
1618 strictEqual(approvalRequest.toolCallId, toolCall.id)
1619 }
1620 }).pipe(Effect.provide(makeTestLayer({
1621 body: {
1622 output: [makeMcpApprovalRequest("CheckPackage", { packageName: "effect" })]
1623 }
1624 }))))
1625
1626 it.effect("extracts reasoning parts", () =>
1627 Effect.gen(function*() {
1628 const result = yield* LanguageModel.generateText({
1629 prompt: "Think about this"
1630 }).pipe(Effect.provide(OpenAiLanguageModel.model("o1")))
1631
1632 const reasoningParts = result.content.filter((p) => p.type === "reasoning")
1633 strictEqual(reasoningParts.length, 2)
1634 if (reasoningParts[0]?.type === "reasoning") {
1635 strictEqual(reasoningParts[0].text, "First thought")
1636 }
1637 }).pipe(Effect.provide(makeTestLayer({
1638 body: {
1639 model: "o1",
1640 output: [makeReasoningOutput(["First thought", "Second thought"])]
1641 }
1642 }))))
1643
1644 it.effect("extracts usage information", () =>
1645 Effect.gen(function*() {
1646 const result = yield* LanguageModel.generateText({
1647 prompt: "Hello"
1648 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1649
1650 const finishPart = result.content.find((p) => p.type === "finish")
1651 assert.isDefined(finishPart)
1652 if (finishPart?.type === "finish") {
1653 deepStrictEqual(finishPart.usage.inputTokens, {
1654 uncached: 7,
1655 total: 10,
1656 cacheRead: 3,
1657 cacheWrite: 4
1658 })
1659 deepStrictEqual(finishPart.usage.outputTokens, { total: 20, text: 20, reasoning: 0 })
1660 }
1661 }).pipe(Effect.provide(makeTestLayer({
1662 body: {
1663 output: [makeTextOutput("Hello")],
1664 usage: makeUsage({
1665 input_tokens_details: {
1666 cached_tokens: 3,
1667 cache_write_tokens: 4
1668 }
1669 })
1670 }
1671 }))))
1672
1673 it.effect("determines finish reason from incomplete_details", () =>
1674 Effect.gen(function*() {
1675 const result = yield* LanguageModel.generateText({
1676 prompt: "Hello"
1677 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1678
1679 const finishPart = result.content.find((p) => p.type === "finish")
1680 if (finishPart?.type === "finish") {
1681 strictEqual(finishPart.reason, "content-filter")
1682 }
1683 }).pipe(Effect.provide(makeTestLayer({
1684 body: {
1685 output: [makeTextOutput("Hello")],
1686 incomplete_details: { reason: "content_filter" }
1687 }
1688 }))))
1689
1690 it.effect("defaults finish reason to stop", () =>
1691 Effect.gen(function*() {
1692 const result = yield* LanguageModel.generateText({
1693 prompt: "Hello"
1694 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1695
1696 const finishPart = result.content.find((p) => p.type === "finish")
1697 if (finishPart?.type === "finish") {
1698 strictEqual(finishPart.reason, "stop")
1699 }
1700 }).pipe(Effect.provide(makeTestLayer({
1701 body: { output: [makeTextOutput("Hello")] }
1702 }))))
1703
1704 it.effect("sets finish reason to tool-calls when has tool calls", () =>
1705 Effect.gen(function*() {
1706 const result = yield* LanguageModel.generateText({
1707 prompt: "Use the tool",
1708 toolkit: TestToolkit
1709 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1710
1711 const finishPart = result.content.find((p) => p.type === "finish")
1712 if (finishPart?.type === "finish") {
1713 strictEqual(finishPart.reason, "tool-calls")
1714 }
1715 }).pipe(
1716 Effect.provide([
1717 makeTestLayer({
1718 body: { output: [makeFunctionCall("TestTool", { input: "test" })] }
1719 }),
1720 TestToolkitLayer
1721 ])
1722 ))
1723
1724 it.effect("extracts url citations as source parts", () =>
1725 Effect.gen(function*() {
1726 const result = yield* LanguageModel.generateText({
1727 prompt: "Hello"
1728 }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")))
1729
1730 const sourcePart = result.content.find((p) => p.type === "source")
1731 assert.isDefined(sourcePart)
1732 if (sourcePart?.type === "source" && sourcePart.sourceType === "url") {
1733 strictEqual(sourcePart.url.href, "https://example.com/")
1734 strictEqual(sourcePart.title, "Example")
1735 }
1736 }).pipe(Effect.provide(makeTestLayer({
1737 body: {
1738 output: [{
1739 type: "message",
1740 id: "msg_123",
1741 role: "assistant",
1742 status: "completed",
1743 content: [{
1744 type: "output_text",
1745 text: "Check this out",
1746 annotations: [{
1747 type: "url_citation",
1748 url: "https://example.com",
1749 title: "Example",
1750 start_index: 0,
1751 end_index: 14
1752 }],
1753 logprobs: []
1754 }]
1755 }]
1756 }
1757 }))))
1758 })
1759 })
1760
1761 describe("streamText", () => {
1762 it.effect.each(["completed", "failed"] as const)(
1763 "preserves prompt cache diagnostics in response.%s",
1764 (status) =>
1765 Effect.gen(function*() {
1766 const event = {
1767 type: `response.${status}`,
1768 sequence_number: 1,
1769 response: {
1770 ...makeDefaultResponse({
1771 status,
1772 error: status === "failed" ? { code: "server_error", message: "generation failed" } : null
1773 }),
1774 service_tier: "fast",
1775 prompt_cache_diagnostics: { type: "cache_hit" }
1776 }
1777 }
1778 const parts = yield* LanguageModel.streamText({ prompt: "test" }).pipe(
1779 Stream.runCollect,
1780 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1781 Effect.provide(OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") })),
1782 Effect.provideService(
1783 HttpClient.HttpClient,
1784 makeRawResponseClient(
1785 `data: ${JSON.stringify(event)}\n\n`,
1786 "text/event-stream"
1787 )
1788 )
1789 )
1790
1791 const finish = parts.find((part) => part.type === "finish")
1792 assert.isDefined(finish)
1793 deepStrictEqual(finish.metadata.openai, { promptCacheDiagnostics: { type: "cache_hit" } })
1794 })
1795 )
1796
1797 it.effect("extracts usage information", () =>
1798 Effect.gen(function*() {
1799 const streamEvents = [
1800 {
1801 type: "response.created",
1802 sequence_number: 1,
1803 response: makeDefaultResponse({ status: "in_progress" })
1804 },
1805 {
1806 type: "response.completed",
1807 sequence_number: 2,
1808 response: makeDefaultResponse({
1809 usage: makeUsage({
1810 input_tokens_details: {
1811 cached_tokens: 3,
1812 cache_write_tokens: 4
1813 }
1814 })
1815 })
1816 }
1817 ] as unknown as ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>
1818
1819 const partsChunk = yield* LanguageModel.streamText({
1820 prompt: "Hello"
1821 }).pipe(
1822 Stream.runCollect,
1823 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1824 Effect.provide(makeStreamTestLayer(streamEvents))
1825 )
1826
1827 const finishPart = globalThis.Array.from(partsChunk).find((part) => part.type === "finish")
1828 assert.isDefined(finishPart)
1829 if (finishPart?.type === "finish") {
1830 deepStrictEqual(finishPart.usage.inputTokens, {
1831 uncached: 7,
1832 total: 10,
1833 cacheRead: 3,
1834 cacheWrite: 4
1835 })
1836 }
1837 }))
1838
1839 it.effect("emits valid apply_patch tool params JSON for update_file diffs", () =>
1840 Effect.gen(function*() {
1841 const diff = "@@ -1 +1 @@\n-old\n+new\n"
1842 const outputItem = {
1843 type: "apply_patch_call",
1844 id: "patch_item_1",
1845 call_id: "patch_call_1",
1846 status: "in_progress",
1847 operation: {
1848 type: "update_file",
1849 path: "src/example.ts",
1850 diff
1851 }
1852 } as const
1853
1854 const streamEvents = [
1855 {
1856 type: "response.created",
1857 sequence_number: 1,
1858 response: makeDefaultResponse({
1859 id: "resp_patch_stream",
1860 status: "in_progress",
1861 output: []
1862 })
1863 },
1864 {
1865 type: "response.output_item.added",
1866 output_index: 0,
1867 sequence_number: 2,
1868 item: outputItem
1869 },
1870 {
1871 type: "response.apply_patch_call_operation_diff.delta",
1872 sequence_number: 3,
1873 output_index: 0,
1874 item_id: outputItem.id,
1875 delta: diff
1876 },
1877 {
1878 type: "response.apply_patch_call_operation_diff.done",
1879 sequence_number: 4,
1880 output_index: 0,
1881 item_id: outputItem.id
1882 }
1883 ] as unknown as ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>
1884
1885 const partsChunk = yield* LanguageModel.streamText({
1886 prompt: "Update src/example.ts",
1887 disableToolCallResolution: true
1888 }).pipe(
1889 Stream.runCollect,
1890 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1891 Effect.provide(makeStreamTestLayer(streamEvents))
1892 )
1893
1894 const parts = globalThis.Array.from(partsChunk)
1895 const params = decodeToolParamsFromStream(parts, outputItem.call_id)
1896
1897 deepStrictEqual(params, {
1898 call_id: outputItem.call_id,
1899 operation: {
1900 type: "update_file",
1901 path: "src/example.ts",
1902 diff
1903 }
1904 })
1905 }))
1906
1907 it.effect("emits tool call from function_call_arguments.done when output_item.done is missing", () =>
1908 Effect.gen(function*() {
1909 const streamEvents = [
1910 {
1911 type: "response.created",
1912 sequence_number: 1,
1913 response: makeDefaultResponse({
1914 id: "resp_function_call_done",
1915 status: "in_progress",
1916 output: []
1917 })
1918 },
1919 {
1920 type: "response.output_item.added",
1921 sequence_number: 2,
1922 output_index: 0,
1923 item: {
1924 type: "function_call",
1925 id: "fc_1",
1926 call_id: "call_1",
1927 name: "TestTool",
1928 arguments: "",
1929 status: "in_progress"
1930 }
1931 },
1932 {
1933 type: "response.function_call_arguments.delta",
1934 sequence_number: 3,
1935 output_index: 0,
1936 item_id: "fc_1",
1937 delta: "{\"input\":\"hel"
1938 },
1939 {
1940 type: "response.function_call_arguments.done",
1941 sequence_number: 4,
1942 output_index: 0,
1943 item_id: "fc_1",
1944 name: "TestTool",
1945 arguments: "{\"input\":\"hello\"}"
1946 },
1947 {
1948 type: "response.completed",
1949 sequence_number: 5,
1950 response: makeDefaultResponse({
1951 id: "resp_function_call_done",
1952 status: "completed",
1953 output: []
1954 })
1955 }
1956 ] as unknown as ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>
1957
1958 const partsChunk = yield* LanguageModel.streamText({
1959 prompt: "Use the test tool",
1960 toolkit: TestToolkit,
1961 disableToolCallResolution: true
1962 }).pipe(
1963 Stream.runCollect,
1964 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
1965 Effect.provide(makeStreamTestLayer(streamEvents)),
1966 Effect.provide(TestToolkitLayer)
1967 )
1968
1969 const parts = globalThis.Array.from(partsChunk)
1970 const toolCalls = parts.filter((part) => part.type === "tool-call" && part.id === "call_1")
1971 strictEqual(toolCalls.length, 1)
1972 const toolCall = toolCalls[0]
1973 assert.isDefined(toolCall)
1974 if (toolCall?.type === "tool-call") {
1975 strictEqual(toolCall.name, "TestTool")
1976 deepStrictEqual(toolCall.params, { input: "hello" })
1977 }
1978
1979 const toolParamsEnd = parts.find((part) => part.type === "tool-params-end" && part.id === "call_1")
1980 assert.isDefined(toolParamsEnd)
1981 }))
1982
1983 it.effect("routes invalid tool call params through failureMode: return without failing the stream", () =>
1984 Effect.gen(function*() {
1985 const toolkit = Toolkit.make(ReturnModeTool)
1986 const handlers = toolkit.toLayer({
1987 ReturnModeTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` })
1988 })
1989
1990 const streamEvents = [
1991 {
1992 type: "response.created",
1993 sequence_number: 1,
1994 response: makeDefaultResponse({
1995 id: "resp_invalid_params",
1996 status: "in_progress",
1997 output: []
1998 })
1999 },
2000 {
2001 type: "response.output_item.added",
2002 sequence_number: 2,
2003 output_index: 0,
2004 item: {
2005 type: "function_call",
2006 id: "fc_1",
2007 call_id: "call_1",
2008 name: "ReturnModeTool",
2009 arguments: "",
2010 status: "in_progress"
2011 }
2012 },
2013 {
2014 type: "response.function_call_arguments.done",
2015 sequence_number: 3,
2016 output_index: 0,
2017 item_id: "fc_1",
2018 name: "ReturnModeTool",
2019 arguments: "{\"input\":123}"
2020 },
2021 {
2022 type: "response.completed",
2023 sequence_number: 4,
2024 response: makeDefaultResponse({
2025 id: "resp_invalid_params",
2026 status: "completed",
2027 output: []
2028 })
2029 }
2030 ] as unknown as ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>
2031
2032 const partsChunk = yield* LanguageModel.streamText({
2033 prompt: "Use the test tool",
2034 toolkit
2035 }).pipe(
2036 Stream.runCollect,
2037 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
2038 Effect.provide(makeStreamTestLayer(streamEvents)),
2039 Effect.provide(handlers)
2040 )
2041
2042 const parts = globalThis.Array.from(partsChunk)
2043 const toolResults = parts.filter((part) => part.type === "tool-result")
2044 strictEqual(toolResults.length, 1)
2045 const toolResult = toolResults[0]
2046 if (toolResult?.type === "tool-result") {
2047 strictEqual(toolResult.isFailure, true)
2048 const result = toolResult.result as AiError.AiError
2049 strictEqual(result._tag, "AiError")
2050 strictEqual(result.reason._tag, "ToolParameterValidationError")
2051 }
2052 assert.isDefined(parts.find((part) => part.type === "finish"))
2053 }))
2054
2055 it.effect("waits for the stable streamed web search action before emitting the tool call", () =>
2056 Effect.gen(function*() {
2057 const toolkit = Toolkit.make(OpenAiTool.WebSearch({}))
2058 const streamEvents = [
2059 {
2060 type: "response.created",
2061 sequence_number: 1,
2062 response: makeDefaultResponse({ status: "in_progress" })
2063 },
2064 {
2065 type: "response.output_item.added",
2066 sequence_number: 2,
2067 output_index: 0,
2068 item: {
2069 type: "web_search_call",
2070 id: "ws_123",
2071 status: "in_progress"
2072 }
2073 },
2074 {
2075 type: "response.output_item.done",
2076 sequence_number: 3,
2077 output_index: 0,
2078 item: makeWebSearchCall()
2079 }
2080 ] as unknown as ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>
2081
2082 const parts = yield* LanguageModel.streamText({
2083 prompt: "Search the web",
2084 toolkit,
2085 disableToolCallResolution: true
2086 }).pipe(
2087 Stream.runCollect,
2088 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
2089 Effect.provide(makeStreamTestLayer(streamEvents))
2090 )
2091
2092 const toolCalls = parts.filter((part) => part.type === "tool-call")
2093 strictEqual(toolCalls.length, 1)
2094 const toolCall = toolCalls[0]
2095 assert.isDefined(toolCall)
2096 assert.deepStrictEqual(toolCall.params, {
2097 action: { type: "search", query: "Effect TypeScript" }
2098 })
2099
2100 const toolResult = parts.find((part) => part.type === "tool-result")
2101 assert.isDefined(toolResult)
2102 assert.deepStrictEqual(toolResult.result, {
2103 action: { type: "search", query: "Effect TypeScript" },
2104 status: "completed"
2105 })
2106 }))
2107
2108 for (const { tool, status } of webSearchFailures) {
2109 it.effect(
2110 `${tool.name} preserves ${status} results from output_item.done`,
2111 () =>
2112 Effect.gen(function*() {
2113 const parts = yield* LanguageModel.streamText({
2114 prompt: "Search the web",
2115 toolkit: Toolkit.make(tool)
2116 }).pipe(
2117 Stream.runCollect,
2118 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
2119 Effect.provide(makeStreamTestLayer([{
2120 type: "response.output_item.done",
2121 sequence_number: 1,
2122 output_index: 0,
2123 item: makeWebSearchCall({ status })
2124 }]))
2125 )
2126
2127 const toolResult = parts.find((part) => part.type === "tool-result")!
2128 strictEqual(toolResult.isFailure, true)
2129 deepStrictEqual(toolResult.result, {
2130 action: { type: "search", query: "Effect TypeScript" },
2131 status
2132 })
2133 })
2134 )
2135 }
2136
2137 it.effect.each(fileSearchOutcomes)(
2138 "OpenAiFileSearch reports $status results from output_item.done",
2139 ({ status, isFailure, results }) =>
2140 Effect.gen(function*() {
2141 const parts = yield* LanguageModel.streamText({
2142 prompt: "Search the files",
2143 toolkit: Toolkit.make(OpenAiTool.FileSearch({ vector_store_ids: ["vs_123"] }))
2144 }).pipe(
2145 Stream.runCollect,
2146 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
2147 Effect.provide(makeStreamTestLayer([{
2148 type: "response.output_item.done",
2149 sequence_number: 1,
2150 output_index: 0,
2151 item: makeFileSearchCall({ status, ...(results === undefined ? {} : { results }) })
2152 }]))
2153 )
2154
2155 const toolResult = parts.find((part) => part.type === "tool-result")!
2156 strictEqual(toolResult.isFailure, isFailure)
2157 deepStrictEqual(toolResult.result, {
2158 status,
2159 queries: ["Effect TypeScript"],
2160 results: results ?? null
2161 })
2162 })
2163 )
2164
2165 it.effect("handles reasoning summary events when reasoning state is missing", () =>
2166 Effect.gen(function*() {
2167 const streamEvents = [
2168 {
2169 type: "response.created",
2170 sequence_number: 1,
2171 response: makeDefaultResponse({
2172 id: "resp_reasoning_missing_state",
2173 status: "in_progress",
2174 output: []
2175 })
2176 },
2177 {
2178 type: "response.reasoning_summary_part.added",
2179 sequence_number: 2,
2180 output_index: 0,
2181 item_id: "rs_missing",
2182 summary_index: 1
2183 },
2184 {
2185 type: "response.reasoning_summary_text.delta",
2186 sequence_number: 3,
2187 output_index: 0,
2188 item_id: "rs_missing",
2189 summary_index: 1,
2190 delta: "thinking"
2191 },
2192 {
2193 type: "response.reasoning_summary_part.done",
2194 sequence_number: 4,
2195 output_index: 0,
2196 item_id: "rs_missing",
2197 summary_index: 1
2198 },
2199 {
2200 type: "response.output_item.done",
2201 sequence_number: 5,
2202 output_index: 0,
2203 item: makeReasoningOutput(["thinking"], { id: "rs_missing" })
2204 },
2205 {
2206 type: "response.output_item.done",
2207 sequence_number: 6,
2208 output_index: 1,
2209 item: makeReasoningOutput([], { id: "rs_done_only" })
2210 },
2211 {
2212 type: "response.completed",
2213 sequence_number: 7,
2214 response: makeDefaultResponse({
2215 id: "resp_reasoning_missing_state",
2216 status: "completed",
2217 output: []
2218 })
2219 }
2220 ] as unknown as ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>
2221
2222 const partsChunk = yield* LanguageModel.streamText({
2223 prompt: "reason"
2224 }).pipe(
2225 Stream.runCollect,
2226 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
2227 Effect.provide(makeStreamTestLayer(streamEvents))
2228 )
2229
2230 const parts = globalThis.Array.from(partsChunk)
2231 assert.isDefined(parts.find((part) => part.type === "reasoning-start" && part.id === "rs_missing:1"))
2232 assert.isDefined(parts.find((part) => part.type === "reasoning-end" && part.id === "rs_missing:1"))
2233 assert.isDefined(parts.find((part) => part.type === "finish"))
2234 }))
2235
2236 it.effect("uses canonical OpenAiMcp name for streamed mcp_call", () =>
2237 Effect.gen(function*() {
2238 const outputItem = makeMcpCall("CheckPackage", { packageName: "effect" }, { id: "mcp_call_1" })
2239 const streamEvents = [
2240 {
2241 type: "response.created",
2242 sequence_number: 1,
2243 response: makeDefaultResponse({
2244 id: "resp_mcp_stream",
2245 status: "in_progress",
2246 output: []
2247 })
2248 },
2249 {
2250 type: "response.output_item.done",
2251 output_index: 0,
2252 sequence_number: 2,
2253 item: outputItem
2254 }
2255 ] as unknown as ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>
2256
2257 const partsChunk = yield* LanguageModel.streamText({
2258 prompt: "Use MCP",
2259 toolkit: McpToolkit,
2260 disableToolCallResolution: true
2261 }).pipe(
2262 Stream.runCollect,
2263 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
2264 Effect.provide(makeStreamTestLayer(streamEvents))
2265 )
2266
2267 const parts = globalThis.Array.from(partsChunk)
2268 const toolCall = parts.find((part) => part.type === "tool-call")
2269 assert.isDefined(toolCall)
2270 if (toolCall?.type === "tool-call") {
2271 strictEqual(toolCall.name, "OpenAiMcp")
2272 deepStrictEqual(toolCall.params, { packageName: "effect" })
2273 }
2274
2275 const toolResult = parts.find((part) => part.type === "tool-result")
2276 assert.isDefined(toolResult)
2277 if (toolResult?.type === "tool-result") {
2278 strictEqual(toolResult.name, "OpenAiMcp")
2279 assertTrue(!toolResult.isFailure, "expected a successful MCP result")
2280 strictEqual(toolResult.result.name, "CheckPackage")
2281 }
2282 }))
2283
2284 it.effect("uses canonical OpenAiMcp name for streamed mcp_approval_request", () =>
2285 Effect.gen(function*() {
2286 const outputItem = makeMcpApprovalRequest("CheckPackage", { packageName: "effect" }, { id: "approval_1" })
2287 const streamEvents = [
2288 {
2289 type: "response.created",
2290 sequence_number: 1,
2291 response: makeDefaultResponse({
2292 id: "resp_mcp_approval_stream",
2293 status: "in_progress",
2294 output: []
2295 })
2296 },
2297 {
2298 type: "response.output_item.done",
2299 output_index: 0,
2300 sequence_number: 2,
2301 item: outputItem
2302 }
2303 ] as unknown as ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>
2304
2305 const partsChunk = yield* LanguageModel.streamText({
2306 prompt: "Use MCP",
2307 toolkit: McpToolkit,
2308 disableToolCallResolution: true
2309 }).pipe(
2310 Stream.runCollect,
2311 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
2312 Effect.provide(makeStreamTestLayer(streamEvents))
2313 )
2314
2315 const parts = globalThis.Array.from(partsChunk)
2316 const toolCall = parts.find((part) => part.type === "tool-call")
2317 assert.isDefined(toolCall)
2318 if (toolCall?.type === "tool-call") {
2319 strictEqual(toolCall.name, "OpenAiMcp")
2320 deepStrictEqual(toolCall.params, { packageName: "effect" })
2321 }
2322
2323 const approvalRequest = parts.find((part) => part.type === "tool-approval-request")
2324 assert.isDefined(approvalRequest)
2325 if (toolCall?.type === "tool-call" && approvalRequest?.type === "tool-approval-request") {
2326 strictEqual(approvalRequest.toolCallId, toolCall.id)
2327 }
2328 }))
2329
2330 it.effect("pre-resolves denied OpenAiMcp approvals without lookup failure", () =>
2331 Effect.gen(function*() {
2332 const result = yield* LanguageModel.generateText({
2333 prompt: Prompt.make([
2334 {
2335 role: "assistant",
2336 content: [
2337 Prompt.toolCallPart({
2338 id: "mcp_tool_call_1",
2339 name: "OpenAiMcp",
2340 params: { packageName: "effect" },
2341 providerExecuted: true
2342 }),
2343 Prompt.makePart("tool-approval-request", {
2344 approvalId: "approval_1",
2345 toolCallId: "mcp_tool_call_1"
2346 })
2347 ]
2348 },
2349 {
2350 role: "tool",
2351 content: [
2352 Prompt.toolApprovalResponsePart({
2353 approvalId: "approval_1",
2354 approved: false,
2355 reason: "Denied"
2356 })
2357 ]
2358 },
2359 { role: "user", content: "Continue" }
2360 ]),
2361 toolkit: McpToolkit
2362 }).pipe(
2363 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")),
2364 Effect.provide(makeTestLayer({
2365 body: {
2366 output: [makeTextOutput("Handled denied MCP approval")]
2367 }
2368 }))
2369 )
2370
2371 strictEqual(result.text, "Handled denied MCP approval")
2372 }))
2373 })
2374
2375 describe("withConfigOverride", () => {
2376 it.effect("merges config overrides", () =>
2377 Effect.gen(function*() {
2378 yield* LanguageModel.generateText({ prompt: "test" }).pipe(
2379 OpenAiLanguageModel.withConfigOverride({ temperature: 0.5 }),
2380 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))
2381 )
2382
2383 const requests = yield* MockHttpClient.requests
2384 const body = yield* getRequestBody(requests[0])
2385
2386 strictEqual(body.temperature, 0.5)
2387 }).pipe(Effect.provide(makeTestLayer())))
2388
2389 it.effect("override takes precedence", () =>
2390 Effect.gen(function*() {
2391 yield* LanguageModel.generateText({ prompt: "test" }).pipe(
2392 OpenAiLanguageModel.withConfigOverride({ temperature: 0.9 }),
2393 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", { temperature: 0.5 }))
2394 )
2395
2396 const requests = yield* MockHttpClient.requests
2397 const body = yield* getRequestBody(requests[0])
2398
2399 strictEqual(body.temperature, 0.9)
2400 }).pipe(Effect.provide(makeTestLayer())))
2401 })
2402
2403 describe("config", () => {
2404 it.effect("does not leak library-only fields into request body", () =>
2405 Effect.gen(function*() {
2406 yield* LanguageModel.generateText({ prompt: "test" }).pipe(
2407 Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini", {
2408 fileIdPrefixes: ["file-"],
2409 strictJsonSchema: false,
2410 useItemReferences: false,
2411 temperature: 0.5
2412 }))
2413 )
2414
2415 const requests = yield* MockHttpClient.requests
2416 const body = yield* getRequestBody(requests[0])
2417
2418 strictEqual(body.fileIdPrefixes, undefined)
2419 strictEqual(body.strictJsonSchema, undefined)
2420 strictEqual(body.useItemReferences, undefined)
2421 strictEqual(body.temperature, 0.5)
2422 }).pipe(Effect.provide(makeTestLayer())))
2423 })
2424})
2425
2426// =============================================================================
2427// Test Infrastructure
2428// =============================================================================
2429
2430class MockOpenAiResponse extends Context.Service<MockOpenAiResponse, {
2431 readonly status: number
2432 readonly body: Generated.Response
2433 readonly headers?: Record<string, string> | undefined
2434}>()("MockOpenAiResponse") {}
2435
2436class MockHttpClient extends Context.Service<MockHttpClient, {
2437 readonly requests: Effect.Effect<ReadonlyArray<HttpClientRequest.HttpClientRequest>>
2438}>()("MockHttpClient") {
2439 static requests = Effect.service(MockHttpClient).pipe(
2440 Effect.flatMap((client) => client.requests)
2441 )
2442}
2443
2444const encodeResponse = Schema.encodeUnknownEffect(OpenAiSchema.Response)
2445
2446const makeHttpClient = Effect.gen(function*() {
2447 const capturedRequests = yield* Ref.make<ReadonlyArray<HttpClientRequest.HttpClientRequest>>([])
2448 const response = yield* MockOpenAiResponse
2449 const body = yield* Effect.orDie(encodeResponse(response.body))
2450
2451 const httpClient = HttpClient.makeWith(
2452 Effect.fnUntraced(function*(requestEffect) {
2453 const request = yield* requestEffect
2454 yield* Ref.update(capturedRequests, Array.append(request))
2455 return HttpClientResponse.fromWeb(
2456 request,
2457 new Response(JSON.stringify(body), {
2458 headers: response.headers ?? {},
2459 status: response.status
2460 })
2461 )
2462 }),
2463 Effect.succeed as HttpClient.HttpClient.Preprocess<HttpClientError.HttpClientError, never>
2464 )
2465
2466 return Context.make(HttpClient.HttpClient, httpClient).pipe(
2467 Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) }))
2468 )
2469})
2470
2471const HttpClientLayer = Layer.effectContext(makeHttpClient)
2472
2473const makeRawResponseClient = (body: string, contentType: string) =>
2474 HttpClient.make((request) =>
2475 Effect.succeed(HttpClientResponse.fromWeb(
2476 request,
2477 new Response(body, { headers: { "content-type": contentType } })
2478 ))
2479 )
2480
2481const makeStreamTestLayer = (events: ReadonlyArray<typeof Generated.ResponseStreamEvent.Type>) => {
2482 const response = HttpClientResponse.fromWeb(
2483 HttpClientRequest.get("https://api.openai.com/v1/responses"),
2484 new Response("", {
2485 status: 200,
2486 headers: { "content-type": "text/event-stream" }
2487 })
2488 )
2489
2490 return Layer.succeed(
2491 OpenAiClient.OpenAiClient,
2492 OpenAiClient.OpenAiClient.of({
2493 client: undefined as any,
2494 createResponse: () => Effect.die(new Error("unexpected createResponse call")),
2495 createResponseStream: () => Effect.succeed([response, Stream.fromIterable(events)]),
2496 createEmbedding: () => Effect.die(new Error("unexpected createEmbedding call"))
2497 })
2498 )
2499}
2500
2501const makeDefaultResponse = (
2502 overrides: Partial<Generated.Response> = {}
2503): Generated.Response => ({
2504 id: "resp_test123",
2505 object: "response",
2506 created_at: Math.floor(Date.now() / 1000),
2507 model: "gpt-4o-mini",
2508 status: "completed",
2509 output: [],
2510 metadata: null,
2511 temperature: null,
2512 top_p: null,
2513 tools: [],
2514 tool_choice: "auto",
2515 error: null,
2516 incomplete_details: null,
2517 instructions: null,
2518 parallel_tool_calls: false,
2519 ...overrides
2520})
2521
2522const makeTestLayer = (options: {
2523 readonly body?: Partial<Generated.Response>
2524 readonly status?: number
2525 readonly headers?: Record<string, string>
2526} = {}) =>
2527 OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe(
2528 Layer.provideMerge(HttpClientLayer),
2529 Layer.provide(Layer.succeed(MockOpenAiResponse, {
2530 body: makeDefaultResponse(options.body),
2531 status: options.status ?? 200,
2532 headers: options.headers ?? {}
2533 }))
2534 )
2535
2536const getRequestBody = (request: HttpClientRequest.HttpClientRequest) =>
2537 Effect.gen(function*() {
2538 const body = request.body
2539 if (body._tag === "Uint8Array") {
2540 const text = new TextDecoder().decode(body.body)
2541 return JSON.parse(text)
2542 }
2543 return yield* Effect.die(new Error("Expected Uint8Array body"))
2544 })
2545
2546const decodeToolParamsFromStream = (
2547 parts: ReadonlyArray<any>,
2548 toolCallId: string
2549): Record<string, unknown> => {
2550 const start = parts.find((part) => part.type === "tool-params-start" && part.id === toolCallId)
2551 const end = parts.find((part) => part.type === "tool-params-end" && part.id === toolCallId)
2552 assert.isDefined(start)
2553 assert.isDefined(end)
2554
2555 const deltas = parts
2556 .filter((part) => part.type === "tool-params-delta" && part.id === toolCallId)
2557 .map((part) => part.delta)
2558 .join("")
2559
2560 return JSON.parse(deltas) as Record<string, unknown>
2561}
2562
2563const makeTextOutput = (
2564 text: string,
2565 overrides: Partial<Generated.OutputMessage> = {}
2566): Generated.OutputMessage => ({
2567 type: "message",
2568 id: "msg_123",
2569 role: "assistant" as const,
2570 status: "completed",
2571 content: [{ type: "output_text", text, annotations: [], logprobs: [] }],
2572 ...overrides
2573})
2574
2575const makeFunctionCall = (
2576 name: string,
2577 args: Record<string, unknown>,
2578 overrides: Partial<Generated.FunctionToolCall> = {}
2579): Generated.FunctionToolCall => ({
2580 type: "function_call",
2581 id: "fc_123",
2582 call_id: "call_123",
2583 name,
2584 arguments: JSON.stringify(args),
2585 status: "completed",
2586 ...overrides
2587})
2588
2589const makeWebSearchCall = (
2590 overrides: Partial<Generated.WebSearchToolCall> = {}
2591): Generated.WebSearchToolCall => ({
2592 type: "web_search_call",
2593 id: "ws_123",
2594 status: "completed",
2595 action: { type: "search", query: "Effect TypeScript" },
2596 ...overrides
2597})
2598
2599const makeFileSearchCall = (
2600 overrides: Partial<Generated.FileSearchToolCall> = {}
2601): Generated.FileSearchToolCall => ({
2602 type: "file_search_call",
2603 id: "fs_123",
2604 status: "completed",
2605 queries: ["Effect TypeScript"],
2606 ...overrides
2607})
2608
2609const makeMcpCall = (
2610 name: string,
2611 args: Record<string, unknown>,
2612 overrides: Partial<Generated.MCPToolCall> = {}
2613): Generated.MCPToolCall => ({
2614 type: "mcp_call",
2615 id: "mcp_call_123",
2616 server_label: "npm",
2617 name,
2618 arguments: JSON.stringify(args),
2619 output: "ok",
2620 status: "completed",
2621 ...overrides
2622})
2623
2624const makeMcpApprovalRequest = (
2625 name: string,
2626 args: Record<string, unknown>,
2627 overrides: Partial<Generated.MCPApprovalRequest> & { readonly approval_request_id?: string } = {}
2628): Generated.MCPApprovalRequest => ({
2629 type: "mcp_approval_request",
2630 id: "approval_123",
2631 server_label: "npm",
2632 name,
2633 arguments: JSON.stringify(args),
2634 ...overrides
2635})
2636
2637const makeReasoningOutput = (
2638 summaries: Array<string>,
2639 overrides: Partial<Generated.ReasoningItem> = {}
2640): Generated.ReasoningItem => ({
2641 type: "reasoning",
2642 id: "rs_123",
2643 summary: summaries.map((text) => ({ type: "summary_text", text })),
2644 encrypted_content: null,
2645 ...overrides
2646})
2647
2648type TestResponseUsage = Omit<Generated.ResponseUsage, "input_tokens_details"> & {
2649 readonly input_tokens_details: {
2650 readonly cached_tokens: number
2651 readonly cache_write_tokens?: number
2652 }
2653}
2654
2655const makeUsage = (
2656 overrides: Partial<TestResponseUsage> = {}
2657): TestResponseUsage => ({
2658 input_tokens: 10,
2659 output_tokens: 20,
2660 total_tokens: 30,
2661 input_tokens_details: { cached_tokens: 0 },
2662 output_tokens_details: { reasoning_tokens: 0 },
2663 ...overrides
2664})
2665
2666const TestTool = Tool.make("TestTool", {
2667 description: "A test tool",
2668 parameters: Schema.Struct({ input: Schema.String }),
2669 success: Schema.Struct({ output: Schema.String })
2670})
2671
2672const ReturnModeTool = Tool.make("ReturnModeTool", {
2673 description: "A test tool",
2674 failureMode: "return",
2675 parameters: Schema.Struct({ input: Schema.String }),
2676 success: Schema.Struct({ output: Schema.String }),
2677 failure: Schema.Struct({ error: Schema.String })
2678})
2679
2680const TransformParamsTool = Tool.make("TransformParamsTool", {
2681 description: "A test tool",
2682 parameters: Schema.Struct({ input: Schema.FiniteFromString }),
2683 success: Schema.Struct({ output: Schema.Finite })
2684})
2685
2686class ParamEncodeService extends Context.Service<ParamEncodeService, {
2687 readonly use: Effect.Effect<void>
2688}>()("ParamEncodeService") {}
2689
2690const AsymmetricParam = Schema.String.pipe(
2691 Schema.decodeTo(Schema.String, {
2692 decode: SchemaGetter.passthrough(),
2693 encode: SchemaGetter.transformEffect<string, string, ParamEncodeService>((value) =>
2694 Effect.service(ParamEncodeService).pipe(
2695 Effect.flatMap((service) => service.use),
2696 Effect.as(value)
2697 )
2698 )
2699 })
2700)
2701
2702const AsymmetricParamsTool = Tool.make("AsymmetricParamsTool", {
2703 description: "A test tool",
2704 parameters: Schema.Struct({ input: AsymmetricParam }),
2705 success: Schema.Struct({ output: Schema.String })
2706})
2707
2708const TestToolkit = Toolkit.make(TestTool)
2709
2710const storedHistoryPrompt = Prompt.make([
2711 { role: "user", content: "Question" },
2712 {
2713 role: "assistant",
2714 content: [
2715 Prompt.textPart({
2716 text: "Answer",
2717 options: { openai: { itemId: "msg_1" } }
2718 }),
2719 Prompt.reasoningPart({
2720 text: "Thinking",
2721 options: {
2722 openai: {
2723 itemId: "rs_1",
2724 encryptedContent: "encrypted-reasoning"
2725 }
2726 }
2727 }),
2728 Prompt.toolCallPart({
2729 id: "call_1",
2730 name: "TestTool",
2731 params: { input: "value" },
2732 providerExecuted: false,
2733 options: { openai: { itemId: "fc_1" } }
2734 })
2735 ]
2736 },
2737 {
2738 role: "tool",
2739 content: [Prompt.toolResultPart({
2740 id: "call_1",
2741 name: "TestTool",
2742 isFailure: false,
2743 result: { output: "result" },
2744 providerExecuted: false
2745 })]
2746 },
2747 { role: "user", content: "Continue" }
2748])
2749
2750const providerExecutedCall = Prompt.toolCallPart({
2751 id: "ws_1",
2752 name: "OpenAiWebSearch",
2753 params: {
2754 action: { type: "search", query: "Effect TypeScript" }
2755 },
2756 providerExecuted: true,
2757 options: { openai: { itemId: "ws_1", status: "completed" } }
2758})
2759
2760const providerExecutedResult = Prompt.toolResultPart({
2761 id: "ws_1",
2762 name: "OpenAiWebSearch",
2763 isFailure: false,
2764 result: {
2765 action: { type: "search", query: "Effect TypeScript" },
2766 status: "completed"
2767 },
2768 providerExecuted: true,
2769 options: { openai: { itemId: "ws_1", status: "completed" } }
2770})
2771
2772const providerExecutedHistoryPrompt = Prompt.make([
2773 { role: "user", content: "Search" },
2774 { role: "assistant", content: [providerExecutedCall, providerExecutedResult] },
2775 { role: "user", content: "Continue" }
2776])
2777
2778const McpToolkit = Toolkit.make(OpenAiTool.Mcp({
2779 server_label: "npm",
2780 server_url: "https://example.com/mcp",
2781 require_approval: "never"
2782}))
2783
2784const TestToolkitLayer = TestToolkit.toLayer({
2785 TestTool: ({ input }) => Effect.succeed({ output: `processed: ${input}` })
2786})