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.
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.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
| Endpoint | Generated hook |
|---|---|
GET /app/inference/policy | useAppGetInferencePolicy β FirmInferencePolicyView (default tier + availableTiers) |
PUT /app/inference/policy | useAppSetInferencePolicy β { 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.
| Piece | What & why |
|---|---|
| Nav | settings-sidebar.tsx β new AI inference item under the existing Firm configuration group, shield icon (Shield). Route /settings/firm/ai-inference. |
| Browserβserver transport | The 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 shell | firm/ai-inference/page.tsx β requireOkAppMe gate, RSC-prefetches the policy + Global providers, dehydrate β HydrationBoundary (warm first paint, ADR-095), passes canWrite = hasSettingsWritePermission(me). |
| Client orchestrator | ai-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 card | tier-card.tsx β pure presentational radio card; full-card click selects via <label htmlFor> β Radix radio button. Coming-soon / read-only β disabled. |
| Approved providers | approved-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). |
| Presentation | lib/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. |
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.
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.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
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.58 file(s), +4021 / -0 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +72 | -0 |
| messages/hi.json | Config | +72 | -0 |
| openapi/app-api.json | Config | +554 | -0 |
| src/app/[locale]/(workspace)/settings/firm/ai-inference/page.tsx | App route | +43 | -0 |
| src/app/api/internal/inference/policy/route.ts | App route | +66 | -0 |
| src/app/api/internal/inference/providers/route.ts | App route | +32 | -0 |
| src/components/settings/ai-inference/ai-inference-page.tsx | Component | +174 | -0 |
| src/components/settings/ai-inference/approved-providers-sheet.stories.tsx | Component | +59 | -0 |
| src/components/settings/ai-inference/approved-providers-sheet.tsx | Component | +149 | -0 |
| src/components/settings/ai-inference/tier-card.stories.tsx | Component | +85 | -0 |
| src/components/settings/ai-inference/tier-card.tsx | Component | +113 | -0 |
| src/components/settings/ai-inference/use-tier-view.ts | Component | +50 | -0 |
| src/components/settings/settings-sidebar.tsx | Component | +8 | -0 |
| src/gen/client/appGetInferencePolicy.ts | Generated contract | +54 | -0 |
| src/gen/client/appListInferenceProviders.ts | Generated contract | +57 | -0 |
| src/gen/client/appSetInferencePolicy.ts | Generated contract | +68 | -0 |
| src/gen/client/index.ts | Generated contract | +3 | -0 |
| src/gen/hooks/index.ts | Generated contract | +20 | -0 |
| src/gen/hooks/useAppGetInferencePolicy.ts | Generated contract | +114 | -0 |
| src/gen/hooks/useAppGetInferencePolicySuspense.ts | Generated contract | +113 | -0 |
| src/gen/hooks/useAppListInferenceProviders.ts | Generated contract | +120 | -0 |
| src/gen/hooks/useAppListInferenceProvidersSuspense.ts | Generated contract | +119 | -0 |
| src/gen/hooks/useAppSetInferencePolicy.ts | Generated contract | +156 | -0 |
| src/gen/index.ts | Generated contract | +105 | -0 |
| src/gen/schemas/approvedProviderItem.json | Generated contract | +72 | -0 |
| src/gen/schemas/approvedProvidersResponse.json | Generated contract | +93 | -0 |
| src/gen/schemas/firmInferencePolicyView.json | Generated contract | +97 | -0 |
| src/gen/schemas/inferenceTier.json | Generated contract | +7 | -0 |
| src/gen/schemas/setFirmInferencePolicyRequest.json | Generated contract | +24 | -0 |
| src/gen/schemas/tierOption.json | Generated contract | +38 | -0 |
| src/gen/schemas/tierStatus.json | Generated contract | +7 | -0 |
| src/gen/types/AppGetInferencePolicy.ts | Generated contract | +54 | -0 |
| src/gen/types/AppListInferenceProviders.ts | Generated contract | +66 | -0 |
| src/gen/types/AppSetInferencePolicy.ts | Generated contract | +76 | -0 |
| src/gen/types/ApprovedProviderItem.ts | Generated contract | +63 | -0 |
| src/gen/types/ApprovedProvidersResponse.ts | Generated contract | +25 | -0 |
| src/gen/types/FirmInferencePolicyView.ts | Generated contract | +52 | -0 |
| src/gen/types/InferenceTier.ts | Generated contract | +13 | -0 |
| src/gen/types/SetFirmInferencePolicyRequest.ts | Generated contract | +26 | -0 |
| src/gen/types/TierOption.ts | Generated contract | +39 | -0 |
| src/gen/types/TierStatus.ts | Generated contract | +13 | -0 |
| src/gen/types/index.ts | Generated contract | +41 | -0 |
| src/gen/zod/appGetInferencePolicySchema.ts | Generated contract | +73 | -0 |
| src/gen/zod/appListInferenceProvidersSchema.ts | Generated contract | +85 | -0 |
| src/gen/zod/appSetInferencePolicySchema.ts | Generated contract | +101 | -0 |
| src/gen/zod/approvedProviderItemSchema.ts | Generated contract | +37 | -0 |
| src/gen/zod/approvedProvidersResponseSchema.ts | Generated contract | +36 | -0 |
| src/gen/zod/firmInferencePolicyViewSchema.ts | Generated contract | +56 | -0 |
| src/gen/zod/index.ts | Generated contract | +38 | -0 |
| src/gen/zod/inferenceTierSchema.ts | Generated contract | +17 | -0 |
| src/gen/zod/setFirmInferencePolicyRequestSchema.ts | Generated contract | +29 | -0 |
| src/gen/zod/tierOptionSchema.ts | Generated contract | +39 | -0 |
| src/gen/zod/tierStatusSchema.ts | Generated contract | +17 | -0 |
| src/lib/inference/gateway.ts | Library | +112 | -0 |
| src/lib/inference/inference-fixtures.ts | Library | +80 | -0 |
| src/lib/inference/presentation.ts | Library | +49 | -0 |
| src/lib/inference/types.ts | Library | +24 | -0 |
| src/lib/kubb-client.ts | Library | +16 | -0 |