Skip to content

feat(ui): manage providers, pricing, and aliases at runtime from the dashboard#297

Open
njbrake wants to merge 2 commits into
mainfrom
ui-2
Open

feat(ui): manage providers, pricing, and aliases at runtime from the dashboard#297
njbrake wants to merge 2 commits into
mainfrom
ui-2

Conversation

@njbrake

@njbrake njbrake commented Jul 17, 2026

Copy link
Copy Markdown
Member

Description

Lets an admin launch otari with almost nothing configured, then add provider API keys, set model pricing, and manage aliases entirely from the web dashboard. No config-file edit or restart required.

On first run with no master key, otari generates one and prints it to the console; the dashboard sign-in screen takes that key. From there the new Providers page can add credentials, which are encrypted at rest and merged into the live provider config by a background refresher.

Backend

  • Encryption at rest. services/secret_box.py encrypts stored keys with OTARI_SECRET_KEY (Fernet). Keys are write-only over the API; responses expose only last4. Undecryptable rows (for example after the secret key changes) are flagged rather than crashing.
  • Provider store. services/provider_store_service.py persists credentials and overlays them onto config.providers through a TTL refresher (standalone only), mirroring the existing alias refresher. The per-config baseline lives on config._provider_baseline.
  • First-run bootstrap. services/master_key_service.py generates and prints a master key when none is set; the hash is stored in runtime_settings.
  • API. /v1/provider-credentials CRUD, a per-instance test endpoint and an ad-hoc test-before-save endpoint, and a providers catalog endpoint. All routes are master-key gated and standalone-only (not mounted in hybrid mode).
  • Migration. New Alembic revision for the provider_credentials table.
  • Bug fix. /v1/models returned a 500 when an orphaned pricing key referenced a provider that had been removed. normalize_pricing_key now tolerates AnyLLMError, so the stale key is skipped instead of failing the request.

Dashboard

  • Providers page. Two-tab add form (known vs custom), autopopulation of provider type and base URL, test-before-save, and a clear flag when a stored key can no longer be decrypted.
  • Onboarding. A first-run panel routes a fresh install to Providers with a short walkthrough.
  • Aliases moved to their own page. "Make an alias" from a model detail navigates over with the target prefilled.
  • Models page reworked around browsing plus inline pricing. Removed the add form and the Released and Features columns. Price "Reset" now has a tooltip explaining it restores defaults.
  • Routing. Adopted react-router-dom (HashRouter) in place of hardcoded section switching.
  • Layout polish. Header with logo and sign-out, main-pane-only scroll, update prompt aligned under the header, a subtle otari.ai link, and CSS-masked secret fields.

Notes for reviewers

  • The Postgres integration tests (test_provider_store.py, test_provider_credentials_api.py) run in CI under Docker; locally I exercised the same behavior against SQLite.
  • Secret fields use -webkit-text-security for masking, so the key renders unmasked on Firefox (masked on Chromium and Safari). Called out in case we want a JS fallback later.
  • Routing uses HashRouter deliberately, to avoid a server catch-all shadowing /v1. Happy to switch to BrowserRouter with clean URLs if preferred.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used: Claude Code (Opus 4.8)

Any additional AI details you'd like to share:

Implemented by Claude Code through back-and-forth with @njbrake. The reasoning and design decisions are his; the code and prose are Claude's.

  • I am an AI Agent filling out this form (check box if true)

…dashboard

Let an admin launch otari with almost nothing set, then add provider API
keys, set pricing, and manage aliases from the web dashboard.

Backend:
- Encrypt stored provider keys at rest with OTARI_SECRET_KEY (Fernet) via
  services/secret_box.py; keys are write-only over the API (responses expose
  only last4).
- services/provider_store_service.py persists provider credentials and
  overlays them onto config.providers through a TTL refresher (standalone
  only), mirroring the alias refresher; baseline kept on
  config._provider_baseline.
- services/master_key_service.py generates and prints a master key on first
  run when none is set; hash stored in runtime_settings.
- /v1/provider-credentials CRUD plus per-instance and ad-hoc test endpoints,
  and a providers catalog endpoint.
- Alembic migration for the provider_credentials table.
- Fix a 500 on /v1/models when an orphaned pricing key referenced a removed
  provider (normalize_pricing_key now tolerates AnyLLMError).

Dashboard:
- New Providers page (two-tab known/custom add form, test-before-save,
  flags undecryptable keys) and a first-run onboarding panel.
- Aliases moved to their own page; "Make an alias" from a model detail
  prefills the target.
- Models page reworked around browsing plus inline pricing; removed the add
  form and the Released and Features columns.
- Adopt react-router-dom (HashRouter) instead of hardcoded section paths.
- Header with logo and sign-out, main-pane-only scroll, aligned update
  prompt, subtle otari.ai link, and CSS-masked secret fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 17, 2026 18:25 — with GitHub Actions Inactive
@github-actions github-actions Bot added the missing-template PR is missing required template sections label Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@njbrake, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb42c3bd-efd2-41e0-ba51-12ef67204a4e

📥 Commits

Reviewing files that changed from the base of the PR and between adc4669 and a44ee67.

⛔ Files ignored due to path filters (1)
  • docs/public/openapi.json is excluded by !docs/public/openapi.json
📒 Files selected for processing (11)
  • README.md
  • docs/public/otari.postman_collection.json
  • src/gateway/api/routes/providers.py
  • src/gateway/services/model_discovery_service.py
  • src/gateway/services/provider_store_service.py
  • src/gateway/static/dashboard/assets/index-DjR2NrLT.js
  • src/gateway/static/dashboard/index.html
  • tests/integration/test_provider_store.py
  • web/src/App.tsx
  • web/src/pages/ModelsPage.tsx
  • web/src/pages/ProvidersPage.tsx

Walkthrough

The PR adds encrypted runtime provider management across the gateway and dashboard, including persistence, master-key bootstrap, provider overlays, CRUD and connection-testing APIs, hash-based routing, provider onboarding, alias management, and updated documentation and bundled assets.

Changes

Runtime provider management

Layer / File(s) Summary
Credential storage and key security
alembic/..., src/gateway/services/..., src/gateway/models/...
Adds encrypted provider credentials, generated master-key persistence, hashed authentication, and Fernet key rotation support.
Provider overlay and management API
src/gateway/services/provider_store_service.py, src/gateway/api/routes/providers.py, tests/integration/*
Adds cached config overlays, provider catalog and credential CRUD/test endpoints, redacted responses, concurrency checks, and integration coverage.
Pricing and provider compatibility
src/gateway/services/pricing_service.py, src/gateway/services/provider_kwargs.py, src/gateway/core/config.py
Centralizes missing-pricing responses and adds provider aliases and resilient pricing-key normalization.
Dashboard routing and provider workflows
web/src/App.tsx, web/src/components/*, web/src/pages/*, web/src/api/*
Adds HashRouter navigation, provider onboarding and editing, aliases management, inline model pricing, and API hooks/types.
Documentation and committed dashboard assets
README.md, docs/*, AGENTS.md, src/gateway/static/dashboard/*
Updates setup guidance, runtime provider documentation, API examples, and bundled dashboard references.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: khaledosman

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The title is relevant, but it uses feat(ui): instead of the պահանջed Conventional Commit prefix and runs longer than the stated ~70 characters. Change it to a valid Conventional Commit title like feat: manage providers, pricing, and aliases in the dashboard.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description follows the required template with all major sections present and filled out, including checklist and AI usage details.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ui-2
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch ui-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from khaledosman July 17, 2026 18:27
@github-actions github-actions Bot removed the missing-template PR is missing required template sections label Jul 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 158-163: Update the fenced block containing the Otari first-run
output to specify the text language, using a text fence while preserving its
existing contents.

In `@src/gateway/api/routes/providers.py`:
- Around line 235-250: Extend _validate_instance to validate api_base and any
URL-bearing client_args with the repository’s existing outbound URL/SSRF
validator, rejecting invalid endpoints before provider calls. Invoke this
validation for both ad-hoc test flows and persisted write flows referenced by
the affected sections, reusing the shared validator rather than introducing new
SSRF logic.
- Around line 364-380: Make the optimistic concurrency handling in the PATCH
flow around save_credential atomic: perform the update using instance and the
expected updated_at as conditions, or lock the existing row within the
transaction before saving. Detect when the conditional update affects no row and
raise HTTPException with status 412, while preserving normal saves when the
version still matches.
- Around line 204-214: Update UpdateStoredProviderRequest handling and the
corresponding save_credential call to distinguish omitted fields from explicitly
provided null values using request.model_fields_set or an equivalent sentinel.
Preserve omitted fields as unchanged, while passing explicit null so
provider_type, api_base, client_args, and api_key can be cleared or removed as
requested.

In `@src/gateway/services/model_discovery_service.py`:
- Around line 227-229: Update the exception handling in the provider connection
test to stop using raw exception text: log only impl_name and the exception
class, and return a stable public error message instead of _short_error(exc).
Preserve the existing ProviderDiscovery structure and empty models result.

In `@web/src/App.tsx`:
- Around line 40-41: Update the provider query failure handling around the
isSuccess check in App so an isError result no longer returns null indefinitely.
Render the existing error/retry state if available, or redirect to /providers,
while preserving the current loading and successful provider-rendering behavior.

In `@web/src/components/Login.tsx`:
- Around line 71-79: Update the Login component’s “First run? Where to find your
key” help text to remove instructions for retrieving the master key from server
logs, including the “Your master key” and docker logs guidance. Direct operators
to the approved permission-restricted key location or explicit setup flow
instead, without exposing or logging the secret.

In `@web/src/pages/ModelsPage.tsx`:
- Around line 388-391: Update InfoTooltip to generate its DOM id with React
useId() instead of the fixed "pricing-info-tip" value, ensuring every tooltip
instance has a unique id while preserving its aria-describedby association and
existing label behavior.
- Line 772: Update the pricing error display in the ModelsPage editor to render
failures from both setPricing and deletePricing, ensuring reset errors provide
the same feedback as save errors. Reuse the existing errorMessage helper and
preserve the current styling and conditional rendering behavior.

In `@web/src/pages/ProvidersPage.tsx`:
- Line 684: Update the ErrorBanner invocation in ProvidersPage to include errors
from settings, updateSettings, and deleteProvider in addition to the existing
meta.error and stored.error fallbacks. Preserve the current precedence while
ensuring failures from all provider management actions are surfaced.
- Around line 650-659: Update runTest to use testProvider.mutateAsync with
row-local async error handling, ensuring each invocation independently updates
its instance through success or failure. Preserve the existing pending, done,
result, and error state shapes while preventing later tests from replacing
earlier completion handlers.
- Around line 120-127: Update the onSelectionChange handler to handle null keys
by clearing the parent provider value and selectedKey, rather than ignoring
deselection. Preserve the existing provider lookup and text update for non-null
keys so the UI and submitted providerType remain synchronized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 96967236-43f5-4213-bb05-03be5ac098fe

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3542b and adc4669.

⛔ Files ignored due to path filters (3)
  • docs/public/openapi.json is excluded by !docs/public/openapi.json
  • uv.lock is excluded by !**/*.lock, !**/uv.lock
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (46)
  • AGENTS.md
  • README.md
  • alembic/versions/b4d6f8a0c2e4_add_provider_credentials.py
  • docs/configuration.md
  • docs/public/otari.postman_collection.json
  • pyproject.toml
  • src/gateway/api/deps.py
  • src/gateway/api/routes/_passthrough.py
  • src/gateway/api/routes/_pipeline.py
  • src/gateway/api/routes/providers.py
  • src/gateway/cli.py
  • src/gateway/core/config.py
  • src/gateway/main.py
  • src/gateway/models/entities.py
  • src/gateway/services/master_key_service.py
  • src/gateway/services/model_discovery_service.py
  • src/gateway/services/pricing_service.py
  • src/gateway/services/provider_kwargs.py
  • src/gateway/services/provider_metadata_service.py
  • src/gateway/services/provider_store_service.py
  • src/gateway/services/secret_box.py
  • src/gateway/static/dashboard/assets/index-8efccAl2.css
  • src/gateway/static/dashboard/assets/index-BiTSmG14.js
  • src/gateway/static/dashboard/assets/index-BmfhJJsH.css
  • src/gateway/static/dashboard/assets/index-C8ycIJF3.js
  • src/gateway/static/dashboard/index.html
  • tests/integration/test_provider_credentials_api.py
  • tests/integration/test_provider_store.py
  • tests/unit/test_gateway_cli.py
  • tests/unit/test_master_key_service.py
  • tests/unit/test_provider_instances.py
  • tests/unit/test_provider_store_service.py
  • tests/unit/test_secret_box.py
  • web/package.json
  • web/src/App.tsx
  • web/src/api/hooks.ts
  • web/src/api/types.ts
  • web/src/components/AppShell.tsx
  • web/src/components/Login.tsx
  • web/src/components/UpdatePrompt.tsx
  • web/src/pages/AliasesPage.test.tsx
  • web/src/pages/AliasesPage.tsx
  • web/src/pages/ModelsPage.test.tsx
  • web/src/pages/ModelsPage.tsx
  • web/src/pages/ProvidersPage.test.tsx
  • web/src/pages/ProvidersPage.tsx

Comment thread README.md Outdated
Comment thread src/gateway/api/routes/providers.py
Comment thread src/gateway/api/routes/providers.py
Comment thread src/gateway/api/routes/providers.py
Comment thread src/gateway/services/model_discovery_service.py
Comment thread web/src/pages/ModelsPage.tsx
Comment thread web/src/pages/ModelsPage.tsx Outdated
Comment thread web/src/pages/ProvidersPage.tsx
Comment thread web/src/pages/ProvidersPage.tsx
Comment thread web/src/pages/ProvidersPage.tsx Outdated
- providers: distinguish an omitted field from an explicit null on PATCH
  (model_fields_set plus a tri-state sentinel in save_credential), so
  api_base, provider_type, client_args, and the key can each be cleared or
  left untouched
- providers: lock the row FOR UPDATE on PATCH so the optimistic-concurrency
  check and the write it guards are atomic on PostgreSQL
- discovery: log only the exception class on a failed model listing or
  connection test, since some providers echo a partial key in the message;
  the capped message still reaches the master-key caller in the response
- dashboard: never strand the index route on a blank screen when the
  providers query fails, fall back to Providers
- dashboard: surface settings/update/delete failures on Providers and reset
  failures on the inline price form
- dashboard: give each pricing tooltip a unique id (useId) and clear the
  provider combobox value when the selection is cleared
- docs: add a language to a README fenced block

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 17, 2026 18:51 — with GitHub Actions Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant