AI
Server-side abstraction over Anthropic and Google LLM providers behind one strategy interface.
@halostack/ai is a small, server-side abstraction over two LLM providers —
Anthropic (Claude, via @anthropic-ai/sdk) and Google (Gemini, via
@google/genai) — sitting behind a single AiStrategy interface. It manages API
keys and provider selection through @halostack/env and exposes thin, zod-typed
helpers (generateText, generateObject, streamText, and streamObject) so
callers never have to touch a raw SDK.
Importing
Everything ships from one entrypoint, @halostack/ai (src/index.ts):
createAiClient(opts?)returns aPromise<AiStrategy>. It picks a provider (defaulting to theAI_PROVIDERenv var) and rejects withAiConfigErrorif that provider's key is unset.checkAiAvailability(provider?)andconfiguredProviders()are async, network-free checks for which keys are present.- The error hierarchy is
AiError, withAiConfigError,AiProviderError, andAiValidationErrorbeneath it. - The exported types include
AiProvider,AiStrategy, andAiUsagealong with the option and result types, plus thenormalizeUsageandconfigFromEnvhelpers.
Because createAiClient() is async, you await it before making a call:
createAiClient(), configFromEnv(), checkAiAvailability(), and
configuredProviders() are all async on purpose: env is imported lazily, so
merely importing @halostack/ai never triggers @halostack/env's eager full-schema
validation. If you'd rather skip the env read entirely, pass an explicit config
— for example one built from a narrow env subset like @halostack/env/docs — which
keeps the package decoupled from server-only vars such as DATABASE_URL.
Structure
The package is split into small modules along clear seams:
src/types.ts—AiProvider,AiStrategy, the option and result types, andAiUsage.src/errors.ts— theAiErrorhierarchy.src/config.ts—configFromEnv(), which resolves the keys and per-provider model from@halostack/env.src/client.ts—createAiClient(), the factory and registry built around an exhaustiveswitch.src/availability.ts— the key-presence checks.src/usage.ts—normalizeUsage(), which maps each SDK's counts into uniform token counts.src/providers/anthropic.tsandsrc/providers/google.ts— the per-provider strategies.src/providers/partial-json.ts— best-effort repair for partial JSON received while streaming.
Conventions
All configuration and keys flow through @halostack/env only. The env vars live in
config/env/src/schemas/ai.ts — ANTHROPIC_API_KEY,
GOOGLE_GENERATIVE_AI_API_KEY, AI_PROVIDER, AI_ANTHROPIC_MODEL, and
AI_GOOGLE_MODEL. Never read process.env, and remember the keys are
server-only.
The default Claude model is claude-opus-4-8; Gemini defaults to
gemini-2.5-flash. Both are configurable through those env vars.
Structured output uses each SDK's native path — messages.parse with
zodOutputFormat on Anthropic, and responseJsonSchema with zod validation on
Gemini — driven by a single zod schema for both. streamObject yields
best-effort partial objects as the stream arrives, then emits a final, fully
zod-validated object as its last value.
When something fails, wrap SDK failures in AiProviderError and schema or parse
failures in AiValidationError.