Skip to content

feat(ai): AI gateway integration with custom provider support #1072

Open
ddecrulle wants to merge 9 commits into
mainfrom
ia-integration
Open

feat(ai): AI gateway integration with custom provider support #1072
ddecrulle wants to merge 9 commits into
mainfrom
ia-integration

Conversation

@ddecrulle

@ddecrulle ddecrulle commented May 5, 2026

Copy link
Copy Markdown
Member
  • AI gateway port & adapter — new Ai port (OIDC token exchange, model listing) with an OpenWebUI adapter and a mock; config comes from the deployment region
    (region.ai)
  • ai usecase — initialize lifecycle (pending/success/no-account/error), token refresh, model selection; selected model is injected into Helm values when launching a
    service
  • "AI" tab in account settings — displays gateway credentials (API base URL, token, model selector), manages custom AI providers (OpenAI-compatible endpoints) stored in
    localStorage
  • "Test connection" button — validates a custom provider before saving (fetches /models), shows success with model count or an error message
  • Simplified provider card — no longer displays apiBase/apiKey after saving, only the provider name and model selector
  • i18n — 9 languages (en, fr, de, es, fi, it, nl, no, zh-CN)
image

Summary by CodeRabbit

  • New Features
    • Added an AI account tab with an AI Gateway credentials UI (OIDC) plus support for region-based providers and custom OpenAI-compatible providers.
    • Enabled region-based AI discovery when AI is turned on, including token handling and per-provider model listing with model selection.
    • Added custom provider management (add/edit/delete), connection testing, and persisted provider/model choices across sessions.
    • Added an ENABLED_AI environment toggle to show or hide the AI tab.
  • Documentation
    • Added repository guidance for AI/code contributor workflows and architecture conventions.
  • Tests
    • Added coverage for persisted AI configuration parsing/serialization.

@sonarqubecloud

sonarqubecloud Bot commented May 5, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1ba6e46-2603-43b2-9dc9-b0683fb5ec53

📥 Commits

Reviewing files that changed from the base of the PR and between cd0257b and d8b2870.

📒 Files selected for processing (6)
  • web/.env
  • web/src/core/bootstrap.ts
  • web/src/core/usecases/ai/thunks.ts
  • web/src/env.ts
  • web/src/ui/App/App.tsx
  • web/src/vite-env.d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/core/bootstrap.ts

📝 Walkthrough

Walkthrough

Adds AI gateway integration across core contracts, adapters, persisted config, state, bootstrap wiring, account UI, and translations. It introduces persisted AI configuration, provider/model handling, OIDC token exchange, and a new account tab for configuring and using AI providers.

Changes

AI Gateway Integration

Layer / File(s) Summary
Developer guide
web/CLAUDE.md
Adds repository guidance for commands, architecture, layering, and key library roles.
Ports and API shapes
web/src/core/ports/Ai.ts, web/src/core/ports/OnyxiaApi/DeploymentRegion.ts, web/src/core/ports/OnyxiaApi/XOnyxia.ts, web/src/core/adapters/onyxiaApi/ApiTypes.ts
Extends AI, deployment region, XOnyxia, and public configuration types with AI provider metadata and model objects.
OIDC, token exchange, and AI adapters
web/src/core/tools/oidcTokenExchange.ts, web/src/core/adapters/ai/*, web/src/core/adapters/oidc/oidc.ts, web/src/core/tools/fetchAiModels.ts
Adds token exchange, OpenWebUI AI model fetching, the matching mock adapter, barrel export, and optional DPoP disabling for OIDC clients.
Persisted AI config and usecase state
web/src/core/usecases/ai/decoupledLogic/persistedAiConfig.ts, web/src/core/usecases/ai/state.ts, web/src/core/usecases/userConfigs.ts
Defines persisted AI config parsing/serialization, then reshapes AI state and reducers around provider initialization, auth, models, selection, and custom provider CRUD.
AI selectors and thunks
web/src/core/usecases/ai/selectors.ts, web/src/core/usecases/ai/thunks.ts, web/src/core/usecases/ai/index.ts
Builds selectors, persists selection state, initializes providers from region config and saved config, fetches model catalogs, and exposes the AI usecase barrel.
Core bootstrap and wiring
web/src/core/bootstrap.ts, web/src/core/adapters/onyxiaApi/onyxiaApi.ts, web/src/core/usecases/index.ts, web/src/core/usecases/launcher/thunks.ts, web/src/env.ts, web/src/vite-env.d.ts, web/.env, web/src/ui/App/App.tsx
Maps region AI config from Onyxia API, adds AI context bootstrap, registers the AI usecase, injects AI into launcher context, and wires AI config storage and enablement through environment parsing and app bootstrap.
Account AI tab
web/src/ui/pages/account/AccountAiTab.tsx, web/src/ui/pages/account/Page.tsx, web/src/ui/pages/account/accountTabIds.ts, web/src/ui/i18n/types.ts
Adds the account AI tab component, routes it into the account page, extends tab ids and i18n component keys, and renders provider cards, model selection, and custom provider dialogs.
Internationalization
web/src/ui/i18n/resources/{de,en,es,fi,fr,it,nl,no,zh-CN}.tsx
Adds the AI account label and AI gateway tab translations across nine languages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

A bunny hopped through adapters bright,
With OIDC tokens and models in sight 🐇
Region and custom providers joined the dance,
In nine tongues, the AI tab found its stance.
Little paws tapped save, select, and test—
This gateway journey feels nicely blessed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: AI gateway integration with custom provider support.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ia-integration
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch ia-integration

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 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
web/src/core/ports/Ai.ts (1)

8-11: ⚡ Quick win

error variant carries no diagnostic payload — error context is lost

The { status: "error" } branch gives thunks and UI no information about what went wrong (network failure, unexpected HTTP status, etc.), making it hard to display a meaningful error message. Even a minimal reason?: string or httpStatus?: number would allow the UI to differentiate transient failures from configuration errors.

♻️ Proposed extension
 export type GetTokenResult =
     | { status: "success"; token: string }
     | { status: "no-account" }
-    | { status: "error" };
+    | { status: "error"; reason?: string };
🤖 Prompt for 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.

In `@web/src/core/ports/Ai.ts` around lines 8 - 11, The GetTokenResult union
currently has an { status: "error" } variant with no diagnostic data; update the
type so the "error" branch carries minimal payload (e.g., reason?: string and
httpStatus?: number or similar fields) so callers can surface meaningful
messages; change the union definition for GetTokenResult to include those
optional fields and then update all places that construct or pattern-match on
GetTokenResult (e.g., any code creating a { status: "error" } result or
switching on GetTokenResult.status) to populate and consume the new fields.
web/src/core/ports/OnyxiaApi/XOnyxia.ts (1)

196-203: 💤 Low value

enabled: true discriminant is redundant given | undefined

Since ai is typed as { enabled: true; ... } | undefined, the enabled: true literal adds no information — an AI block being present already implies it is enabled. Chart templates that check ai.enabled could equivalently check ai != null. Consider dropping the enabled field to keep the shape consistent with how vault and s3 are modelled in the same type.

♻️ Proposed simplification
     ai:
         | {
-              enabled: true;
               token: string;
               apiBase: string;
               model: string;
           }
         | undefined;
🤖 Prompt for 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.

In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts` around lines 196 - 203, Remove the
redundant enabled: true literal from the ai union type in XOnyxia (i.e., change
the ai type from "{ enabled: true; token: string; apiBase: string; model:
string; } | undefined" to "{ token: string; apiBase: string; model: string; } |
undefined") and update any runtime checks that test ai.enabled to instead test
for ai != null (or Boolean(ai)), ensuring places referencing the removed enabled
property (e.g., code paths that read ai.enabled) are adjusted to treat
presence/absence of the ai object as the enablement signal.
web/src/core/adapters/ai/openWebUi.ts (1)

40-43: ⚡ Quick win

Unguarded cast on data.data may throw a TypeError at runtime.

If the gateway returns a response where data.data is undefined, null, or not an array, .map() will throw, which callers don't expect — listModels should either validate or provide a fallback.

🛡️ Proposed defensive guard
 const data = await response.json();

-return (data.data as { id: string }[]).map(m => m.id);
+const entries: unknown[] = Array.isArray(data.data) ? data.data : [];
+return (entries as { id: string }[]).map(m => m.id);
🤖 Prompt for 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.

In `@web/src/core/adapters/ai/openWebUi.ts` around lines 40 - 43, The unguarded
cast in listModels reads response.json() into data and assumes data.data is an
array, which can throw when it's undefined/null/not-an-array; update the
listModels implementation to validate that data && Array.isArray(data.data)
before mapping, and if not return an empty array (or a safe fallback). When
mapping, defensively extract ids (e.g., filter items with an id property and
coerce to string) so the method always returns string[] without throwing.
web/src/core/usecases/ai/thunks.ts (1)

145-150: 🏗️ Heavy lift

testCustomProvider returns a Promise<string[]> which violates the reactive thunk pattern.

Per the coding guidelines for web/src/core/**/*.ts: "Use observable state with thunks (reactive pattern) rather than promise-based returns; dispatch actions and read state instead of returning values from thunks."

The test result (model list or error) should be stored in state via new actions (e.g., providerTestStarted, providerTestSucceeded({ models }), providerTestFailed) and exposed via a selector, with the UI subscribing to state instead of awaiting the thunk.

🤖 Prompt for 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.

In `@web/src/core/usecases/ai/thunks.ts` around lines 145 - 150,
testCustomProvider currently returns a Promise<string[]> which breaks the
project's reactive thunk pattern; change it to dispatch actions and update state
instead of returning values. Replace the direct return of fetchModels(apiBase,
apiKey) in testCustomProvider with dispatch(providerTestStarted()), call
fetchModels, then on success dispatch(providerTestSucceeded({ models })) and on
error dispatch(providerTestFailed({ error })); update the reducer/state to store
test status and models and expose them via a selector for the UI to subscribe
to, and make the thunk return void (or Promise<void>) rather than
Promise<string[]>; reference the existing testCustomProvider thunk and
fetchModels helper when applying these changes.
🤖 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 `@web/src/core/usecases/ai/selectors.ts`:
- Around line 16-26: Replace the hard assertion in the "no-account" branch so
the selector won't throw when region.ai is missing: remove the assert on
region.ai in the branch where initializationStatus === "no-account" and return
isEnabled: false and initializationStatus as before but only attach webUiUrl
when region.ai exists (e.g., use a safe optional access or conditional property)
so the selector returns a safe fallback instead of throwing; look for symbols
initializationStatus, region.ai and the returned webUiUrl in this function to
update.

In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 190-193: The call to ai.listModels(token) can throw and leaves the
initializeStart/“pending” state unresolved; wrap the ai.listModels(token)
invocation in a try/catch around the block that follows initializeStart and, on
any error, dispatch initializeFailed with the error (or a normalized message) so
the thunk resolves correctly; reference tokenResult, ai.listModels(token),
initializeStart, and initializeFailed when locating where to add the try/catch
and the error dispatch.

In `@web/src/core/usecases/launcher/thunks.ts`:
- Around line 777-790: The current ai block builds an ai object even when
aiState.selectedModel is undefined (using selectedModel ?? ""), which injects an
empty model into the XOnyxiaContext; update the anonymous IIFE in the ai
property (the code using aiUsecase.selectors.main(getState()) and aiState) to
return undefined unless aiState.isEnabled, aiState.token are present AND
aiState.selectedModel is a non-empty value (e.g., check aiState.selectedModel !=
null && aiState.selectedModel !== ""), otherwise return undefined so no ai key
is written into the context.

In `@web/src/ui/i18n/resources/no.tsx`:
- Around line 126-127: Update the localized helper string for the translation
key "custom providers section helper" to include the user-facing disclosure that
credentials (API keys) are stored locally in the browser; locate the string for
"custom providers section helper" in web/src/ui/i18n/resources/no.tsx and modify
its value to append a short sentence making the storage disclosure (e.g.,
"API-nøkler lagres i nettleseren"), keeping the rest of the guidance about base
URL and API key unchanged.

In `@web/src/ui/pages/account/AccountAiTab.tsx`:
- Around line 100-112: Wrap the ai.addCustomProvider call inside onSaveProvider
in a try/catch so failures are handled: call ai.addCustomProvider(...) in the
try block and only clear the form (setAddFormOpen(false), setPendingLabel(""),
setPendingApiBase(""), setPendingApiKey(""), setTestStatus("idle"),
setTestModelCount(0)) on success; in catch set a failure state (e.g.,
setTestStatus("error") and optionally set an error message state) and keep the
add form open so the user can correct inputs; ensure the catch re-enables any UI
affordances disabled during save and does not leave the promise rejection
unhandled.

In `@web/src/ui/pages/account/Page.tsx`:
- Line 56: The ternary in the .filter callback for accountTabId is redundant;
replace the ternary that returns true when accountTabId !== "ai" else
ai.isAvailable() with a logical OR of the inequality check and ai.isAvailable()
(i.e., test whether accountTabId is not "ai" OR ai.isAvailable()) in the Page
component where accountTabId and ai.isAvailable() are used, and apply the same
simplification to the sibling .filter calls that use the same pattern.

---

Nitpick comments:
In `@web/src/core/adapters/ai/openWebUi.ts`:
- Around line 40-43: The unguarded cast in listModels reads response.json() into
data and assumes data.data is an array, which can throw when it's
undefined/null/not-an-array; update the listModels implementation to validate
that data && Array.isArray(data.data) before mapping, and if not return an empty
array (or a safe fallback). When mapping, defensively extract ids (e.g., filter
items with an id property and coerce to string) so the method always returns
string[] without throwing.

In `@web/src/core/ports/Ai.ts`:
- Around line 8-11: The GetTokenResult union currently has an { status: "error"
} variant with no diagnostic data; update the type so the "error" branch carries
minimal payload (e.g., reason?: string and httpStatus?: number or similar
fields) so callers can surface meaningful messages; change the union definition
for GetTokenResult to include those optional fields and then update all places
that construct or pattern-match on GetTokenResult (e.g., any code creating a {
status: "error" } result or switching on GetTokenResult.status) to populate and
consume the new fields.

In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts`:
- Around line 196-203: Remove the redundant enabled: true literal from the ai
union type in XOnyxia (i.e., change the ai type from "{ enabled: true; token:
string; apiBase: string; model: string; } | undefined" to "{ token: string;
apiBase: string; model: string; } | undefined") and update any runtime checks
that test ai.enabled to instead test for ai != null (or Boolean(ai)), ensuring
places referencing the removed enabled property (e.g., code paths that read
ai.enabled) are adjusted to treat presence/absence of the ai object as the
enablement signal.

In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 145-150: testCustomProvider currently returns a Promise<string[]>
which breaks the project's reactive thunk pattern; change it to dispatch actions
and update state instead of returning values. Replace the direct return of
fetchModels(apiBase, apiKey) in testCustomProvider with
dispatch(providerTestStarted()), call fetchModels, then on success
dispatch(providerTestSucceeded({ models })) and on error
dispatch(providerTestFailed({ error })); update the reducer/state to store test
status and models and expose them via a selector for the UI to subscribe to, and
make the thunk return void (or Promise<void>) rather than Promise<string[]>;
reference the existing testCustomProvider thunk and fetchModels helper when
applying these changes.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2cb9147a-b5ad-46ea-be5c-b123ddd634a2

📥 Commits

Reviewing files that changed from the base of the PR and between d092838 and 38ba553.

📒 Files selected for processing (30)
  • web/CLAUDE.md
  • web/src/core/adapters/ai/index.ts
  • web/src/core/adapters/ai/mock.ts
  • web/src/core/adapters/ai/openWebUi.ts
  • web/src/core/adapters/onyxiaApi/ApiTypes.ts
  • web/src/core/adapters/onyxiaApi/onyxiaApi.ts
  • web/src/core/bootstrap.ts
  • web/src/core/ports/Ai.ts
  • web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
  • web/src/core/ports/OnyxiaApi/XOnyxia.ts
  • web/src/core/tools/oidcTokenExchange.ts
  • web/src/core/usecases/ai/index.ts
  • web/src/core/usecases/ai/selectors.ts
  • web/src/core/usecases/ai/state.ts
  • web/src/core/usecases/ai/thunks.ts
  • web/src/core/usecases/index.ts
  • web/src/core/usecases/launcher/thunks.ts
  • web/src/ui/i18n/resources/de.tsx
  • web/src/ui/i18n/resources/en.tsx
  • web/src/ui/i18n/resources/es.tsx
  • web/src/ui/i18n/resources/fi.tsx
  • web/src/ui/i18n/resources/fr.tsx
  • web/src/ui/i18n/resources/it.tsx
  • web/src/ui/i18n/resources/nl.tsx
  • web/src/ui/i18n/resources/no.tsx
  • web/src/ui/i18n/resources/zh-CN.tsx
  • web/src/ui/i18n/types.ts
  • web/src/ui/pages/account/AccountAiTab.tsx
  • web/src/ui/pages/account/Page.tsx
  • web/src/ui/pages/account/accountTabIds.ts

Comment thread web/src/core/usecases/ai/selectors.ts Outdated
Comment thread web/src/core/usecases/ai/thunks.ts Outdated
Comment thread web/src/core/usecases/launcher/thunks.ts Outdated
Comment thread web/src/ui/i18n/resources/no.tsx Outdated
Comment on lines +100 to +112
const onSaveProvider = useConstCallback(async () => {
await ai.addCustomProvider({
label: pendingLabel,
apiBase: pendingApiBase,
apiKey: pendingApiKey
});
setAddFormOpen(false);
setPendingLabel("");
setPendingApiBase("");
setPendingApiKey("");
setTestStatus("idle");
setTestModelCount(0);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle add-provider failures in onSaveProvider.

If ai.addCustomProvider fails at Line 101, the rejection is unhandled and the UI gives no clear recovery path. Catch the error and keep the form in a controlled error state.

Proposed fix
     const onSaveProvider = useConstCallback(async () => {
-        await ai.addCustomProvider({
-            label: pendingLabel,
-            apiBase: pendingApiBase,
-            apiKey: pendingApiKey
-        });
+        try {
+            await ai.addCustomProvider({
+                label: pendingLabel,
+                apiBase: pendingApiBase,
+                apiKey: pendingApiKey
+            });
+        } catch {
+            setTestStatus("error");
+            return;
+        }
         setAddFormOpen(false);
         setPendingLabel("");
         setPendingApiBase("");
         setPendingApiKey("");
         setTestStatus("idle");
         setTestModelCount(0);
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onSaveProvider = useConstCallback(async () => {
await ai.addCustomProvider({
label: pendingLabel,
apiBase: pendingApiBase,
apiKey: pendingApiKey
});
setAddFormOpen(false);
setPendingLabel("");
setPendingApiBase("");
setPendingApiKey("");
setTestStatus("idle");
setTestModelCount(0);
});
const onSaveProvider = useConstCallback(async () => {
try {
await ai.addCustomProvider({
label: pendingLabel,
apiBase: pendingApiBase,
apiKey: pendingApiKey
});
} catch {
setTestStatus("error");
return;
}
setAddFormOpen(false);
setPendingLabel("");
setPendingApiBase("");
setPendingApiKey("");
setTestStatus("idle");
setTestModelCount(0);
});
🤖 Prompt for 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.

In `@web/src/ui/pages/account/AccountAiTab.tsx` around lines 100 - 112, Wrap the
ai.addCustomProvider call inside onSaveProvider in a try/catch so failures are
handled: call ai.addCustomProvider(...) in the try block and only clear the form
(setAddFormOpen(false), setPendingLabel(""), setPendingApiBase(""),
setPendingApiKey(""), setTestStatus("idle"), setTestModelCount(0)) on success;
in catch set a failure state (e.g., setTestStatus("error") and optionally set an
error message state) and keep the add form open so the user can correct inputs;
ensure the catch re-enables any UI affordances disabled during save and does not
leave the promise rejection unhandled.

.filter(accountTabId =>
accountTabId !== "vault" ? true : vaultCredentials.isAvailable()
)
.filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Simplify the negated ternary condition (SonarCloud warning).

accountTabId !== "ai" ? true : ai.isAvailable() reads as a double-negation and SonarCloud flags it. The direct form is clearer:

♻️ Proposed simplification
-.filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable()))
+.filter(accountTabId => accountTabId !== "ai" || ai.isAvailable())

Note: the same pattern appears in the sibling .filter calls on lines 46, 49–52, and 53–55 — those could be cleaned up consistently in a follow-up.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable()))
.filter(accountTabId => accountTabId !== "ai" || ai.isAvailable())
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 56-56: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia&issues=AZ33YH7OM9gut30udPw5&open=AZ33YH7OM9gut30udPw5&pullRequest=1072

🤖 Prompt for 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.

In `@web/src/ui/pages/account/Page.tsx` at line 56, The ternary in the .filter
callback for accountTabId is redundant; replace the ternary that returns true
when accountTabId !== "ai" else ai.isAvailable() with a logical OR of the
inequality check and ai.isAvailable() (i.e., test whether accountTabId is not
"ai" OR ai.isAvailable()) in the Page component where accountTabId and
ai.isAvailable() are used, and apply the same simplification to the sibling
.filter calls that use the same pattern.

ddecrulle and others added 3 commits June 9, 2026 14:09
- Add AI usecase (state/thunks/selectors) with initializeStart/initializeSucceed/initializeFailed lifecycle actions
- getToken() returns a discriminated result type in the Ai port — no-account (403) vs error cases handled without leaking adapter details into usecases
- Gracefully disable AI features on init failure; show a link to the gateway URL when user has no account
- Add AccountAiGatewayTab with token/model display and full i18n for all 9 languages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
web/src/core/ports/OnyxiaApi/XOnyxia.ts (1)

185-194: ⚡ Quick win

Prefer optional property syntax for ai context.

ai: {...} | undefined forces callers to always include the key. If this field is truly optional, ai?: {...} is the safer contract for existing XOnyxiaContext object construction paths.

Suggested change
-    ai:
-        | {
+    ai?:
+        {
               enabled: boolean;
               apiKey: string;
               apiBase: string;
               model: string;
               provider: string;
               embeddingsModel: string;
           }
-        | undefined;
+        ;
🤖 Prompt for 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.

In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts` around lines 185 - 194, The ai
property in the XOnyxiaContext type uses union syntax with undefined (ai: {...}
| undefined) which requires callers to always provide the key. Replace this with
the optional property syntax (ai?: {...}) to allow the property to be omitted
entirely when constructing XOnyxiaContext objects, making it a safer contract
for existing code paths that build this type.
web/src/core/ports/Ai.ts (1)

1-8: ⚡ Quick win

Use an interface for the port contract in src/core/ports.

This contract should be declared as an interface to match the ports-and-adapters rule for web/src/core/ports/**/*.ts.

Suggested change
-export type Ai = {
+export interface Ai {
     id: string;
     name: string;
     webUiUrl: string;
     apiBase: string;
     getToken: () => Promise<GetTokenResult>;
     listModels: (token: string) => Promise<{ id: string; name: string }[]>;
-};
+}

As per coding guidelines, web/src/core/ports/**/*.ts: “Define TypeScript interfaces for external dependencies in src/core/ports/ using the ports-and-adapters pattern.”

🤖 Prompt for 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.

In `@web/src/core/ports/Ai.ts` around lines 1 - 8, The Ai export in the file
should be declared as an interface instead of a type alias to follow the
ports-and-adapters coding guidelines. Replace the existing type Ai declaration
(which uses the type keyword with an object literal syntax) with an interface
declaration that has the same name and properties, removing the equals sign and
curly brace syntax used for type aliases.

Source: Coding guidelines

🤖 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 `@web/src/core/adapters/ai/mock.ts`:
- Line 10: The apiBase string composition concatenates webUiUrl directly with
`/api`, which creates a double slash (`...//api`) if webUiUrl ends with a
forward slash, potentially breaking strict gateways or proxies. Normalize the
webUiUrl by removing any trailing slash before composing the apiBase string. Use
a method to strip the trailing slash from webUiUrl (such as using replace or
trimEnd with proper regex/string manipulation) so that the final apiBase always
has exactly one slash between the base URL and the `/api` path.

In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 31-42: The Zod schema in the fetchModels function requires a name
field that does not exist in the actual OpenAI API response, causing parse
failures. Modify the schema object definition to either make the name field
optional by adding .optional() to the z.string() for name, or use
.default(model.id) to derive the name from the id field as a fallback. If using
the optional approach, update the mapping logic to handle cases where name may
be undefined. If using the default approach, ensure the default value is applied
appropriately in the schema definition.

In `@web/src/core/usecases/launcher/thunks.ts`:
- Line 772: The activeProvider selector can return a valid AI context object
with model set to an empty string when no model is selected, which causes issues
with AI-enabled Helm charts. At the location where
aiUsecase.selectors.activeProvider is called in the launcher thunks, either add
validation logic in the launcher UI to prevent service launch when modelId is
undefined (enforcing model selection before launch), or modify the
activeProvider selector itself to return undefined instead of a valid context
object when provider.selection.modelId is not set, ensuring downstream code
receives undefined rather than a context with an empty model string.

---

Nitpick comments:
In `@web/src/core/ports/Ai.ts`:
- Around line 1-8: The Ai export in the file should be declared as an interface
instead of a type alias to follow the ports-and-adapters coding guidelines.
Replace the existing type Ai declaration (which uses the type keyword with an
object literal syntax) with an interface declaration that has the same name and
properties, removing the equals sign and curly brace syntax used for type
aliases.

In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts`:
- Around line 185-194: The ai property in the XOnyxiaContext type uses union
syntax with undefined (ai: {...} | undefined) which requires callers to always
provide the key. Replace this with the optional property syntax (ai?: {...}) to
allow the property to be omitted entirely when constructing XOnyxiaContext
objects, making it a safer contract for existing code paths that build this
type.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 374834a3-0605-4579-b5f9-554819d05232

📥 Commits

Reviewing files that changed from the base of the PR and between 38ba553 and 246bd75.

📒 Files selected for processing (32)
  • web/CLAUDE.md
  • web/src/core/adapters/ai/index.ts
  • web/src/core/adapters/ai/mock.ts
  • web/src/core/adapters/ai/openWebUi.ts
  • web/src/core/adapters/onyxiaApi/ApiTypes.ts
  • web/src/core/adapters/onyxiaApi/onyxiaApi.ts
  • web/src/core/bootstrap.ts
  • web/src/core/ports/Ai.ts
  • web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
  • web/src/core/ports/OnyxiaApi/XOnyxia.ts
  • web/src/core/tools/oidcTokenExchange.ts
  • web/src/core/usecases/ai/decoupledLogic/persistedAiConfig.ts
  • web/src/core/usecases/ai/index.ts
  • web/src/core/usecases/ai/selectors.ts
  • web/src/core/usecases/ai/state.ts
  • web/src/core/usecases/ai/thunks.ts
  • web/src/core/usecases/index.ts
  • web/src/core/usecases/launcher/thunks.ts
  • web/src/core/usecases/userConfigs.ts
  • web/src/ui/i18n/resources/de.tsx
  • web/src/ui/i18n/resources/en.tsx
  • web/src/ui/i18n/resources/es.tsx
  • web/src/ui/i18n/resources/fi.tsx
  • web/src/ui/i18n/resources/fr.tsx
  • web/src/ui/i18n/resources/it.tsx
  • web/src/ui/i18n/resources/nl.tsx
  • web/src/ui/i18n/resources/no.tsx
  • web/src/ui/i18n/resources/zh-CN.tsx
  • web/src/ui/i18n/types.ts
  • web/src/ui/pages/account/AccountAiTab.tsx
  • web/src/ui/pages/account/Page.tsx
  • web/src/ui/pages/account/accountTabIds.ts
💤 Files with no reviewable changes (13)
  • web/src/ui/i18n/resources/en.tsx
  • web/src/ui/pages/account/accountTabIds.ts
  • web/src/ui/i18n/resources/fr.tsx
  • web/src/ui/i18n/resources/zh-CN.tsx
  • web/src/ui/i18n/types.ts
  • web/src/ui/i18n/resources/it.tsx
  • web/src/core/usecases/ai/index.ts
  • web/src/ui/i18n/resources/nl.tsx
  • web/src/ui/i18n/resources/no.tsx
  • web/src/ui/i18n/resources/es.tsx
  • web/src/ui/pages/account/Page.tsx
  • web/src/ui/i18n/resources/fi.tsx
  • web/src/ui/pages/account/AccountAiTab.tsx
✅ Files skipped from review due to trivial changes (1)
  • web/CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • web/src/core/adapters/ai/index.ts
  • web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
  • web/src/core/usecases/index.ts
  • web/src/core/adapters/onyxiaApi/ApiTypes.ts
  • web/src/ui/i18n/resources/de.tsx
  • web/src/core/tools/oidcTokenExchange.ts
  • web/src/core/adapters/ai/openWebUi.ts

id,
name,
webUiUrl,
apiBase: `${webUiUrl}/api`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize webUiUrl before composing apiBase.

If webUiUrl ends with /, this produces a double slash (...//api), which can break strict gateways/proxies.

Suggested change
 export function createAi(params: { id: string; name: string; webUiUrl: string }): Ai {
     const { id, name, webUiUrl } = params;
+    const normalizedWebUiUrl = webUiUrl.replace(/\/+$/, "");

     return {
         id,
         name,
         webUiUrl,
-        apiBase: `${webUiUrl}/api`,
+        apiBase: `${normalizedWebUiUrl}/api`,
         getToken: async () => ({ status: "success" as const, token: "mock-ai-token" }),
         listModels: async () => [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
apiBase: `${webUiUrl}/api`,
export function createAi(params: { id: string; name: string; webUiUrl: string }): Ai {
const { id, name, webUiUrl } = params;
const normalizedWebUiUrl = webUiUrl.replace(/\/+$/, "");
return {
id,
name,
webUiUrl,
apiBase: `${normalizedWebUiUrl}/api`,
getToken: async () => ({ status: "success" as const, token: "mock-ai-token" }),
listModels: async () => [
🤖 Prompt for 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.

In `@web/src/core/adapters/ai/mock.ts` at line 10, The apiBase string composition
concatenates webUiUrl directly with `/api`, which creates a double slash
(`...//api`) if webUiUrl ends with a forward slash, potentially breaking strict
gateways or proxies. Normalize the webUiUrl by removing any trailing slash
before composing the apiBase string. Use a method to strip the trailing slash
from webUiUrl (such as using replace or trimEnd with proper regex/string
manipulation) so that the final apiBase always has exactly one slash between the
base URL and the `/api` path.

Comment thread web/src/core/usecases/ai/thunks.ts Outdated
Comment thread web/src/core/usecases/launcher/thunks.ts
ddecrulle and others added 3 commits June 19, 2026 15:35
The access token is handed over to OpenWebUI's token exchange endpoint,
which validates it server-side and cannot present a DPoP proof. It must
therefore be a plain bearer token, never sender-constrained, even when
DPoP is globally enabled.

Forward oidc-spa's `disableDPoP` flag through the createOidc wrapper and
set it on the dedicated AI OIDC client instance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the embeddings model picker across all layers: UI select row and
i18n key, usecase state/thunk/selector, persisted config schema, and the
`embeddingsModel` field of the XOnyxia templating context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/core/usecases/ai/state.ts (1)

140-163: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

editCustomProvider keeps a possibly-stale selectedModelId after credential change.

You reset models to fetching but leave selectedModelId untouched. When the refetched catalog no longer contains the old selectedModelId, modelsLoaded won't replace it (it only defaults when selectedModelId === undefined), so selectors.activeProvider can inject a model id that the new endpoint doesn't expose.

Consider clearing the selection here so the first available model is re-defaulted on load:

🔧 Proposed fix
             provider.label = payload.label;
             provider.apiBase = payload.apiBase;
             provider.apiKey = payload.apiKey;
             // Credentials changed → the previous models list no longer applies.
             provider.models = { stateDescription: "fetching" };
+            // Drop the stale selection so the new catalog re-defaults it.
+            provider.selectedModelId = undefined;
🤖 Prompt for 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.

In `@web/src/core/usecases/ai/state.ts` around lines 140 - 163, editCustomProvider
currently resets provider.models to fetching after credentials change, but it
leaves provider.selectedModelId behind, which can keep a stale model selection
and break selectors.activeProvider. Update editCustomProvider in state.ts to
also clear the selected model when apiBase/apiKey/label change, so modelsLoaded
can re-default to the first available model after the refetch. Use the existing
editCustomProvider and modelsLoaded flow to locate the fix.
♻️ Duplicate comments (1)
web/src/ui/pages/account/AccountAiTab.tsx (1)

146-153: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

onSave swallows add/edit failures.

The dialog is closed before awaiting ai.addCustomProvider / ai.editCustomProvider, and neither call is wrapped, so a rejection (e.g. persistConfig throwing) becomes an unhandled rejection with no user feedback. This mirrors the earlier concern; the close-first ordering also means a failure can't surface in the form anymore.

🧹 Nitpick comments (1)
web/src/ui/pages/account/AccountAiTab.tsx (1)

28-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Re-export core model types instead of redefining them.

AiModel/Models here duplicate State.AiModel/State.Models from core/usecases/ai/state. Since the main selector already returns provider.models typed as the core shape, importing those types keeps the UI in lockstep and avoids silent drift if the core union changes.

🤖 Prompt for 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.

In `@web/src/ui/pages/account/AccountAiTab.tsx` around lines 28 - 34,
`AccountAiTab` is duplicating the AI model state types instead of using the
shared core definitions. Update the `AiModel` and `Models` references in
`AccountAiTab.tsx` to import and reuse `State.AiModel` and `State.Models` from
`core/usecases/ai/state`, and then align the `main` selector usage with those
shared types so the UI stays consistent with the core union shape.
🤖 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.

Outside diff comments:
In `@web/src/core/usecases/ai/state.ts`:
- Around line 140-163: editCustomProvider currently resets provider.models to
fetching after credentials change, but it leaves provider.selectedModelId
behind, which can keep a stale model selection and break
selectors.activeProvider. Update editCustomProvider in state.ts to also clear
the selected model when apiBase/apiKey/label change, so modelsLoaded can
re-default to the first available model after the refetch. Use the existing
editCustomProvider and modelsLoaded flow to locate the fix.

---

Nitpick comments:
In `@web/src/ui/pages/account/AccountAiTab.tsx`:
- Around line 28-34: `AccountAiTab` is duplicating the AI model state types
instead of using the shared core definitions. Update the `AiModel` and `Models`
references in `AccountAiTab.tsx` to import and reuse `State.AiModel` and
`State.Models` from `core/usecases/ai/state`, and then align the `main` selector
usage with those shared types so the UI stays consistent with the core union
shape.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e5a5cbf0-d313-4254-b550-5452ab155eb6

📥 Commits

Reviewing files that changed from the base of the PR and between ec4dac5 and cd0257b.

📒 Files selected for processing (9)
  • web/src/core/bootstrap.ts
  • web/src/core/tools/fetchAiModels.ts
  • web/src/core/tools/oidcTokenExchange.ts
  • web/src/core/usecases/ai/decoupledLogic/persistedAiConfig.test.ts
  • web/src/core/usecases/ai/decoupledLogic/persistedAiConfig.ts
  • web/src/core/usecases/ai/selectors.ts
  • web/src/core/usecases/ai/state.ts
  • web/src/core/usecases/ai/thunks.ts
  • web/src/ui/pages/account/AccountAiTab.tsx
✅ Files skipped from review due to trivial changes (1)
  • web/src/core/usecases/ai/decoupledLogic/persistedAiConfig.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/core/tools/oidcTokenExchange.ts
  • web/src/core/bootstrap.ts

@sonarqubecloud

Copy link
Copy Markdown

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