HaloStack Docs
Open App
Packages

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 a Promise<AiStrategy>. It picks a provider (defaulting to the AI_PROVIDER env var) and rejects with AiConfigError if that provider's key is unset.
  • checkAiAvailability(provider?) and configuredProviders() are async, network-free checks for which keys are present.
  • The error hierarchy is AiError, with AiConfigError, AiProviderError, and AiValidationError beneath it.
  • The exported types include AiProvider, AiStrategy, and AiUsage along with the option and result types, plus the normalizeUsage and configFromEnv helpers.

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.tsAiProvider, AiStrategy, the option and result types, and AiUsage.
  • src/errors.ts — the AiError hierarchy.
  • src/config.tsconfigFromEnv(), which resolves the keys and per-provider model from @halostack/env.
  • src/client.tscreateAiClient(), the factory and registry built around an exhaustive switch.
  • src/availability.ts — the key-presence checks.
  • src/usage.tsnormalizeUsage(), which maps each SDK's counts into uniform token counts.
  • src/providers/anthropic.ts and src/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.tsANTHROPIC_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.