PR: #336: [Story-1313] AI inference settings page (Global live; US/EU/India coming-soon) by @ankitmaheshwari-com
View PR →
Story-1313 · AI inference settings · pramaan-docs ADR-120 · 🌐 app

The firm AI-inference policy page β€” where a firm sees and sets where AI processing runs

This ships the firm-facing AI inference settings surface (pramaan-docs ADR-120, D16): a single firm-wide policy page where an admin picks the residency tier its matter content is processed in for AI analysis and drafting, reviews the named approved providers, accepts the data-processing terms, and saves. It is a consumer of the pramaan-functions inference contract (get/set policy + approved-providers) that merged to main; the OpenAPI mirror was synced and pnpm codegen run as the first step of this PR.

Honesty over steering (D6/D-honesty). Only Global β€” Cost-Optimised is genuinely live today; US / EU / India are coming-soon and disabled, driven by the backend's availableTiers[].status β€” not hardcoded. No "Recommended" badge. Retention copy says "limited retention controls", never "never stored". The stale reference mock (US shown as live default) was deliberately not followed.

Step 0.5 β€” contract sync (part of this PR)

gh api .../pramaan-functions/contents/openapi/app-api.json | base64 -d > openapi/app-api.json
pnpm codegen   # Kubb regenerates src/gen for the 3 new endpoints
EndpointGenerated hook
GET /app/inference/policyuseAppGetInferencePolicy β†’ FirmInferencePolicyView (default tier + availableTiers)
PUT /app/inference/policyuseAppSetInferencePolicy ← { tier, acceptedTermsVersion } (D7 ToS gate, audit-logged backend-side)
GET /app/inference/providers?tier=useAppListInferenceProviders β†’ ApprovedProvidersResponse (named, versioned, Direct/Brokered, training-excluded β€” D9/D10)

The diff to src/gen/ is inference-only + the regenerated barrels; no unrelated drift.

What ships (pramaan-app only)

PieceWhat & why
Navsettings-sidebar.tsx β€” new AI inference item under the existing Firm configuration group, shield icon (Shield). Route /settings/firm/ai-inference.
Browser→server transportThe canonical BFF triad (settings AGENTS.md): toBrowserRoute maps in kubb-client.ts (/app/inference/* → /api/internal/inference/*); proxy handlers api/internal/inference/policy/route.ts (GET/PUT) + providers/route.ts (GET); server-only lib/inference/gateway.ts with assertContract at every trust boundary (HR-21). Secrets stay server-side.
Server shellfirm/ai-inference/page.tsx β€” requireOkAppMe gate, RSC-prefetches the policy + Global providers, dehydrate β†’ HydrationBoundary (warm first paint, ADR-095), passes canWrite = hasSettingsWritePermission(me).
Client orchestratorai-inference-page.tsx (≀150 lines) β€” reads via useAppGetInferencePolicy(), owns tier selection + ToS checkbox state, writes via useAppSetInferencePolicy + invalidate. Save gated on canWrite && accepted && tier is live. Reuses SettingsPageHeader, PrimaryButton, Checkbox, RadioGroup β€” no hand-rolled chrome.
Tier cardtier-card.tsx β€” pure presentational radio card; full-card click selects via <label htmlFor> β†’ Radix radio button. Coming-soon / read-only β‡’ disabled.
Approved providersapproved-providers-sheet.tsx β€” the linked surface (D9): the picker shows no model names; this drawer renders the named, versioned list scoped to the selected tier. Lazy (enabled: open).
Presentationlib/inference/presentation.ts β€” the app-owned INFERENCE_TERMS_VERSION (the backend stores the client-supplied version opaquely), tier ordering, route-tone + date helpers. use-tier-view.ts builds the bilingual per-tier card copy.

Why tier copy is i18n-keyed, not backend-rendered

The backend TierOption carries label/reason as English-only strings. Rendering them directly would break Hindi. So the backend remains the source of truth for which tiers exist, their status, and the default, while the display copy (label, description, region, India quality-notice) reads from next-intl keyed by the tier enum β€” present in both en.json and hi.json (51 keys each, parity verified). Provider names / model families / retention posture are registry data (proper nouns) rendered as-is; only the column headers are translated.

The ToS version question (D7)

The set-policy request requires acceptedTermsVersion, but the contract exposes no "current version to accept". The backend service.py stores whatever string the client sends. So the app owns the terms copy, therefore owns the version identifier: INFERENCE_TERMS_VERSION = "2026-06-15" in presentation.ts, sent on save and bumped whenever the ToS copy materially changes. Checking the box and clicking Save is the recorded acceptance; changing the selected tier resets the checkbox so a fresh acceptance is always captured.

Verification

pnpm typecheck   clean
pnpm lint        clean (0 errors; 4 pre-existing warnings in unrelated files, none introduced)
pnpm test        514 passed (89 files)
pnpm build       green β€” /[locale]/settings/firm/ai-inference + both /api/internal/inference/* routes compiled
storybook:drift  passed
token scan       rg hex/rgb/hsl over new files β†’ none (tokens only)
i18n scan        en/hi AiInference parity = 51/51, zero hardcoded user copy
openapi mirror   synced from pramaan-functions main; codegen diff is inference-only
Storybook stories cover TierCard (live-selected / live-unselected / coming-soon / India-with-notice) and ApprovedProvidersSheet (with providers / empty), seeded from inference-fixtures.ts β€” the empty first-run state is exercised, not just the happy fixture.

Files changed

58 file(s), +4021 / -0 lines

Path Role + -
messages/en.jsonConfig+72-0
messages/hi.jsonConfig+72-0
openapi/app-api.jsonConfig+554-0
src/app/[locale]/(workspace)/settings/firm/ai-inference/page.tsxApp route+43-0
src/app/api/internal/inference/policy/route.tsApp route+66-0
src/app/api/internal/inference/providers/route.tsApp route+32-0
src/components/settings/ai-inference/ai-inference-page.tsxComponent+174-0
src/components/settings/ai-inference/approved-providers-sheet.stories.tsxComponent+59-0
src/components/settings/ai-inference/approved-providers-sheet.tsxComponent+149-0
src/components/settings/ai-inference/tier-card.stories.tsxComponent+85-0
src/components/settings/ai-inference/tier-card.tsxComponent+113-0
src/components/settings/ai-inference/use-tier-view.tsComponent+50-0
src/components/settings/settings-sidebar.tsxComponent+8-0
src/gen/client/appGetInferencePolicy.tsGenerated contract+54-0
src/gen/client/appListInferenceProviders.tsGenerated contract+57-0
src/gen/client/appSetInferencePolicy.tsGenerated contract+68-0
src/gen/client/index.tsGenerated contract+3-0
src/gen/hooks/index.tsGenerated contract+20-0
src/gen/hooks/useAppGetInferencePolicy.tsGenerated contract+114-0
src/gen/hooks/useAppGetInferencePolicySuspense.tsGenerated contract+113-0
src/gen/hooks/useAppListInferenceProviders.tsGenerated contract+120-0
src/gen/hooks/useAppListInferenceProvidersSuspense.tsGenerated contract+119-0
src/gen/hooks/useAppSetInferencePolicy.tsGenerated contract+156-0
src/gen/index.tsGenerated contract+105-0
src/gen/schemas/approvedProviderItem.jsonGenerated contract+72-0
src/gen/schemas/approvedProvidersResponse.jsonGenerated contract+93-0
src/gen/schemas/firmInferencePolicyView.jsonGenerated contract+97-0
src/gen/schemas/inferenceTier.jsonGenerated contract+7-0
src/gen/schemas/setFirmInferencePolicyRequest.jsonGenerated contract+24-0
src/gen/schemas/tierOption.jsonGenerated contract+38-0
src/gen/schemas/tierStatus.jsonGenerated contract+7-0
src/gen/types/AppGetInferencePolicy.tsGenerated contract+54-0
src/gen/types/AppListInferenceProviders.tsGenerated contract+66-0
src/gen/types/AppSetInferencePolicy.tsGenerated contract+76-0
src/gen/types/ApprovedProviderItem.tsGenerated contract+63-0
src/gen/types/ApprovedProvidersResponse.tsGenerated contract+25-0
src/gen/types/FirmInferencePolicyView.tsGenerated contract+52-0
src/gen/types/InferenceTier.tsGenerated contract+13-0
src/gen/types/SetFirmInferencePolicyRequest.tsGenerated contract+26-0
src/gen/types/TierOption.tsGenerated contract+39-0
src/gen/types/TierStatus.tsGenerated contract+13-0
src/gen/types/index.tsGenerated contract+41-0
src/gen/zod/appGetInferencePolicySchema.tsGenerated contract+73-0
src/gen/zod/appListInferenceProvidersSchema.tsGenerated contract+85-0
src/gen/zod/appSetInferencePolicySchema.tsGenerated contract+101-0
src/gen/zod/approvedProviderItemSchema.tsGenerated contract+37-0
src/gen/zod/approvedProvidersResponseSchema.tsGenerated contract+36-0
src/gen/zod/firmInferencePolicyViewSchema.tsGenerated contract+56-0
src/gen/zod/index.tsGenerated contract+38-0
src/gen/zod/inferenceTierSchema.tsGenerated contract+17-0
src/gen/zod/setFirmInferencePolicyRequestSchema.tsGenerated contract+29-0
src/gen/zod/tierOptionSchema.tsGenerated contract+39-0
src/gen/zod/tierStatusSchema.tsGenerated contract+17-0
src/lib/inference/gateway.tsLibrary+112-0
src/lib/inference/inference-fixtures.tsLibrary+80-0
src/lib/inference/presentation.tsLibrary+49-0
src/lib/inference/types.tsLibrary+24-0
src/lib/kubb-client.tsLibrary+16-0