-
Notifications
You must be signed in to change notification settings - Fork 108
feat(ai): AI gateway integration with custom provider support #1072
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
59a7b06
386656f
553843e
c57e81c
fe6fea1
4342f34
ce04391
0bc8513
0f00457
9a18c27
989498b
674d985
bfcd005
518a5a7
1c8e628
8f81c0b
40a36ab
64c9bc3
46d9e11
a97561e
cc4bce8
a4d2408
9fc2bfd
614b024
5a02100
f25c649
0dd7fcb
55c0d9e
3c2094d
b6cd906
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Commands | ||
|
|
||
| All commands use **Yarn** (not npm). | ||
|
|
||
| ```bash | ||
| yarn dev # Start dev server (processes env YAML first via scripts/unyamlify-env-local.ts) | ||
| yarn build # Type-check (tsc) then build for production | ||
| yarn test # Run all tests once (Vitest, non-watch) | ||
| yarn format # Format all .ts/.tsx/.json/.md files with Prettier | ||
| yarn format:check # Check formatting without writing | ||
| yarn storybook # Launch Storybook on port 6006 | ||
| ``` | ||
|
|
||
| **Run a single test file:** | ||
|
|
||
| ```bash | ||
| yarn vitest run src/core/usecases/launcher/decoupledLogic/computeHelmValues.test.ts | ||
| ``` | ||
|
|
||
| **Run tests matching a name pattern:** | ||
|
|
||
| ```bash | ||
| yarn vitest run --reporter=verbose -t "pattern" | ||
| ``` | ||
|
|
||
| Pre-commit hooks run `eslint --fix` and `prettier --write` via lint-staged. | ||
|
|
||
| ## Architecture | ||
|
|
||
| Onyxia Web is a React SPA — a data science platform portal for launching Kubernetes services (Helm charts), browsing catalogs, managing S3 files, managing Vault secrets, and querying data via DuckDB. It is deployed as static files served by nginx. | ||
|
|
||
| ### Core principles | ||
|
|
||
| - **React is only for rendering.** Business logic is React-agnostic and lives in `src/core/`. The `src/ui/` layer is strictly for React components and hooks. | ||
| - **Unidirectional dependencies.** `src/core/` never imports from `src/ui/`, not even for types. | ||
| - **Reactive over promise-based.** Thunks update observable state; the UI reacts to state changes. Prefer dispatching actions and reading state over returning values from thunks. | ||
| - **Constants outside Redux state.** Values that don't change are not stored in state — they are retrieved from thunks when needed, to avoid unnecessary re-renders. | ||
|
|
||
| ### `src/core/` — Business logic | ||
|
|
||
| Follows a clean-architecture / ports-and-adapters pattern using the `clean-architecture` npm package (a Redux-like store without Redux). | ||
|
|
||
| - **`ports/`** — TypeScript interfaces defining contracts for external dependencies (`OnyxiaApi`, `Oidc`, `S3Client`, `SecretsManager`, `SqlOlap`). | ||
| - **`adapters/`** — Concrete implementations: `onyxiaApi/` (axios-based HTTP), `oidc/` (oidc-spa), `s3Client/` (AWS SDK v3), `secretManager/` (Vault), `sqlOlap/` (DuckDB WASM). Each adapter has a mock counterpart for dev/testing. | ||
| - **`usecases/`** — One folder per feature (20+ total: `catalog`, `launcher`, `serviceManagement`, `fileExplorer`, `secretExplorer`, `dataExplorer`, etc.). Each usecase follows the pattern: | ||
| - `state.ts` — state shape + `createUsecaseActions` (slice-like) | ||
| - `thunks.ts` — async side effects, accesses adapters via `createUsecaseContextApi` | ||
| - `selectors.ts` — memoized state derivations | ||
| - `index.ts` — re-exports all three | ||
| - **`bootstrap.ts`** — Wires adapters together and creates the core store. | ||
| - **`index.ts`** — Exports `useCoreState`, `getCore`, `createReactApi` bindings consumed by `src/ui/`. | ||
|
|
||
| **Complex use-cases** (especially `launcher/`) have a `decoupledLogic/` subfolder with pure functions and no framework dependencies — this is where most unit tests live. | ||
|
|
||
| ### `src/ui/` — React layer | ||
|
|
||
| - **`App/`** — Root layout: Header, LeftBar, Main, Footer. `App.tsx` triggers core bootstrap; `Main.tsx` is the route-based page switcher. | ||
| - **`pages/`** — One folder per route/page. Each page exports `routeDefs` (via `type-route`'s `defineRoute`) and `routeGroup`. All are merged in `pages/index.ts`. | ||
| - **`routes.tsx`** — Router instantiation. Navigation uses `routes.catalog(...).push()` or `session.push()`. | ||
| - **`i18n/`** — i18nifty setup. Translation keys are declared at the component level via `declareComponentKeys`, collected into a `ComponentKey` union in `i18n/types.ts`. Nine languages: en, fr, zh-CN, no, fi, nl, it, es, de. | ||
| - **`theme/`** — onyxia-ui theme setup (palette, fonts, favicon). | ||
| - **`shared/`** — Reusable components (CommandBar, CodeBlock, SettingField, etc.). | ||
|
|
||
| ### Key patterns | ||
|
|
||
| **Consuming core state in React:** | ||
|
|
||
| ```ts | ||
| import { useCoreState, getCore } from "core"; | ||
| const helmReleases = useCoreState(state => state.serviceManagement.helmReleases); | ||
| await getCore().dispatch(usecases.serviceManagement.thunks.initialize()); | ||
| ``` | ||
|
|
||
| **Styling — tss-react** (not plain CSS modules): | ||
|
|
||
| ```ts | ||
| import { tss } from "tss"; | ||
| const useStyles = tss.withName({ MyComponent }).create(({ theme }) => ({ ... })); | ||
| const { classes, cx } = useStyles(); | ||
| ``` | ||
|
|
||
| **Absolute imports** — `tsconfig.json` sets `baseUrl: "src"`, so use `import { foo } from "core/usecases/catalog"` (not relative paths). | ||
|
|
||
| **Environment variables** — All env vars are centrally parsed and validated in `src/env.ts`. The `index.html` is an EJS template processed by `vite-envs` at build time. | ||
|
|
||
| **Authentication** — OIDC init (`oidc-spa`) happens before React renders, in `main.tsx`. Use the `Oidc` port interface, not the adapter directly. | ||
|
|
||
| **Plugin system** — `src/pluginSystem.ts` exposes `window.onyxia` after boot and fires an `"onyxiaready"` `CustomEvent`, allowing external JS to interact with core state, routes, theme, and i18n. | ||
|
|
||
| **Keycloak theme** — `src/keycloak-theme/` is a Keycloakify login theme that shares env and i18n infrastructure with the main app. Build with `yarn build-keycloak-theme`. | ||
|
|
||
| ## Key libraries | ||
|
|
||
| | Library | Role | | ||
| | -------------------- | ------------------------------------------------------------ | | ||
| | `onyxia-ui` | In-house design system on top of MUI v6 | | ||
| | `type-route` | Strongly-typed client-side router | | ||
| | `i18nifty` | Component-level i18n | | ||
| | `clean-architecture` | Redux-like store (ports/usecases pattern) | | ||
| | `oidc-spa` | OIDC/OAuth2 authentication | | ||
| | `keycloakify` | Keycloak login theme from React components | | ||
| | `tss-react` | CSS-in-JS bound to onyxia-ui theme | | ||
| | `vite-envs` | Env var injection into EJS `index.html` at build time | | ||
| | DuckDB WASM | In-browser SQL OLAP queries (`dataExplorer`, `sqlOlapShell`) | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from "./openWebUi"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import type { Ai, GetTokenResult } from "core/ports/Ai"; | ||
| import { oidcTokenExchange, OidcTokenExchangeError } from "core/tools/oidcTokenExchange"; | ||
| import { z } from "zod"; | ||
|
|
||
| export function createAi(params: { | ||
| id: string; | ||
| name: string; | ||
| provider: Ai["provider"]; | ||
| description: Ai["description"]; | ||
| accountCreation: Ai["accountCreation"]; | ||
| webUiUrl: string; | ||
| oauthProvider: string; | ||
| getOidcAccessToken: () => Promise<string>; | ||
| }): Ai { | ||
| const { | ||
| id, | ||
| name, | ||
| provider, | ||
| description, | ||
| accountCreation, | ||
| webUiUrl, | ||
| oauthProvider, | ||
| getOidcAccessToken | ||
| } = params; | ||
|
|
||
| const apiBase = `${webUiUrl}/api`; | ||
|
|
||
| return { | ||
| id, | ||
| name, | ||
| provider, | ||
| description, | ||
| accountCreation, | ||
| webUiUrl, | ||
| apiBase, | ||
| getToken: async (): Promise<GetTokenResult> => { | ||
| const oidcAccessToken = await getOidcAccessToken(); | ||
|
|
||
| return oidcTokenExchange({ | ||
| tokenExchangeEndpoint: `${webUiUrl}/api/v1/auths/oauth/${oauthProvider}/token/exchange`, | ||
| oidcAccessToken | ||
| }) | ||
| .then(token => ({ status: "success" as const, token })) | ||
| .catch((error: unknown) => { | ||
| if (error instanceof OidcTokenExchangeError && error.status === 403) { | ||
| return { status: "no-account" as const }; | ||
| } | ||
| return { status: "error" as const }; | ||
| }); | ||
| }, | ||
| listModels: async (token: string) => { | ||
| const response = await fetch(`${apiBase}/models`, { | ||
| headers: { Authorization: `Bearer ${token}` } | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Failed to list models (${response.status})`); | ||
| } | ||
|
|
||
| const { data } = z | ||
| .object({ data: z.array(z.object({ id: z.string(), name: z.string() })) }) | ||
| .parse(await response.json()); | ||
|
|
||
| return data.map(({ id, name }) => ({ id, name })); | ||
| } | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||||||||||||||||||||||||||||||||||
| import type { SqlOlap } from "core/ports/SqlOlap"; | ||||||||||||||||||||||||||||||||||||||
| import { usecases } from "./usecases"; | ||||||||||||||||||||||||||||||||||||||
| import type { SecretsManager } from "core/ports/SecretsManager"; | ||||||||||||||||||||||||||||||||||||||
| import type { Ai } from "core/ports/Ai"; | ||||||||||||||||||||||||||||||||||||||
| import type { Oidc } from "core/ports/Oidc"; | ||||||||||||||||||||||||||||||||||||||
| import type { Language } from "core/ports/OnyxiaApi/Language"; | ||||||||||||||||||||||||||||||||||||||
| import { createDuckDbSqlOlap } from "core/adapters/sqlOlap"; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -16,6 +17,7 @@ | |||||||||||||||||||||||||||||||||||||
| import { assert } from "tsafe/assert"; | ||||||||||||||||||||||||||||||||||||||
| import { fnv1aHashToHex } from "core/tools/fnv1aHashToHex"; | ||||||||||||||||||||||||||||||||||||||
| import { type S3Config, parseS3ConfigFromEnvValue } from "core/ports/OnyxiaApi/S3Config"; | ||||||||||||||||||||||||||||||||||||||
| import { parseAiConfigFromEnvValue } from "core/ports/OnyxiaApi/AiConfig"; | ||||||||||||||||||||||||||||||||||||||
| import { setRootContext } from "./rootContext"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export type ParamsOfBootstrapCore = { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,8 +33,10 @@ | |||||||||||||||||||||||||||||||||||||
| isAuthGloballyRequired: boolean; | ||||||||||||||||||||||||||||||||||||||
| enableOidcDebugLogs: boolean; | ||||||||||||||||||||||||||||||||||||||
| disableDisplayAllCatalog: boolean; | ||||||||||||||||||||||||||||||||||||||
| isAiEnabled: boolean; | ||||||||||||||||||||||||||||||||||||||
| getIsDarkModeEnabled: () => boolean; | ||||||||||||||||||||||||||||||||||||||
| S3_envValue: string; | ||||||||||||||||||||||||||||||||||||||
| AI_envValue: string; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export type Context = { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -42,6 +46,7 @@ | |||||||||||||||||||||||||||||||||||||
| secretsManager: SecretsManager; | ||||||||||||||||||||||||||||||||||||||
| sqlOlap: SqlOlap; | ||||||||||||||||||||||||||||||||||||||
| s3Config: S3Config; | ||||||||||||||||||||||||||||||||||||||
| ai: Ai[]; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export type Core = GenericCore<typeof usecases, Context>; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -53,7 +58,8 @@ | |||||||||||||||||||||||||||||||||||||
| onyxiaApiUrl, | ||||||||||||||||||||||||||||||||||||||
| transformBeforeRedirectForKeycloakTheme, | ||||||||||||||||||||||||||||||||||||||
| getCurrentLang, | ||||||||||||||||||||||||||||||||||||||
| enableOidcDebugLogs | ||||||||||||||||||||||||||||||||||||||
| enableOidcDebugLogs, | ||||||||||||||||||||||||||||||||||||||
| isAiEnabled | ||||||||||||||||||||||||||||||||||||||
| } = params; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const isAuthGloballyRequired = | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -65,6 +71,10 @@ | |||||||||||||||||||||||||||||||||||||
| envValue: params.S3_envValue | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const aiConfig = isAiEnabled | ||||||||||||||||||||||||||||||||||||||
| ? parseAiConfigFromEnvValue({ envValue: params.AI_envValue }) | ||||||||||||||||||||||||||||||||||||||
| : { entries: [] }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let oidc: Oidc | undefined = undefined; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const onyxiaApi: OnyxiaApi = await (async () => { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -181,7 +191,6 @@ | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (isAuthGloballyRequired && !oidc.isUserLoggedIn) { | ||||||||||||||||||||||||||||||||||||||
| await oidc.login({ doesCurrentHrefRequiresAuth: true }); | ||||||||||||||||||||||||||||||||||||||
| // NOTE: Never reached | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const context: Context = { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -222,7 +231,8 @@ | |||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||
| s3Config | ||||||||||||||||||||||||||||||||||||||
| s3Config, | ||||||||||||||||||||||||||||||||||||||
| ai: [] | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| setRootContext(context); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -339,7 +349,7 @@ | |||||||||||||||||||||||||||||||||||||
| await dispatch(usecases.userProfileForm.protectedThunks.initialize()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| init_s3ProfilesManagement: { | ||||||||||||||||||||||||||||||||||||||
|
Check warning on line 352 in web/src/core/bootstrap.ts
|
||||||||||||||||||||||||||||||||||||||
| if (!oidc.isUserLoggedIn) { | ||||||||||||||||||||||||||||||||||||||
| break init_s3ProfilesManagement; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -347,6 +357,87 @@ | |||||||||||||||||||||||||||||||||||||
| await dispatch(usecases.s3ProfilesManagement.protectedThunks.initialize()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| init_ai: { | ||||||||||||||||||||||||||||||||||||||
|
Check warning on line 360 in web/src/core/bootstrap.ts
|
||||||||||||||||||||||||||||||||||||||
| if (!isAiEnabled) { | ||||||||||||||||||||||||||||||||||||||
| break init_ai; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!oidc.isUserLoggedIn) { | ||||||||||||||||||||||||||||||||||||||
| break init_ai; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Wire one Ai adapter per instance-configured gateway into `context.ai` (none | ||||||||||||||||||||||||||||||||||||||
| // if the AI env is empty: only custom providers will then be loaded). | ||||||||||||||||||||||||||||||||||||||
| configured_ai: { | ||||||||||||||||||||||||||||||||||||||
|
Check warning on line 371 in web/src/core/bootstrap.ts
|
||||||||||||||||||||||||||||||||||||||
| if (aiConfig.entries.length === 0) { | ||||||||||||||||||||||||||||||||||||||
| break configured_ai; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const [{ createAi }, { createOidc, mergeOidcParams }, { oidcParams }] = | ||||||||||||||||||||||||||||||||||||||
| await Promise.all([ | ||||||||||||||||||||||||||||||||||||||
| import("core/adapters/ai"), | ||||||||||||||||||||||||||||||||||||||
| import("core/adapters/oidc"), | ||||||||||||||||||||||||||||||||||||||
| onyxiaApi.getAvailableRegionsAndOidcParams() | ||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| assert(oidcParams !== undefined); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+376
to
+383
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Handle missing global OIDC params here instead of asserting.
Suggested fix- assert(oidcParams !== undefined);
+ if (oidcParams === undefined) {
+ break region_ai;
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Providers may share the same OIDC client: oidc-spa identifies a client by | ||||||||||||||||||||||||||||||||||||||
| // issuerUri + clientId, so creating it twice would collide. Create each | ||||||||||||||||||||||||||||||||||||||
| // distinct client only once. | ||||||||||||||||||||||||||||||||||||||
| const getOidcAccessTokenByOidcKey = new Map<string, () => Promise<string>>(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| for (const aiConfigEntry of aiConfig.entries) { | ||||||||||||||||||||||||||||||||||||||
| const oidcParams_ai = mergeOidcParams({ | ||||||||||||||||||||||||||||||||||||||
| oidcParams, | ||||||||||||||||||||||||||||||||||||||
| oidcParams_partial: aiConfigEntry.oidcParams | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+391
to
+394
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a managed gateway is configured without a complete Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const oidcKey = `${oidcParams_ai.issuerUri}\0${oidcParams_ai.clientId}`; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let getOidcAccessToken = getOidcAccessTokenByOidcKey.get(oidcKey); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (getOidcAccessToken === undefined) { | ||||||||||||||||||||||||||||||||||||||
| const oidc_ai = await createOidc({ | ||||||||||||||||||||||||||||||||||||||
| ...oidcParams_ai, | ||||||||||||||||||||||||||||||||||||||
| transformBeforeRedirectForKeycloakTheme, | ||||||||||||||||||||||||||||||||||||||
| getCurrentLang, | ||||||||||||||||||||||||||||||||||||||
| autoLogin: true, | ||||||||||||||||||||||||||||||||||||||
| enableDebugLogs: enableOidcDebugLogs, | ||||||||||||||||||||||||||||||||||||||
| // 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. | ||||||||||||||||||||||||||||||||||||||
| disableDPoP: true | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| getOidcAccessToken = async () => | ||||||||||||||||||||||||||||||||||||||
| (await oidc_ai.getTokens()).accessToken; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| getOidcAccessTokenByOidcKey.set(oidcKey, getOidcAccessToken); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| context.ai.push( | ||||||||||||||||||||||||||||||||||||||
| createAi({ | ||||||||||||||||||||||||||||||||||||||
| id: aiConfigEntry.id, | ||||||||||||||||||||||||||||||||||||||
| name: aiConfigEntry.name ?? new URL(aiConfigEntry.url).hostname, | ||||||||||||||||||||||||||||||||||||||
| provider: aiConfigEntry.provider, | ||||||||||||||||||||||||||||||||||||||
| description: aiConfigEntry.description, | ||||||||||||||||||||||||||||||||||||||
| accountCreation: aiConfigEntry.accountCreation, | ||||||||||||||||||||||||||||||||||||||
| webUiUrl: aiConfigEntry.url, | ||||||||||||||||||||||||||||||||||||||
| oauthProvider: aiConfigEntry.oauthProvider, | ||||||||||||||||||||||||||||||||||||||
| getOidcAccessToken | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Sole initiator of the AI use-case, dispatched only now that any managed | ||||||||||||||||||||||||||||||||||||||
| // adapters are wired into `context.ai`. Fire-and-forget so app start isn't | ||||||||||||||||||||||||||||||||||||||
| // blocked; consumers await readiness via `ai...waitForInitialization`. | ||||||||||||||||||||||||||||||||||||||
| dispatch(usecases.ai.protectedThunks.initialize()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| pluginSystemInitCore({ core, context }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return { core }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
getOidcAccessToken()rejects, for example because refreshing the gateway-specific OIDC session fails, this await occurs before the exchange's.catch()and therefore escapesgetToken()instead of returning{ status: "error" }. The initialization-widetryinai/thunks.tsthen dispatchesinitializationFailed, hiding every other managed and custom provider because one gateway's OIDC client failed.Useful? React with 👍 / 👎.