@factory/ai
The AI gateway for AI Software Factory (ARCHITECTURE.md §10,
docs/architecture/ai-architecture.md). One package, zero provider SDK
dependencies: adapters speak raw HTTP + SSE over an injectable fetch, so the
whole surface is unit-testable offline (the MockProvider runs the CI
capability suite, TASK-0121).
Source-first: main/types point at src/index.ts — no build step until
Phase 0 completion.
What's in here
| Export | Purpose |
|---|---|
AiGateway |
Streaming chat (base primitive), chatCollected, embed, ping. Retries (429/5xx/network, 1s→4s→16s, pre-output only), fallback provider once, per-provider circuit breaker (5 failures → 30s open, half-open probe), submit-time budget guard, usage ledger, error normalization. |
OpenAIAdapter / OssAdapter / CustomAdapter |
One OpenAI-compatible adapter (baseUrl + Bearer). OSS embedding dimension discovered at first use — never assumed. |
AnthropicAdapter |
claude family; system → top-level param; tool_use reassembly from input_json_delta streams. No embed (gateway routes to embedFallbackProvider, default openai). |
GoogleAdapter |
gemini family (:streamGenerateContent?alt=sse); system → systemInstruction; functionCall parts. |
MockProvider |
Deterministic in-process provider for tests (echoes, honors the chunk protocol, failTimes/failStatus injection). |
resolveAiConfig |
Zod-validated per-field merge: workspace → org policy → env → defaults (arch §2.3; the richer arch shape wins over TASK-0121's flat shape — recorded conflict). |
UsageLedger / BudgetService |
ai_usage records (in-memory + pluggable sink); submit-time checks: tokensPerMonth (estimate = 2×maxTokens), dailySpendUsd, requestsPerMinute (sliding 60s), silent maxOutputTokens clamp. |
validateTools |
Strict JSON-Schema subset (no $ref/pattern/combinators) — tools are validated before any spend. |
Errors
Provider failures never leak raw bodies. Normalized shapes:
- auth (401/403) →
AI_ERROR(502),details.code = SERVICE_UNAVAILABLE {reason:"auth"} - provider 429 →
RATE_LIMITED(429) - 5xx / timeout / transport →
AI_ERROR(502),details = SERVICE_UNAVAILABLE {reason, retryable} - budget → status 402, shared envelope
AI_ERROR,details.code = AI_BUDGET_EXCEEDED
Recorded conflict (rule 29): TASK-0121 requires error codes
AI_BUDGET_EXCEEDED/SERVICE_UNAVAILABLE, but the frozen@factory/sharedErrorCodesenum has neither. Resolution: those codes live inAiErrorCodes(this package) and ride indetails.code; the shared envelope keeps frozen codes (AI_ERROR/RATE_LIMITED). The API layer mapsdetails.codefor clients.
Config resolution order
resolveAiConfig(workspace, org, env) merges per-field (never all-or-nothing):
- Workspace —
workspace.settings.ai(Zod:AiConfigSchema;{provider, model, endpoint{baseUrl, extraHeaders}, budgets{tokensPerMonth, requestsPerMinute, dailySpendUsd}, secretsRef}) - Org policy — currently
null(enterprise scope) - Env defaults —
AI_PROVIDER/AI_MODEL/AI_EMBEDDING_MODEL/AI_BASE_URL/AI_API_KEY(the API passes these viaapps/api/.../gateway-provider.ts) - Package defaults —
openai/gpt-4o/text-embedding-3-small
Provider selection is configuration-only: swapping AI_PROVIDER (or a
workspace's settings.ai.provider) changes zero application code (ROADMAP
Phase 12 exit criterion). secretsRef is an env-var NAME (env:NAME or bare
NAME → AI_API_KEY), never a literal — enforced by Zod.
AI_PROVIDER=mock (API/worker env) swaps the registry to the deterministic
MockProvider — CI/dev runs the full gateway surface offline. It is test
configuration, never a production value.
Capability suite
tests/integration/ai-capability/ runs the identical assertion set (echo
chat, streaming delta order, tool-call roundtrip, embedding dimension
stability, error mapping) against the mock provider always, and against
the real provider when AI_REAL_TEST=1 is set in CI (skipped with a warning
otherwise). The suite contains zero app-code branching on provider — the
provider is selected by env, exactly like production.
Tests
Vitest suites: config, usage, tools, gateway (retry/fallback/circuit/
budget/clamp), adapters (fake-fetch SSE for all three protocols). Independent
verification harness: .tools/verify/ai.verify.ts (94 checks, 0 failures).