@thomasfosterau/effect-ai-ext
v0.2.0
Published
Provider-neutral AI service abstractions upstream Effect lacks — reranker, classifier, TTS, STT, image, moderation, voice conversion — each shaped like effect/unstable/ai.
Maintainers
Readme
@thomasfosterau/effect-ai-ext
Provider-neutral AI service abstractions that upstream Effect
(effect/unstable/ai) does not yet ship. effect/unstable/ai gives you
LanguageModel, EmbeddingModel, Tokenizer, Tool/Toolkit, Chat,
Prompt/Response, Model, AiError, and Telemetry — and nothing for
reranking, classification, speech, transcription, image generation, moderation,
or voice conversion.
This package fills that gap in upstream's own idiom. Every module is written as
if it could merge into effect/unstable/ai itself — the same shape as
EmbeddingModel:
- a
Context.Servicetag (@thomasfosterau/effect-ai-ext/<Module>), Schema.Classresponse/usage models,- caller
Optionsvs normalisedProviderOptions/ProviderResponsecontracts, - a
Serviceinterface whose operations fail only throughAiError, - a
make(...)constructor that validates provider results before handing them back, wrapping each operation inEffect.withSpan.
No provider code, no HTTP — abstractions only. First-party provider packages
(Cohere, Voyage, ElevenLabs, …) implement these services the same way
effect-ai-mistral implements LanguageModel.
Install
pnpm add @thomasfosterau/effect-ai-ext effecteffect is a peer dependency (it must stay a singleton).
A superset of effect/unstable/ai
The package barrel re-exports every upstream effect/unstable/ai module
(LanguageModel, EmbeddingModel, AiError, Tool/Toolkit, Chat,
Prompt/Response, Model, Tokenizer, Telemetry, IdGenerator, …)
alongside the abstractions below, so one import reaches the whole AI service
surface — upstream and extension:
import { LanguageModel, EmbeddingModel, RerankerModel } from "@thomasfosterau/effect-ai-ext"The re-exports are the same identities as effect/unstable/ai (effect is
the singleton peer), so mixing the two import styles is safe — nothing is
wrapped or shadowed. Prefer importing straight from effect/unstable/ai when
you only need upstream modules.
Modules & their upstream-merge story
Each module is also available as a per-module subpath (mirroring how effect
itself ships), e.g. @thomasfosterau/effect-ai-ext/RerankerModel.
| Module | Operation(s) | If it merged upstream |
| --- | --- | --- |
| RerankerModel | rerank | effect/unstable/ai/RerankerModel — the sibling of EmbeddingModel for relevance reordering. Validates indices (in range, unique, count ≤ topN), sorts by descending score, short-circuits empty inputs. |
| ClassifierModel | classify, fromLanguageModel | effect/unstable/ai/ClassifierModel — single/multi-label text classification with confidences and optional example-shots. |
| SpeechModel | generate, stream | effect/unstable/ai/SpeechModel — text-to-speech with both a one-shot buffer surface and a streaming surface, like LanguageModel.generateText/streamText. |
| TranscriptionModel | transcribe | effect/unstable/ai/TranscriptionModel — speech-to-text with optional segment timestamps and detected language. |
| ImageModel | generate | effect/unstable/ai/ImageModel — prompt-to-image with base64-or-bytes result parts. |
| ModerationModel | moderate, fromLanguageModel | effect/unstable/ai/ModerationModel — per-category policy scores plus a flagged verdict. |
| VoiceConversionModel | convert | effect/unstable/ai/VoiceConversionModel — re-voice audio into a target voice. |
ClassifierModel and ModerationModel additionally expose a
fromLanguageModel adapter that backs the abstraction with any
LanguageModel via structured-output prompting (LanguageModel.generateObject),
running the model's output back through the same make validation path.
Example
import { Effect } from "effect"
import { RerankerModel } from "@thomasfosterau/effect-ai-ext"
// A provider package builds the service via RerankerModel.make(...);
// consumers depend on the tag.
const program = Effect.gen(function* () {
const reranker = yield* RerankerModel.RerankerModel
const response = yield* reranker.rerank({
query: "best editor",
documents: ["vim", "emacs", "nano"],
topN: 2,
})
return response.results // sorted by descending relevanceScore
})Backing a classifier with a language model:
import { Effect, Layer } from "effect"
import { LanguageModel } from "effect/unstable/ai"
import { ClassifierModel } from "@thomasfosterau/effect-ai-ext"
declare const SomeLanguageModelLayer: Layer.Layer<LanguageModel.LanguageModel>
const ClassifierLayer = Layer.effect(
ClassifierModel.ClassifierModel,
ClassifierModel.fromLanguageModel,
).pipe(Layer.provide(SomeLanguageModelLayer))RerankerModel moved here
RerankerModel previously lived in @thomasfosterau/effect-ai-gateway — a
reranker abstraction never belonged in a routing package. It now lives here;
the gateway package re-exports it from
@thomasfosterau/effect-ai-ext/RerankerModel through a deprecation shim.
Development
Part of the effect-ai-ext monorepo. See
AGENTS.md for the toolchain.
pnpm run check # type-check (native TS7 tsc)
pnpm run test # vitest (stubbed fixtures, no live network)
pnpm run build # tsup → one dist file per module entryLicense
MIT
