Skip to content

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

Open
ddecrulle wants to merge 2 commits intomainfrom
ia-integration
Open

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

Conversation

@ddecrulle
Copy link
Copy Markdown
Contributor

@ddecrulle ddecrulle commented May 5, 2026

  • 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 AI Gateway integration with OpenWebUI support for token-based authentication and model management
    • New AI tab in account settings allows users to configure credentials and manage custom AI providers
    • Extended multi-language support for all AI-related features across 10+ languages

ddecrulle and others added 2 commits April 28, 2026 16:45
- 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>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive AI gateway integration feature. It adds type definitions, OpenWebUI adapter implementations, OIDC token exchange utilities, Redux-style state management with thunks and selectors, bootstrap configuration wiring, a new account UI tab for managing credentials and custom providers, and translations across 10 languages.

Changes

AI Gateway Integration

Layer / File(s) Summary
Types & Ports
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
New Ai port defines getToken() and listModels(token) methods, returning GetTokenResult discriminated union. DeploymentRegion gains optional ai config with url, oauthProvider, oidcParams. XOnyxiaContext adds optional ai injection block with enabled, token, apiBase, model. API types extended with ai?: { URL, oauthProvider, oidcConfiguration? }.
Token Exchange Utilities
web/src/core/tools/oidcTokenExchange.ts
New OidcTokenExchangeError class carries HTTP status. oidcTokenExchange() POSTs OIDC token to exchange endpoint, parses response to extract token from data.token or data.access_token, throws error on non-OK or missing token.
AI Adapters
web/src/core/adapters/ai/openWebUi.ts, web/src/core/adapters/ai/mock.ts, web/src/core/adapters/ai/index.ts
openWebUi.createAi() wires OAuth token exchange with OpenWebUI endpoint, mapping 403 status to "no-account" result. mock.createAi() provides test stub returning fixed token and model list. Barrel export from index.ts exposes both adapters.
State Management
web/src/core/usecases/ai/state.ts
AI state slice supports two modes: Disabled (tracks initializationStatus) and Enabled (tracks webUiUrl, apiBase, token, availableModels, selectedModel, customProviders). Reducers manage initialization lifecycle, token refresh, model selection, and custom-provider CRUD (add/delete/test/select) with per-provider modelsFetchStatus.
Thunks & Side Effects
web/src/core/usecases/ai/thunks.ts
thunks object provides availability check, token refresh, model persistence, and custom-provider management (UUID generation, localStorage persistence, async model fetching). protectedThunks.initialize orchestrates startup: acquires token, lists base models, loads persisted custom providers, dispatches success, then fetches models per provider in parallel.
Selectors
web/src/core/usecases/ai/selectors.ts
selectors.main combines AI state with current deployment region. Branches on isEnabled: when disabled with "no-account" status, asserts region.ai and returns disabled result including webUiUrl; when enabled, returns full AI context with credentials and model lists.
Barrel & Core Integration
web/src/core/usecases/ai/index.ts, web/src/core/usecases/index.ts
AI usecase exports via barrel from state, selectors, thunks. Root usecases object includes ai namespace, making it accessible via usecases.ai.*.
Bootstrap Wiring
web/src/core/bootstrap.ts
Context type adds optional `ai: Ai
Onyxia API Integration
web/src/core/adapters/onyxiaApi/onyxiaApi.ts
getAvailableRegionsAndOidcParams maps apiRegion.data?.ai into each DeploymentRegion, including url, oauthProvider, and derived oidcParams via apiTypesOidcConfigurationToOidcParams_Partial.
Launcher Context Injection
web/src/core/usecases/launcher/thunks.ts
protectedThunks.getXOnyxiaContext reads AI selector and conditionally populates xOnyxiaContext.ai: returns undefined when disabled or token missing, otherwise includes enabled, token, apiBase, model.
Account Tab Routes
web/src/ui/pages/account/accountTabIds.ts
accountTabIds array includes new "ai" tab identifier; AccountTabId type derives from updated array.
Account UI Page Integration
web/src/ui/pages/account/Page.tsx
Lazy imports AccountAiGatewayTab. Destructures ai from core. Conditionally includes "ai" tab in tabs list via ai.isAvailable(). Renders AccountAiGatewayTab component on "ai" tab route.
Account AI Tab Component
web/src/ui/pages/account/AccountAiTab.tsx
AccountAiGatewayTab memoized component wires AI credentials UI. On mount, refreshes token if enabled but token missing. Renders API base/token fields (copy-to-clipboard), model <Select> bound to core selectedModel, and custom-providers section. Custom-provider dialog includes label/apiBase/apiKey fields, async test action (with status machine), and async save via ai.addCustomProvider(). CustomProviderCard renders provider metadata and model selector with modelsFetchStatus feedback (loading/error/list).
I18n Component Type
web/src/ui/i18n/types.ts
ComponentKey union adds import("ui/pages/account/AccountAiTab").I18n between account tab entries.
I18n Resources
web/src/ui/i18n/resources/{en,de,es,fi,fr,it,nl,no,zh-CN}.tsx
Each language file adds Account.ai label (e.g., "AI", "KI", "IA"). New AccountAiGatewayTab section per language with strings for OIDC access, API base/token/model labels, custom provider fields, connection test/save/cancel/error messages, and "no account" JSX message with link to webUiUrl.
Documentation
web/CLAUDE.md
New developer guide for Claude AI coding assistant. Documents Yarn command conventions, test running patterns, repository architecture (core vs UI separation, ports-and-adapters structure), layering principles, key libraries table, and development patterns (React-agnostic core, unidirectional dependencies, OIDC timing, plugin system).

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as AccountAiTab
    participant Store as Redux Store
    participant Bootstrap as bootstrapCore
    participant OAuthProvider as OAuth Provider
    participant OpenWebUI as OpenWebUI API
    participant XOnyxia as X-Onyxia Gateway

    User->>UI: Navigate to AI tab
    UI->>Store: Dispatch ai.refreshToken()
    
    Store->>Bootstrap: Thunk executes
    Bootstrap->>OAuthProvider: getOidcAccessToken()
    OAuthProvider-->>Bootstrap: OIDC access token
    
    Bootstrap->>OpenWebUI: oidcTokenExchange(token)
    alt Token Exchange Success
        OpenWebUI-->>Bootstrap: { token: "ai-token" }
        Bootstrap->>Store: Dispatch initializeSucceed
    else 403 Forbidden
        OpenWebUI-->>Bootstrap: HTTP 403
        Bootstrap->>Store: Dispatch initializeFailed("no-account")
    else Other Error
        OpenWebUI-->>Bootstrap: Error
        Bootstrap->>Store: Dispatch initializeFailed("error")
    end
    
    Bootstrap->>OpenWebUI: listModels(token)
    OpenWebUI-->>Bootstrap: ["llama3.2", "mistral-7b", ...]
    
    Bootstrap->>Store: Dispatch initializeSucceed<br/>with models & selectedModel
    Store-->>UI: Update selector result
    
    UI->>User: Render credentials,<br/>model selector,<br/>custom providers
    
    User->>UI: Add custom provider
    UI->>Store: Dispatch addCustomProvider(label, apiBase, apiKey)
    Store->>Store: Generate UUID, set modelsFetchStatus="fetching"
    Store-->>UI: Update UI with new provider
    
    UI->>OpenWebUI: Fetch /models<br/>with custom apiBase & apiKey
    OpenWebUI-->>UI: ["custom-model-1", ...]
    UI->>Store: Dispatch customProviderModelsLoaded
    Store-->>UI: Update provider's availableModels
    UI->>User: Render loaded model list
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 The AI gateway hops into view,
With tokens swapped and models new,
OIDC flows through OAuth's door,
Custom providers, add some more!
Nine tongues now speak what users do—
A feature bold, in form and hue! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 clearly and accurately summarizes the main feature addition: AI gateway integration with custom provider support. It directly reflects the core changes across the PR.
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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 on lines +16 to +26
if (initializationStatus === "no-account") {
assert(
region.ai !== undefined,
"region.ai should exists in case of no-account status"
);

return {
isEnabled: false as const,
initializationStatus,
webUiUrl: region.ai.url
};
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

Guard the no-account branch without a hard assertion.

Line 17 can throw at selector runtime if initializationStatus is "no-account" while region.ai is absent, which would break account page rendering. Prefer a safe fallback and only attach webUiUrl when available.

Proposed fix
-            if (initializationStatus === "no-account") {
-                assert(
-                    region.ai !== undefined,
-                    "region.ai should exists in case of no-account status"
-                );
-
+            if (initializationStatus === "no-account" && region.ai !== undefined) {
                 return {
                     isEnabled: false as const,
                     initializationStatus,
                     webUiUrl: region.ai.url
                 };
             }
🤖 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/selectors.ts` around lines 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.

Comment on lines +190 to +193

const { token } = tokenResult;
const availableModels = await ai.listModels(token);

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 | 🔴 Critical | ⚡ Quick win

ai.listModels(token) is not wrapped in try/catch — the state gets permanently stuck at "pending" on any failure.

After initializeStart is dispatched (state = "pending"), if ai.listModels(token) throws for any reason (network error, unexpected server response, etc.), initializeFailed is never dispatched and the UI shows a loading spinner indefinitely.

🐛 Proposed fix
-const availableModels = await ai.listModels(token);
+let availableModels: string[];
+try {
+    availableModels = await ai.listModels(token);
+} catch {
+    dispatch(actions.initializeFailed({ cause: "error" }));
+    return;
+}
📝 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 { token } = tokenResult;
const availableModels = await ai.listModels(token);
const { token } = tokenResult;
let availableModels: string[];
try {
availableModels = await ai.listModels(token);
} catch {
dispatch(actions.initializeFailed({ cause: "error" }));
return;
}
🤖 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 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.

Comment on lines +777 to +790
ai: (() => {
const aiState = aiUsecase.selectors.main(getState());

if (!aiState.isEnabled || aiState.token === undefined) {
return undefined;
}

return {
enabled: true as const,
token: aiState.token,
apiBase: aiState.apiBase,
model: aiState.selectedModel ?? ""
};
})(),
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

selectedModel ?? "" injects an empty model name into Helm chart values when no model is selected.

When aiState.selectedModel is undefined (e.g., the gateway returned an empty model list), model: "" is still written into the XOnyxiaContext. Any Helm template that interpolates {{ .Values.ai.model }} will receive an empty string, which can silently produce malformed API requests or service launch failures.

The guard should treat an undefined selectedModel the same as a missing token — exclude ai from the context entirely:

🐛 Proposed fix
-if (!aiState.isEnabled || aiState.token === undefined) {
+if (!aiState.isEnabled || aiState.token === undefined || aiState.selectedModel === undefined) {
     return undefined;
 }

 return {
     enabled: true as const,
     token: aiState.token,
     apiBase: aiState.apiBase,
-    model: aiState.selectedModel ?? ""
+    model: aiState.selectedModel
 };
📝 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
ai: (() => {
const aiState = aiUsecase.selectors.main(getState());
if (!aiState.isEnabled || aiState.token === undefined) {
return undefined;
}
return {
enabled: true as const,
token: aiState.token,
apiBase: aiState.apiBase,
model: aiState.selectedModel ?? ""
};
})(),
ai: (() => {
const aiState = aiUsecase.selectors.main(getState());
if (!aiState.isEnabled || aiState.token === undefined || aiState.selectedModel === undefined) {
return undefined;
}
return {
enabled: true as const,
token: aiState.token,
apiBase: aiState.apiBase,
model: aiState.selectedModel
};
})(),
🤖 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/launcher/thunks.ts` around lines 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.

Comment on lines +126 to +127
"custom providers section helper":
"Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.",
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

Keep API key storage disclosure in localized helper text.

This helper omits that credentials are stored in the browser, which is important user-facing security context.

Suggested wording patch
-        "custom providers section helper":
-            "Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.",
+        "custom providers section helper":
+            "Legg til dine egne AI-leverandører (OpenAI, Anthropic eller andre OpenAI-kompatible endepunkter). Legitimasjon lagres i nettleseren din.",
📝 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
"custom providers section helper":
"Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.",
"custom providers section helper":
"Legg til dine egne AI-leverandører (OpenAI, Anthropic eller andre OpenAI-kompatible endepunkter). Legitimasjon lagres i nettleseren din.",
🤖 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/i18n/resources/no.tsx` around lines 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.

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.

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