feat(ai): AI gateway integration with custom provider support #1072
feat(ai): AI gateway integration with custom provider support #1072
Conversation
- 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>
|
📝 WalkthroughWalkthroughThis 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. ChangesAI Gateway Integration
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
web/src/core/ports/Ai.ts (1)
8-11: ⚡ Quick win
errorvariant carries no diagnostic payload — error context is lostThe
{ 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 minimalreason?: stringorhttpStatus?: numberwould 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: truediscriminant is redundant given| undefinedSince
aiis typed as{ enabled: true; ... } | undefined, theenabled: trueliteral adds no information — an AI block being present already implies it is enabled. Chart templates that checkai.enabledcould equivalently checkai != null. Consider dropping theenabledfield to keep the shape consistent with howvaultands3are 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 winUnguarded cast on
data.datamay throw aTypeErrorat runtime.If the gateway returns a response where
data.dataisundefined,null, or not an array,.map()will throw, which callers don't expect —listModelsshould 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
testCustomProviderreturns aPromise<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
📒 Files selected for processing (30)
web/CLAUDE.mdweb/src/core/adapters/ai/index.tsweb/src/core/adapters/ai/mock.tsweb/src/core/adapters/ai/openWebUi.tsweb/src/core/adapters/onyxiaApi/ApiTypes.tsweb/src/core/adapters/onyxiaApi/onyxiaApi.tsweb/src/core/bootstrap.tsweb/src/core/ports/Ai.tsweb/src/core/ports/OnyxiaApi/DeploymentRegion.tsweb/src/core/ports/OnyxiaApi/XOnyxia.tsweb/src/core/tools/oidcTokenExchange.tsweb/src/core/usecases/ai/index.tsweb/src/core/usecases/ai/selectors.tsweb/src/core/usecases/ai/state.tsweb/src/core/usecases/ai/thunks.tsweb/src/core/usecases/index.tsweb/src/core/usecases/launcher/thunks.tsweb/src/ui/i18n/resources/de.tsxweb/src/ui/i18n/resources/en.tsxweb/src/ui/i18n/resources/es.tsxweb/src/ui/i18n/resources/fi.tsxweb/src/ui/i18n/resources/fr.tsxweb/src/ui/i18n/resources/it.tsxweb/src/ui/i18n/resources/nl.tsxweb/src/ui/i18n/resources/no.tsxweb/src/ui/i18n/resources/zh-CN.tsxweb/src/ui/i18n/types.tsweb/src/ui/pages/account/AccountAiTab.tsxweb/src/ui/pages/account/Page.tsxweb/src/ui/pages/account/accountTabIds.ts
| 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 | ||
| }; |
There was a problem hiding this comment.
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.
|
|
||
| const { token } = tokenResult; | ||
| const availableModels = await ai.listModels(token); | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| 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 ?? "" | ||
| }; | ||
| })(), |
There was a problem hiding this comment.
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.
| 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.
| "custom providers section helper": | ||
| "Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.", |
There was a problem hiding this comment.
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.
| "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.
| const onSaveProvider = useConstCallback(async () => { | ||
| await ai.addCustomProvider({ | ||
| label: pendingLabel, | ||
| apiBase: pendingApiBase, | ||
| apiKey: pendingApiKey | ||
| }); | ||
| setAddFormOpen(false); | ||
| setPendingLabel(""); | ||
| setPendingApiBase(""); | ||
| setPendingApiKey(""); | ||
| setTestStatus("idle"); | ||
| setTestModelCount(0); | ||
| }); |
There was a problem hiding this comment.
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.
| 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())) |
There was a problem hiding this comment.
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.
| .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.
🤖 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.



Aiport (OIDC token exchange, model listing) with an OpenWebUI adapter and a mock; config comes from the deployment region(
region.ai)aiusecase — initialize lifecycle (pending/success/no-account/error), token refresh, model selection; selected model is injected into Helm values when launching aservice
localStorage
/models), shows success with model count or an error messageSummary by CodeRabbit