Skip to content

6. AI Provider Interface

Status: Roadmap for §6.1–6.2 (text-generation connectors). §6.3 (decision models) is implemented.

Every AI surface in the SDK accepts an AiProvider. The interface is a minimal common denominator that every major LLM satisfies.

interface AiProvider {
run(
model: string,
messages: { role: "system" | "user" | "assistant"; content: string }[]
): Promise<{ text: string }>;
}

Import from @mvrx/mail/providers. Each returns an AiProvider.

Cloudflare Workers AI — zero latency, runs on the same Worker, no egress:

import { cfProvider } from "@mvrx/mail/providers";
const ai = cfProvider(env.AI);
// Uses env.AI.run() — default model: @cf/meta/llama-3.3-70b-instruct
// Override per-call by passing model name to any SDK method

OpenAI:

import { openaiProvider } from "@mvrx/mail/providers";
const ai = openaiProvider({ apiKey: env.OPENAI_KEY });
// Default model: gpt-4o-mini

Anthropic:

import { anthropicProvider } from "@mvrx/mail/providers";
const ai = anthropicProvider({ apiKey: env.ANTHROPIC_KEY });
// Default model: claude-haiku-4-5-20251001

Google Gemini:

import { geminiProvider } from "@mvrx/mail/providers";
const ai = geminiProvider({ apiKey: env.GEMINI_KEY });
// Default model: gemini-2.0-flash

Mistral:

import { mistralProvider } from "@mvrx/mail/providers";
const ai = mistralProvider({ apiKey: env.MISTRAL_KEY });
// Default model: mistral-small-latest

Azure OpenAI:

import { azureProvider } from "@mvrx/mail/providers";
const ai = azureProvider({
endpoint: "https://my-resource.openai.azure.com",
deployment: "gpt-4o-mini",
apiKey: env.AZURE_KEY,
});

Ollama (local / self-hosted):

import { ollamaProvider } from "@mvrx/mail/providers";
const ai = ollamaProvider({ baseUrl: "http://localhost:11434" });
// Default model: llama3.2

Any OpenAI-compatible endpoint:

import { openaiCompatProvider } from "@mvrx/mail/providers";
const ai = openaiCompatProvider({
baseUrl: "https://openrouter.ai/api/v1",
apiKey: env.OPENROUTER_KEY,
defaultModel: "meta-llama/llama-3.3-70b-instruct",
});

Custom:

// Implement the interface directly for any provider not listed above
const ai: AiProvider = {
run: async (model, messages) => {
const res = await myLLM.chat({ model, messages });
return { text: res.output };
},
};

Status: Implemented in @mvrx/aecs/decisions (also re-exported from @mvrx/aecs). This subsection is independent of the roadmap text-generation connectors in §6.1–6.2.

Decision models (also called System One models) do not generate text. They read a state and a map of typed questions, and return one typed answer per question with probabilities. This fits the classification and routing work in §7 and the rules engine (§15): the answer can only be a value the caller defined, so nothing needs to be parsed out of free text.

The SDK targets the System One wire format that TypeSafe Jev and Cloudflare Clef share:

Question type criteria Answer fields
noul optional { true, false } descriptions noul: probability of yes, 0–1
choice map of option id → description (or null), 2–255 options choice, probabilities per option, confidence
score ordered array of level descriptions, lowest first, 2–10 levels score (probability-weighted level index, may fall between levels), probabilities, legend, confidence

A request holds 1–64 questions. Question ids use letters, digits, _, . and - (max 100 characters). Answers come back under the same ids.

interface DecisionProvider {
decide<Q extends DecisionQuestions>(request: {
state: DecisionValue; // string, object or array
questions: Q;
model?: string; // overrides the provider default
images?: (string | { content_type: string; base64: string })[]; // Clef only
signal?: AbortSignal;
}): Promise<{
model: string; // versioned id that answered
answers: DecisionAnswers<Q>; // typed per question; choice answers keep their option ids
usage: { input_tokens: number; output_tokens: number };
}>;
}

Every provider MUST validate the question map before sending it, and MUST check that each answer matches its question’s type: a choice must be one of the defined options and a score must be in range. Any failure throws DecisionError (with status and body when the failure came from HTTP).

TypeSafe Jev: POST https://api.typesafe.ai/v1/systemone. Text only, so images are not sent.

import { jevProvider } from "@mvrx/aecs/decisions";
const jev = jevProvider({ apiKey: env.TYPESAFE_KEY });
// Default model: jev-latest. Pin "jev-1.13.0" if you tune confidence thresholds.
// Through OpenRouter: jevProvider({ apiKey: env.OPENROUTER_KEY,
// endpoint: "https://openrouter.ai/api/v1/systemone", model: "typesafe/jev-1.13" })

Cloudflare Clef, through the Workers AI binding (@cf/cloudflare/clef or @cf/cloudflare/clef-flash). Accepts up to 4 images:

import { clefProvider, clefRestProvider } from "@mvrx/aecs/decisions";
const clef = clefProvider(env.AI); // default model: clef
const fast = clefProvider(env.AI, { model: "clef-flash" });
// Outside a Worker:
const rest = clefRestProvider({ accountId: env.CF_ACCOUNT_ID, apiToken: env.CF_API_TOKEN });

Any System One endpoint: systemOneProvider({ endpoint, apiKey, defaultModel, supportsImages? }).

OpenAI Decisions API: announced at DevDay on 2026-09-29. It is in limited preview and OpenAI has not published a request or response schema, so this SDK has no native connector for it yet. Until it does, textDecisionProvider answers the same typed questions with any text LLM, for example an OpenAI model called with a strict JSON schema:

import { textDecisionProvider } from "@mvrx/aecs/decisions";
const viaLlm = textDecisionProvider({
model: "gpt-4o-mini",
complete: async (messages) => (await openai.responses.create({ model: "gpt-4o-mini", input: messages })).output_text,
});

A text model has no calibrated probability distribution. The adapter gives the chosen option probability 1 and confidence 1, so do not use those values for confidence-gated routing.

decideEmail() builds state from a NormalizedEmail: from, to, subject, date, body and attachment names and types. body is content.forAI, so the untrusted-content wrapper applied at parse time (§10) stays in place (§11.1).

import { parse, wrappers } from "@mvrx/aecs";
import { clefProvider, decideEmail } from "@mvrx/aecs/decisions";
const email = await parse(message.raw, { wrapper: wrappers.xml("email") });
const { answers } = await decideEmail(email, clefProvider(env.AI), {
needs_reply: { type: "noul", instructions: "Does the sender expect a reply?" },
route: {
type: "choice",
instructions: "Which team should handle this email?",
criteria: { billing: "Invoices, refunds", support: "Bugs, outages", sales: "Pricing, upgrades" },
},
urgency: { type: "score", instructions: "How urgent is this?", criteria: ["Can wait", "This week", "Right now"] },
});
if (answers.route.confidence > 0.8) await assign(answers.route.choice); // "billing" | "support" | "sales"

emailToDecisionState(email, { includeMetadata?, includeAttachments? }) returns the same state for use with provider.decide() directly.