1import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter"
2import { assert, describe, it } from "@effect/vitest"
3import { deepStrictEqual } from "@effect/vitest/utils"
4import { Effect, Schema, Stream } from "effect"
5import { type AiError, LanguageModel, type Response, Tool, Toolkit } from "effect/ai"
6import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"
7
8describe("Generated", () => {
9 it.effect("sends audio transcriptions as JSON with structured provider options", () =>
10 Effect.gen(function*() {
11 const payload: typeof Generated.CreateAudioTranscriptionsRequestJson.Encoded = {
12 model: "openai/whisper-1",
13 input_audio: { data: "UklGRg==", format: "wav" },
14 provider: { options: { openai: { prompt: "Effect" } } },
15 language: "en",
16 response_format: "verbose_json",
17 timestamp_granularities: ["word"]
18 }
19 const response = { text: "Hello Effect", language: "en", duration: 1.5 }
20 let requests = 0
21 const client = Generated.make(
22 HttpClient.make((request) => {
23 requests++
24 assert.strictEqual(request.method, "POST")
25 assert.strictEqual(request.url, "https://openrouter.ai/api/v1/audio/transcriptions")
26 assert.strictEqual(request.headers["content-type"], "application/json")
27 assert.strictEqual(request.body._tag, "Uint8Array")
28 if (request.body._tag === "Uint8Array") {
29 deepStrictEqual(JSON.parse(new TextDecoder().decode(request.body.body)), payload)
30 }
31 return Effect.succeed(HttpClientResponse.fromWeb(
32 request,
33 globalThis.Response.json(response)
34 ))
35 }).pipe(HttpClient.mapRequest(HttpClientRequest.prependUrl("https://openrouter.ai/api/v1")))
36 )
37 deepStrictEqual(yield* client.createAudioTranscriptions({ payload }), response)
38 assert.strictEqual(requests, 1)
39 }))
40
41 it("decodes nullable generation statistics", () => {
42 const response: Generated.GetGeneration200 = {
43 data: {
44 id: "gen-test",
45 upstream_id: null,
46 total_cost: 0.003294,
47 cache_discount: null,
48 upstream_inference_cost: null,
49 created_at: "2026-07-24T12:00:00Z",
50 data_region: "global",
51 model: "openrouter/auto",
52 app_id: null,
53 streamed: null,
54 cancelled: null,
55 provider_name: null,
56 http_referer: null,
57 latency: null,
58 moderation_latency: null,
59 generation_time: null,
60 finish_reason: null,
61 tokens_prompt: null,
62 tokens_completion: null,
63 native_tokens_prompt: null,
64 native_tokens_completion: null,
65 native_tokens_completion_images: null,
66 native_tokens_reasoning: null,
67 native_tokens_cached: null,
68 num_fetches: null,
69 num_media_prompt: null,
70 num_input_audio_prompt: null,
71 num_media_completion: null,
72 num_search_results: null,
73 origin: "https://openrouter.ai/",
74 preset_id: null,
75 usage: 0.003294,
76 is_byok: false,
77 native_finish_reason: null,
78 external_user: null,
79 api_type: null,
80 request_id: null,
81 response_cache_source_id: null,
82 router: null,
83 service_tier: null,
84 session_id: null,
85 provider_responses: null,
86 user_agent: null,
87 web_search_engine: null
88 }
89 }
90
91 deepStrictEqual(Schema.decodeUnknownSync(Generated.GetGeneration200)(response), response)
92 })
93
94 it("preserves streamed usage cost fields", () => {
95 const usage = {
96 completion_tokens: 11,
97 prompt_tokens: 7,
98 total_tokens: 18,
99 cost: 0.000365,
100 is_byok: false,
101 prompt_tokens_details: {
102 cached_tokens: 0,
103 cache_write_tokens: 0
104 }
105 }
106
107 deepStrictEqual(Schema.decodeUnknownSync(Generated.ChatUsage)(usage), usage)
108 })
109
110 for (
111 const { encrypted, mode, reason, tool } of [
112 { mode: "stream", encrypted: true, tool: true, reason: "tool-calls" },
113 { mode: "stream", encrypted: false, tool: true, reason: "stop" },
114 { mode: "stream", encrypted: true, tool: false, reason: "stop" },
115 { mode: "generate", encrypted: true, tool: true, reason: "tool-calls" }
116 ] as const
117 ) {
118 it.effect(`${mode}: encrypted=${encrypted}, tool=${tool} finishes with ${reason}`, () =>
119 Effect.gen(function*() {
120 const metadata = { id: "response-1", model: "test/reasoning-model", created: 1 }
121 const reasoningDetails = encrypted
122 ? [{ type: "reasoning.encrypted", data: "opaque-signature", format: "unknown" } as const]
123 : []
124 const toolCalls = tool
125 ? [{
126 index: 0,
127 id: "call-1",
128 type: "function" as const,
129 function: { name: "ProbeTool", arguments: "{\"value\":1}" }
130 }]
131 : []
132 const usage = { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
133 const chunks = yield* Schema.decodeUnknownEffect(Schema.Array(Generated.ChatStreamChunk))([
134 {
135 ...metadata,
136 object: "chat.completion.chunk",
137 choices: [{ index: 0, delta: { reasoning_details: reasoningDetails, tool_calls: toolCalls } }]
138 },
139 {
140 ...metadata,
141 object: "chat.completion.chunk",
142 choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
143 usage
144 }
145 ])
146 const body = yield* Schema.decodeUnknownEffect(Generated.SendChatCompletionRequest200)({
147 ...metadata,
148 object: "chat.completion",
149 system_fingerprint: null,
150 choices: [{
151 index: 0,
152 finish_reason: "stop",
153 message: { role: "assistant", reasoning_details: reasoningDetails, tool_calls: toolCalls }
154 }],
155 usage
156 })
157 const response = HttpClientResponse.fromWeb(
158 HttpClientRequest.post("https://example.com/chat/completions"),
159 new globalThis.Response()
160 )
161 const toolkit = Toolkit.make(Tool.make("ProbeTool", {
162 parameters: Schema.Struct({ value: Schema.Number }),
163 success: Schema.String
164 }))
165 const client = OpenRouterClient.OpenRouterClient.of({
166 client: Generated.make(HttpClient.make(() => Effect.die("Unexpected HTTP request"))),
167 createDecisions: () => Effect.die("Unexpected decisions request"),
168 createChatCompletion: () => Effect.succeed([body, response]),
169 createChatCompletionStream: () => Effect.succeed([response, Stream.fromIterable(chunks)])
170 })
171 const options = { prompt: "Use the probe tool", toolkit, disableToolCallResolution: true } as const
172 const operation: Effect.Effect<Array<Response.AnyPart>, AiError.AiError, LanguageModel.LanguageModel> =
173 mode === "stream"
174 ? LanguageModel.streamText(options).pipe(Stream.runCollect)
175 : LanguageModel.generateText(options).pipe(Effect.map((result) => result.content))
176 const parts = globalThis.Array.from(
177 yield* operation.pipe(
178 Effect.provide(OpenRouterLanguageModel.model(metadata.model)),
179 Effect.provide(toolkit.toLayer({ ProbeTool: () => Effect.succeed("ok") })),
180 Effect.provideService(OpenRouterClient.OpenRouterClient, client)
181 )
182 )
183
184 deepStrictEqual(
185 parts.filter((part) => part.type === "tool-call").map((part) => part.name),
186 tool ? ["ProbeTool"] : []
187 )
188 deepStrictEqual(parts.filter((part) => part.type === "finish").map((part) => part.reason), [reason])
189 }))
190 }
191})