@thomasfosterau/effect-ai-ext
v0.4.2
Published
Provider-neutral AI service abstractions upstream Effect lacks — reranker, classifier, TTS, STT, image, moderation, voice conversion — each shaped like effect/ai.
Maintainers
Readme
@thomasfosterau/effect-ai-ext
Provider-neutral AI service abstractions that upstream Effect
(effect/ai) does not yet ship. effect/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/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 (>=4.0.0-rc.118) is a peer dependency (it must stay a singleton).
A superset of effect/ai
The package barrel re-exports every upstream effect/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/ai (effect is
the singleton peer), so mixing the two import styles is safe — nothing is
wrapped or shadowed. Prefer importing straight from effect/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 | Would be effect/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 | Would be effect/ai/ClassifierModel — single/multi-label text classification with confidences and optional example-shots. |
| SpeechModel | generate, stream | Would be effect/ai/SpeechModel — text-to-speech with both a one-shot buffer surface and a streaming surface, like LanguageModel.generateText/streamText. |
| TranscriptionModel | transcribe | Would be effect/ai/TranscriptionModel — speech-to-text with optional segment timestamps and detected language. |
| ImageModel | generate | Would be effect/ai/ImageModel — prompt-to-image with base64-or-bytes result parts. |
| ModerationModel | moderate, fromLanguageModel | Would be effect/ai/ModerationModel — per-category policy scores plus a flagged verdict. |
| VoiceConversionModel | convert | Would be effect/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/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.
License
MIT
