| 1 | import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" |
| 2 | import { assert, describe, it } from "@effect/vitest" |
| 3 | import { deepStrictEqual } from "@effect/vitest/utils" |
| 4 | import { Effect, Schema, Stream } from "effect" |
| 5 | import { type AiError, LanguageModel, type Response, Tool, Toolkit } from "effect/ai" |
| 6 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http" |
| 7 | |
| 8 | describe("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 | }) |