@pugi/plugin-anvil-provider
v0.1.0-alpha.2
Published
Pugi Anvil provider plugin - multi-tenant routing for Pugi via AI-SDK OpenAI-compatible adapter.
Downloads
283
Maintainers
Readme
@pugi/plugin-anvil-provider
Pugi Anvil provider plugin: multi-tenant routing for the soft-forked Pugi core via the AI-SDK OpenAI-compatible adapter.
Part of the Pugi 1.0 soft fork sprint (see ADR-0081).
What it does
Registers the Pugi Anvil gateway (https://anvil.pugi.io/v1) as a first-class provider inside Pugi and pipes every chat call through it with tenant-scoping headers. A single Pugi process can serve many Pugi tenants because the bearer token is hashed into a stable tenant_<hex> id that is never logged or stored raw.
Anvil owns the server-side provider fallback chain (Together AI Qwen3-Coder-480B by default per CEO directive 2026-06-05, then OpenRouter free tier, Cerebras, Groq) so the plugin stays small: it sets the headers, validates the response, and surfaces typed errors.
Hook surface
| Hook | Purpose |
|---|---|
| config | Register Config.provider['anvil'] with the AI-SDK npm adapter, baseURL, bearer, and static model catalog. Primes the tier cache once at startup. |
| provider | Dynamic model listing via GET /v1/models; falls back to the static catalog when the gateway is unreachable. |
| chat.headers | Inject X-Pugi-Tenant, X-Pugi-Plugin-Version, X-Pugi-Session, X-Pugi-Provider-Fallback, X-Pugi-Tier, and X-Pugi-Upgrade-Hint on every chat call. |
Install
pnpm add @pugi/plugin-anvil-provider
pnpm add @ai-sdk/openai-compatible # peerUsage
// pugi.config.ts
export default {
plugin: [
[
'@pugi/plugin-anvil-provider',
{
apiKey: process.env.PUGI_API_KEY,
anvilBase: 'https://anvil.pugi.io/v1',
// tenantId is derived from sha256(apiKey) if omitted.
// resolveSession is called per request so trace ids stitch in Langfuse.
resolveSession: async () => process.env.PUGI_SESSION_ID,
},
],
],
};Options
| Option | Default | Notes |
|---|---|---|
| apiKey | process.env.PUGI_API_KEY | Required for production use. Used as bearer plus tenant derivation seed. |
| anvilBase | https://anvil.pugi.io/v1 | Trailing slash is normalised away. |
| tenantId | tenant_<sha256(apiKey)[:32]> | Override to pin a specific tenant id (multi-process deployments). |
| providerId | anvil | Key under Config.provider and id in the model picker. |
| tenantHeader | X-Pugi-Tenant | Override only when running against a non-default Anvil deployment. |
| fallbackChain | ['anvil-primary', 'openrouter-free', 'cerebras', 'groq'] | Sent as X-Pugi-Provider-Fallback. Anvil enforces server-side. |
| tierCachePath | ~/.pugi/anvil-tier.json | Filesystem path for the tier snapshot. Override for tests. |
| tierCacheTtlMs | 3600000 (1 hour) | TTL before re-fetch from GET /v1/me. |
| defaultModel | qwen3-coder-480b | Default routed model id. Set to pugi-auto to let Anvil pick. |
| resolveSession | () => undefined | Resolver for the active session id; threaded into X-Pugi-Session. |
Tier matrix
The GET /v1/me response is cached to disk for one hour. Each tier maps to the per-minute and per-month caps that Anvil already enforces server-side; the plugin surfaces the snapshot in the X-Pugi-Tier header so downstream observability can attribute usage.
| Tier | Per-minute rate limit | Monthly token cap | Notes |
|---|---|---|---|
| free | 25 | 1M | Upgrade hint surfaces in X-Pugi-Upgrade-Hint. |
| founder | 50 | 10M | Pugi Founder $20/mo. |
| builder | 100 | 100M | Pugi Builder $99/mo. |
| team | 500 | 1B | Pugi Team $199/mo. |
| enterprise | per contract | per contract | Resolved via the customer table. |
Error handling
All product errors are surfaced as a discriminated AnvilError union. Hook callers use the isAnvilError(err) type guard to narrow:
| kind | Source | Behaviour |
|---|---|---|
| rate_limited | HTTP 429 | retryAfterMs parsed from Retry-After header. |
| upgrade_required | HTTP 402 | tier parsed from response body. Includes upgrade URL. |
| upstream_unavailable | HTTP 5xx | Surfaced verbatim. Anvil already ran its fallback chain. |
| unauthorized | HTTP 401 / 403 | Prompts the operator to re-run pugi login. |
| network | Transport failure | Retried three times with 100 / 500 / 2000 ms backoff before throwing. |
Architecture notes
- HTTP client is native
fetch, no axios. - Tier cache is plain JSON on disk; no Redis dependency.
- Effect-TS is intentionally not imported anywhere in this package, per the ADR-0081 containment rule.
- Retry strategy is fixed delays without jitter; clients are single-tenant and request volume is low.
- Authorization header is set via
Config.provider['anvil'].options.apiKeyso Pugi handles refresh and rotation;chat.headersis reserved for Pugi-specific metadata.
Tests
pnpm --filter @pugi/plugin-anvil-provider testTwelve specs cover option resolution, the three hooks, tier caching, stale-cache replay, retry behaviour, and the 402 / 429 / 5xx response mapping. A node:http fake server is used; no real Anvil traffic is generated.
License
MIT. See LICENSE.
