import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" import { assert, describe, it } from "@effect/vitest" import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { Array, Context, Effect, Layer, Redacted, Ref, Schema } from "effect" import { LanguageModel, Prompt } from "effect/unstable/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("OpenRouterLanguageModel", () => { describe("generateText", () => { describe("message preparation", () => { describe("audio file parts", () => { it.effect("converts audio bytes to input_audio", () => Effect.gen(function*() { const audioData = new Uint8Array([0x49, 0x44, 0x33, 0x04]) // ID3v2 magic bytes yield* LanguageModel.generateText({ prompt: Prompt.make([{ role: "user", content: [ Prompt.filePart({ mediaType: "audio/mpeg", data: audioData }) ] }]) }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) const requests = yield* MockHttpClient.requests const body = yield* getRequestBody(requests[0]) const userMessage = body.messages.find((message: any) => message.role === "user") deepStrictEqual(userMessage.content, [{ type: "input_audio", input_audio: { data: "SUQzBA==", format: "mp3" } }]) }).pipe(Effect.provide(makeTestLayer()))) it.effect("converts base64 data url audio to input_audio", () => Effect.gen(function*() { yield* LanguageModel.generateText({ prompt: Prompt.make([{ role: "user", content: [ Prompt.filePart({ mediaType: "audio/wav", data: "data:audio/wav;base64,UklGRg==" }) ] }]) }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) const requests = yield* MockHttpClient.requests const body = yield* getRequestBody(requests[0]) const userMessage = body.messages.find((message: any) => message.role === "user") deepStrictEqual(userMessage.content, [{ type: "input_audio", input_audio: { data: "UklGRg==", format: "wav" } }]) }).pipe(Effect.provide(makeTestLayer()))) it.effect("maps audio media types to OpenRouter audio formats", () => Effect.gen(function*() { const mediaTypes: ReadonlyArray = [ ["audio/aac", "aac"], ["audio/x-aiff", "aiff"], ["audio/flac", "flac"], ["audio/L16", "pcm16"], ["audio/mp4", "m4a"], ["audio/mp3", "mp3"], ["audio/ogg", "ogg"], ["audio/x-wav", "wav"] ] yield* LanguageModel.generateText({ prompt: Prompt.make([{ role: "user", content: mediaTypes.map(([mediaType]) => Prompt.filePart({ mediaType, data: new Uint8Array([0x00]) }) ) }]) }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) const requests = yield* MockHttpClient.requests const body = yield* getRequestBody(requests[0]) const userMessage = body.messages.find((message: any) => message.role === "user") deepStrictEqual( userMessage.content.map((item: any) => item.input_audio.format), mediaTypes.map(([, format]) => format) ) }).pipe(Effect.provide(makeTestLayer()))) it.effect("fails on unsupported audio media types", () => Effect.gen(function*() { const error = yield* LanguageModel.generateText({ prompt: Prompt.make([{ role: "user", content: [ Prompt.filePart({ mediaType: "audio/webm", data: new Uint8Array([0x00]) }) ] }]) }).pipe( Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")), Effect.flip ) strictEqual(error.reason._tag, "InvalidUserInputError") assert.include(error.message, "audio/webm") }).pipe(Effect.provide(makeTestLayer()))) it.effect("fails on audio URLs", () => Effect.gen(function*() { const error = yield* LanguageModel.generateText({ prompt: Prompt.make([{ role: "user", content: [ Prompt.filePart({ mediaType: "audio/mpeg", data: new URL("https://example.com/audio.mp3") }) ] }]) }).pipe( Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash")), Effect.flip ) strictEqual(error.reason._tag, "InvalidUserInputError") }).pipe(Effect.provide(makeTestLayer()))) it.effect("converts non-audio files to file blocks", () => Effect.gen(function*() { const pdfData = new Uint8Array([0x25, 0x50, 0x44, 0x46]) // %PDF yield* LanguageModel.generateText({ prompt: Prompt.make([{ role: "user", content: [ Prompt.filePart({ mediaType: "application/pdf", fileName: "document.pdf", data: pdfData }) ] }]) }).pipe(Effect.provide(OpenRouterLanguageModel.model("google/gemini-2.5-flash"))) const requests = yield* MockHttpClient.requests const body = yield* getRequestBody(requests[0]) const userMessage = body.messages.find((message: any) => message.role === "user") deepStrictEqual(userMessage.content, [{ type: "file", file: { filename: "document.pdf", file_data: "data:application/pdf;base64,JVBERg==" } }]) }).pipe(Effect.provide(makeTestLayer()))) }) }) }) }) // ============================================================================= // Test Infrastructure // ============================================================================= class MockOpenRouterResponse extends Context.Service | undefined }>()("MockOpenRouterResponse") {} class MockHttpClient extends Context.Service> }>()("MockHttpClient") { static requests = Effect.service(MockHttpClient).pipe( Effect.flatMap((client) => client.requests) ) } const encodeResponse = Schema.encodeEffect(Generated.SendChatCompletionRequest200) const makeHttpClient = Effect.gen(function*() { const capturedRequests = yield* Ref.make>([]) const response = yield* MockOpenRouterResponse const body = yield* Effect.orDie(encodeResponse(response.body)) const httpClient = HttpClient.makeWith( Effect.fnUntraced(function*(requestEffect) { const request = yield* requestEffect yield* Ref.update(capturedRequests, Array.append(request)) return HttpClientResponse.fromWeb( request, new Response(JSON.stringify(body), { headers: response.headers ?? {}, status: response.status }) ) }), Effect.succeed as HttpClient.HttpClient.Preprocess ) return Context.make(HttpClient.HttpClient, httpClient).pipe( Context.add(MockHttpClient, MockHttpClient.of({ requests: Ref.get(capturedRequests) })) ) }) const HttpClientLayer = Layer.effectContext(makeHttpClient) const makeDefaultResponse = ( overrides: Partial = {} ): typeof Generated.SendChatCompletionRequest200.Type => ({ id: "gen-test123", choices: [{ finish_reason: "stop", index: 0, message: { role: "assistant", content: "Hello!" } }], created: 1234567890, model: "google/gemini-2.5-flash", object: "chat.completion", ...overrides }) const makeTestLayer = (options: { readonly body?: Partial readonly status?: number readonly headers?: Record } = {}) => OpenRouterClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( Layer.provideMerge(HttpClientLayer), Layer.provide(Layer.succeed(MockOpenRouterResponse, { body: makeDefaultResponse(options.body), status: options.status ?? 200, headers: options.headers ?? {} })) ) const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => Effect.gen(function*() { const body = request.body if (body._tag === "Uint8Array") { const text = new TextDecoder().decode(body.body) return JSON.parse(text) } return yield* Effect.die(new Error("Expected Uint8Array body")) })