The Effect AI SDK wraps LLM calls in Effect's typed error and dependency-injection model. It supports custom OpenAI-compatible endpoints, so you can point it at OpenDunes and run your AI effects against the full model catalog.
npm install effect @effect/ai @effect/ai-openai
In Effect, the LLM provider is a Layer. Build a CompletionsOpenAI layer with OpenDunes credentials.
import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai";
import { FetchHttpClient } from "@effect/platform";
import { Layer, Config } from "effect";
const OpenDunesClient = OpenAiClient.layerConfig({
apiKey: Config.redacted("OPENDUNES_API_KEY"),
baseUrl: Config.succeed("https://opendunes.com/api/v1"),
}).pipe(Layer.provide(FetchHttpClient.layer));
export const OpenDunesCompletions = OpenAiCompletions.layer(
"anthropic/claude-sonnet-5"
).pipe(Layer.provide(OpenDunesClient));
import { AiLanguageModel, AiResponse } from "@effect/ai";
import { Effect, Layer } from "effect";
import { OpenDunesCompletions } from "./providers";
const program = Effect.gen(function* () {
const model = yield* AiLanguageModel.AiLanguageModel;
const response = yield* model.generateText({
prompt: "What is the Casbah of Algiers known for?",
});
return response.text;
});
const result = await Effect.runPromise(
program.pipe(Effect.provide(OpenDunesCompletions))
);
console.log(result);
import { AiLanguageModel } from "@effect/ai";
import { Schema } from "@effect/schema";
import { Effect } from "effect";
import { OpenDunesCompletions } from "./providers";
const CitySchema = Schema.Struct({
name: Schema.String,
country: Schema.String,
population: Schema.Number,
});
const program = Effect.gen(function* () {
const model = yield* AiLanguageModel.AiLanguageModel;
const city = yield* model.generateObject({
prompt: "Return information about Oran, Algeria.",
schema: CitySchema,
});
return city;
});
const city = await Effect.runPromise(
program.pipe(Effect.provide(OpenDunesCompletions))
);
console.log(city.name, city.population);
Note. Structured output (response_format / JSON mode) is supported on OpenDunes for models with the capability. On models without it, the above falls back to prompt-based extraction; models like anthropic/claude-opus-4.8 produce the most reliable JSON.
import { AiLanguageModel } from "@effect/ai";
import { Effect, Stream } from "effect";
import { OpenDunesCompletions } from "./providers";
const program = Effect.gen(function* () {
const model = yield* AiLanguageModel.AiLanguageModel;
const stream = yield* model.streamText({
prompt: "Count to five in Tamazight.",
});
yield* stream.pipe(
Stream.tap((chunk) => Effect.sync(() => process.stdout.write(chunk.text))),
Stream.runDrain
);
});
await Effect.runPromise(program.pipe(Effect.provide(OpenDunesCompletions)));
Build separate layers for different models and provide them conditionally:
const FastLayer = OpenAiCompletions.layer("meta-llama/llama-4-maverick").pipe(
Layer.provide(OpenDunesClient)
);
const HighQualityLayer = OpenAiCompletions.layer("anthropic/claude-opus-4.8").pipe(
Layer.provide(OpenDunesClient)
);
const layer = userTier === "pro" ? HighQualityLayer : FastLayer;
await Effect.runPromise(program.pipe(Effect.provide(layer)));
Browse the full catalog at /models.