diff --git a/packages/sdk/src/effect/client.ts b/packages/sdk/src/effect/client.ts index 0eb98835d5..7646b4b3bf 100644 --- a/packages/sdk/src/effect/client.ts +++ b/packages/sdk/src/effect/client.ts @@ -33,9 +33,17 @@ export const make = (options?: ClientOptions) => }) .pipe( Effect.flatMap(HttpClientResponse.filterStatusOk), - Effect.flatMap((response) => response.json), - Effect.map((data) => data as A), + Effect.flatMap((response) => response.text), Effect.mapError((cause) => new ModelsDevError({ cause })), + Effect.flatMap((text) => + Effect.try({ + try: () => { + if (text === "") throw new SyntaxError("Unexpected end of JSON input") + return JSON.parse(text) as A + }, + catch: (cause) => new ModelsDevError({ cause }), + }), + ), ) return { diff --git a/packages/sdk/test/effect.test.ts b/packages/sdk/test/effect.test.ts index 943acedbeb..82dbd7475b 100644 --- a/packages/sdk/test/effect.test.ts +++ b/packages/sdk/test/effect.test.ts @@ -3,11 +3,11 @@ import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { Models, ModelsDevError } from "../src/effect.js" -function stub(data: unknown, init?: ResponseInit) { +function stubResponse(body: string | null, init?: ResponseInit) { const requests: Request[] = [] const fetch = (async (input: Parameters[0], requestInit?: RequestInit) => { requests.push(new Request(input instanceof URL ? input.href : (input as string), requestInit)) - return new Response(JSON.stringify(data), { + return new Response(body, { headers: { "content-type": "application/json" }, ...init, }) @@ -16,6 +16,10 @@ function stub(data: unknown, init?: ResponseInit) { return { requests, layer } } +function stub(data: unknown, init?: ResponseInit) { + return stubResponse(JSON.stringify(data) ?? null, init) +} + test("providers() succeeds through an injected transport", async () => { const { requests, layer } = stub({ anthropic: { id: "anthropic" } }) const program = Effect.gen(function* () { @@ -63,6 +67,17 @@ test("non-2xx fails with ModelsDevError in the error channel", async () => { expect(error._tag).toBe("ModelsDevError") }) +test("empty body fails with ModelsDevError", async () => { + const { layer } = stubResponse("") + const program = Effect.gen(function* () { + const client = yield* Models.make() + return yield* client.providers() + }) + const error = await program.pipe(Effect.flip, Effect.provide(layer), Effect.runPromise) + expect(error).toBeInstanceOf(ModelsDevError) + expect(error._tag).toBe("ModelsDevError") +}) + test("Service and layer provide a shared client", async () => { const { requests, layer } = stub({ "openai/gpt-oss-120b": { id: "openai/gpt-oss-120b" } }) const program = Effect.gen(function* () {