| name | llm-api-contract |
| description | LLMService API contract — the correct signatures for complete(), stream(), embed(), and response types. Use when calling LLMService from any layer, writing reasoning strategies, or building LLM-dependent features. |
| user-invocable | false |
LLMService API Contract
The most common source of bugs in this codebase is calling LLMService incorrectly. This skill defines the exact API contract.
Service Definition
export class LLMService extends Context.Tag("LLMService")<
LLMService,
{
readonly complete: (
request: CompletionRequest,
) => Effect.Effect<CompletionResponse, LLMErrors>;
readonly stream: (
request: CompletionRequest,
) => Effect.Effect<Stream.Stream<StreamEvent, LLMErrors>, LLMErrors>;
readonly completeStructured: <A>(
request: StructuredCompletionRequest<A>,
) => Effect.Effect<A, LLMErrors>;
readonly embed: (
texts: readonly string[],
model?: string,
) => Effect.Effect<readonly number[][], LLMErrors>;
readonly countTokens: (
messages: readonly LLMMessage[],
) => Effect.Effect<number, LLMErrors>;
readonly getModelConfig: () => Effect.Effect<ModelConfig, never>;
}
>() {}
CompletionRequest — What You Send
export type CompletionRequest = {
readonly messages: readonly LLMMessage[];
readonly model?: ModelConfig;
readonly maxTokens?: number;
readonly temperature?: number;
readonly stopSequences?: readonly string[];
readonly tools?: readonly ToolDefinition[];
readonly systemPrompt?: string;
readonly logprobs?: boolean;
readonly topLogprobs?: number;
};
CORRECT usage:
const response =
yield *
llm.complete({
messages: [{ role: "user", content: "What is quantum computing?" }],
systemPrompt: "You are a helpful assistant.",
maxTokens: 300,
temperature: 0.7,
});
WRONG — these will NOT compile:
llm.complete({ prompt: "Hello" });
llm.complete({ input: "Hello" });
llm.complete({ messages: "Hello" });
yield* Effect.tryPromise({ try: () => llm.complete({ ... }), ... });
CompletionResponse — What You Get Back
interface CompletionResponse {
readonly content: string;
readonly stopReason: StopReason;
readonly usage: TokenUsage;
readonly model: string;
readonly toolCalls?: readonly ToolCall[];
}
interface TokenUsage {
readonly inputTokens: number;
readonly outputTokens: number;
readonly totalTokens: number;
readonly estimatedCost: number;
}
CORRECT field access:
const response = yield* llm.complete({ messages: [...] });
const text = response.content;
const tokens = response.usage.totalTokens;
const cost = response.usage.estimatedCost;
const reason = response.stopReason;
WRONG field access:
LLMMessage Types
type LLMMessage =
| { readonly role: "system"; readonly content: string }
| {
readonly role: "user";
readonly content: string | readonly ContentBlock[];
}
| {
readonly role: "assistant";
readonly content: string | readonly ContentBlock[];
};
Error Handling
LLMService methods return Effect.Effect<T, LLMErrors> — they already handle errors using Effect. Do NOT wrap calls in Effect.tryPromise. Use Effect.mapError or Effect.catchTag to transform errors:
const result = yield* llm.complete({ messages: [...] }).pipe(
Effect.mapError((e) => new MyError({ message: `LLM failed: ${e.message}` })),
);
const result = yield* llm.complete({ messages: [...] }).pipe(
Effect.catchTag("LLMRateLimitError", (e) =>
Effect.sleep(e.retryAfterMs).pipe(Effect.flatMap(() => llm.complete({ messages: [...] }))),
),
);
Embeddings (Tier 2 Memory Only)
const vectors = yield * llm.embed(["text to embed", "another text"]);
Prompt Caching (Anthropic)
import { makeCacheable } from "@reactive-agents/llm-provider";
const message: LLMMessage = {
role: "user",
content: [
makeCacheable(staticSystemContext),
{ type: "text", text: dynamicInput },
],
};
Model Presets
Available presets: "claude-haiku", "claude-sonnet", "claude-sonnet-4-5", "claude-opus", "gpt-4o-mini", "gpt-4o"
import { ModelPresets } from "@reactive-agents/llm-provider";
const config = ModelPresets["claude-sonnet"];
Provider Fallback Chain
.withFallbacks({
providers: ["anthropic", "openai"],
})
Test Provider
The test provider is auto-selected when using .withTestScenario(). It returns deterministic responses from the TestTurn[] array.