Add generic AI provider external integration type - #2744
Conversation
External integrations can now declare type "ai" and serve as the AI backend of the assistant instead of Gladys Plus. The contract is fully provider-agnostic: the whole agentic loop (MCP tool calling, prompts, history) stays in the core, and the integration only receives OpenAI-compatible chat completion requests over the integration WebSocket (external-integration.ai.chat, 120s ack) and answers with OpenAI-compatible completions — a thin adapter to Claude, DeepSeek, OpenAI, a local LLM or any other provider. Server: - manifest type "ai" (schema + validation), no device screens - ai.chat proxy service capability relayed by the supervisor - AI_PROVIDER system variable: gateway.aiChat routes all AI traffic (chat loop, intent router, weekly digest) to the selected provider, with the Gladys Plus model field stripped and no silent fallback - AI works without any Gladys Plus account (message.create) - GET/POST /api/v1/ai_provider endpoints, selection cleared on uninstall of the selected provider Front: - AI provider selection card on the AI integration page (admin) - generic external integration page branches for the ai type - install warning dedicated to AI providers, catalog placement - chat model selector hidden when an external provider is active - en/fr/de translations Spec: docs/specs/external-integrations.md B.18 + C.1/C.4/C.5/C.8 updated in the same diff (spec-first rule). Community request: https://community.gladysassistant.com/t/10421 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhBhvoV4jdZEhxgadWwXV8
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis change adds ChangesExternal AI provider
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant AiProviderSettings
participant API
participant ExternalIntegration
participant AIProvider
User->>AiProviderSettings: Select AI provider
AiProviderSettings->>API: POST /api/v1/ai_provider
API->>ExternalIntegration: setAiProvider(selector)
ExternalIntegration-->>API: Save AI_PROVIDER
User->>API: Send AI chat request
API->>AIProvider: ai.chat(request)
AIProvider-->>API: OpenAI-compatible completion
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.
Stale comment
Review of Add generic AI provider external integration type (#2744).
Solid, spec-first design: provider-agnostic OpenAI-compatible relay, explicit admin selection, no silent Gladys Plus fallback, install trust warning, and AI without Plus via
message.create. Tests cover the new relay, routing, selection, uninstall cleanup, and API RBAC. No brand-specific device categories inconstants.js.Not approving. This is the highest-trust external-integration class (conversations + tool results + device/scene authority). Labeled
risk:highandneeds:human-review; requesting@atrovato(author is alreadyPierre-Gilles).Must resolve before merge
- Spec section number collision with #2738 — that open PR already claims B.18 for weather. This branch should use B.19 (or renumber weather), including the phase-2 table and C.1/C.4/C.5/C.8 cross-refs.
- Living-spec banner still outdated — the document header still says “B.15–B.17 are phase 2 designs not yet implemented” while this PR ships a new B.18. Update the banner in the same diff (weather’s branch already rewrote it).
- Incomplete
aifront redirects —ExternalIntegrationPagecorrectly drops device tabs forai, and install/catalog URLs go to/config, butdevice-page/index.jsanddiscover-page/index.jsstill only redirectcommunication. Direct/deviceor/discoverURLs for an AI provider can still hit empty device screens.Also note
- Vendored
manifest.schema.jsonacceptsai; the store indexer (GladysAssistant/integration-store) and SDK (onAiChat) need matching companion updates or catalog installs/onAiChatwill lag the core.- Completion validation only rejects non-objects; empty
{}still reachesextractAssistantMessageand fails softer asopenai.request.fail. Tightening is optional.Inline comments below.
Sent by Cursor Automation: Automatic PR review
|
🐳 A Docker image has been built for this branch and pushed to the GitHub Container Registry. You can test this pull request (AMD64 only) by pulling the image below: For example, run it with: sudo docker run -d \
--log-driver json-file \
--log-opt max-size=10m \
--cgroupns=host \
--restart=always \
--privileged \
--network=host \
--name gladys-claude-generic-ai-provider-integration-oebx0g \
-e NODE_ENV=production \
-e SERVER_PORT=80 \
-e TZ=Europe/Paris \
-e SQLITE_FILE_PATH=/var/lib/gladysassistant/gladys-production.db \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /var/lib/gladysassistant:/var/lib/gladysassistant \
-v /dev:/dev \
-v /run/udev:/run/udev:ro \
ghcr.io/gladysassistant/gladys-preview:claude-generic-ai-provider-integration-oebx0gThis comment and the image are automatically updated on every new commit pushed to this pull request. Need an ARM64 image (Raspberry Pi, Apple Silicon, …)? Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@server/lib/external-integration/externalIntegration.aiChat.js`:
- Around line 26-29: Strengthen the completion validation in the result-handling
code by requiring a non-empty choices array and an object at choices[0].message
before returning the response. Preserve throwing
ExternalIntegrationUnavailableError with
EXTERNAL_INTEGRATION_INVALID_AI_RESPONSE for all malformed structures, and add
unit-test cases for an empty object, an empty choices array, and a missing or
invalid first message.
In `@server/lib/external-integration/externalIntegration.uninstall.js`:
- Around line 55-60: Serialize setAiProvider() with uninstall() for the same
integration so provider selection cannot be written after uninstall has checked
or removed the service. Update the synchronization or availability validation
around these methods, preserving normal provider selection while ensuring
AI_PROVIDER is never saved with the uninstalling service.selector.
In `@server/test/lib/message/message.create.test.js`:
- Around line 76-105: The test around MessageHandler.create must also verify
that the Gladys Plus-required fallback event is not emitted when an external AI
provider is configured. Extend the existing event.emit assertions in the test to
assert no openai.plus-required reply is sent, while preserving the current
message.new-for-open-ai assertion and covering the new branch.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c7907e60-adea-4387-8623-4c289a9f7fa4
📒 Files selected for processing (30)
docs/specs/external-integrations.mdfront/src/config/i18n/de.jsonfront/src/config/i18n/en.jsonfront/src/config/i18n/fr.jsonfront/src/routes/chat/AiModelSelector.jsxfront/src/routes/integration/all/external-integration/ExternalIntegrationPage.jsxfront/src/routes/integration/all/external-integration/install-from-github/InstallFromGithubCard.jsxfront/src/routes/integration/all/external-integration/install-page/index.jsfront/src/routes/integration/all/openai/AiProviderSettings.jsxfront/src/routes/integration/all/openai/index.jsfront/src/routes/integration/index.jsserver/api/controllers/externalIntegration.controller.jsserver/api/routes.jsserver/lib/external-integration/constants.jsserver/lib/external-integration/externalIntegration.aiChat.jsserver/lib/external-integration/externalIntegration.getAiProviders.jsserver/lib/external-integration/externalIntegration.registerProxyService.jsserver/lib/external-integration/externalIntegration.setAiProvider.jsserver/lib/external-integration/externalIntegration.uninstall.jsserver/lib/external-integration/externalIntegration.validateManifest.jsserver/lib/external-integration/index.jsserver/lib/external-integration/manifest.schema.jsonserver/lib/gateway/gateway.aiChat.jsserver/lib/message/message.create.jsserver/test/controllers/externalIntegration/aiProvider.controller.test.jsserver/test/lib/external-integration/externalIntegration.aiProvider.test.jsserver/test/lib/external-integration/testUtils.test.jsserver/test/lib/gateway/gateway.aiChat.unit.test.jsserver/test/lib/message/message.create.test.jsserver/utils/constants.js
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2744 +/- ##
=======================================
Coverage 99.15% 99.16%
=======================================
Files 1190 1193 +3
Lines 25057 25120 +63
=======================================
+ Hits 24846 24909 +63
Misses 211 211 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- Renumber spec section B.18 to B.19 (B.18 is taken by the weather type in the parallel PR #2738) and update the living-spec banner to reflect the implemented state of B.15-B.17 and B.19 - Redirect direct URL access to the device/discover screens of an AI integration to its configuration screen (same rule as communication) - Validate the ai.chat completion at the contract boundary: data must carry an object choices[0].message, so adapter bugs surface as an explicit invalid-response error instead of a silent empty turn - Guard setAiProvider against a concurrent uninstall of the selected provider (re-check after write, roll back the variable) - Assert the plus-required reply is not sent when an external AI provider serves the assistant (message.create test) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhBhvoV4jdZEhxgadWwXV8
Deploying gladys-plus with
|
| Latest commit: |
6d43ed9
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e7a18a81.gladys-plus.pages.dev |
| Branch Preview URL: | https://claude-generic-ai-provider-i.gladys-plus.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@server/lib/external-integration/externalIntegration.setAiProvider.js`:
- Around line 27-37: Make the AI-provider selection and
externalIntegration.uninstall cleanup atomic rather than relying on the
post-write stillInstalled check in setAiProvider. Serialize both operations or
use an atomic conditional update/delete so uninstall cannot leave or recreate
AI_PROVIDER with the removed service_id; add a concurrency test covering the
interleaving.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b64b00b9-b31b-45d3-9a69-29406ef2950d
📒 Files selected for processing (9)
docs/specs/external-integrations.mdfront/src/routes/integration/all/external-integration/device-page/index.jsfront/src/routes/integration/all/external-integration/discover-page/index.jsserver/lib/external-integration/externalIntegration.aiChat.jsserver/lib/external-integration/externalIntegration.setAiProvider.jsserver/lib/gateway/gateway.aiChat.jsserver/test/lib/external-integration/externalIntegration.aiProvider.test.jsserver/test/lib/external-integration/testUtils.test.jsserver/test/lib/message/message.create.test.js
🚧 Files skipped from review as they are similar to previous changes (5)
- server/lib/external-integration/externalIntegration.aiChat.js
- server/test/lib/message/message.create.test.js
- server/test/lib/external-integration/externalIntegration.aiProvider.test.js
- server/test/lib/external-integration/testUtils.test.js
- server/lib/gateway/gateway.aiChat.js
…ndbox fix Review fixes (cursor bot, blocking): - renumber the TTS provider section from B.18 to B.20 everywhere (spec, schema comments, code comments): B.18 is owned by the weather type (#2738) and B.19 by the AI provider type (#2744) - clear TTS_ACTIVE_PROVIDER when uninstalling the selected provider (the AI_PROVIDER pattern of #2744): no dangling selector failing every announcement; documented in B.20 and C.5, covered by tests - living-spec truthfulness: the banner now states B.15-B.17 are implemented and reserves B.18/B.19 for the parallel PRs; the "current state" block of B.20 is rewritten as pre-refactor state Review fixes (cursor bot + CodeRabbit, non-blocking): - GET /api/v1/tts/provider now returns a display name per provider (the integration manifest name) so the System settings select matches the Integrations UI instead of showing raw selectors - getLocalApiBaseUrl prefers RFC1918 IPv4 addresses on multi-homed hosts (VPN tunnels or virtual bridges enumerating first) - own-key lookup on the TTS content-type allow-list (inherited Object.prototype keys such as "constructor" are rejected) - defense-in-depth validation in tts.getSpeechUrl: buffer type, size bound and curated content type/extension are re-checked before any byte is cached or served, whatever service implements the provider interface - TTS provider settings card commits the selection only after the POST succeeds (select disabled while saving, reload on error) - device/discover pages resolve the integration type before firing device requests, so config-only integrations redirect cleanly - the tts catalog category is hidden from non-admin users - mirrored unit tests for tts.getProviderConfiguration and tts.purgeExpiredAudios CI fix (Server test failure, 300+ cascading failures): - tts.getLocalApiBaseUrl.test.js called sinon.restore() on the DEFAULT sandbox in afterEach, untracking the fakes other test files create at load time: their sinon.reset() cleanup stopped clearing call history and every suite running after the file leaked call counts ("called twice" assertions, fake-timer timeouts). The stub now lives in a dedicated sinon sandbox (the documented test/models/index.test.js pattern). Full suite verified locally: only the environment-specific failures of this sandbox remain (outbound proxy vs nock, missing sqlite3 binary). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ihJhVk52zW6qUsxBHeqx2
…rovider-integration-oebx0g
There was a problem hiding this comment.
Stale comment
Re-review of
f6898f6(follow-up to the first Cursor pass).Verdict
Architecture matches B.19 well: provider-agnostic OpenAI-compatible relay, agentic loop stays in core, explicit admin selection, no silent Gladys Plus fallback, uninstall clears
AI_PROVIDER, AI stays admin-managed, and non-admin list/detail RBAC still filters tocommunicationonly. Previous blockers (B.18→B.19 renumber, living-spec banner, device/discover redirects, completionchoices[0].messagevalidation, concurrent-uninstall re-check) are addressed. CI is green including Codecov patch.Not approving: this is the highest-trust external-integration class (conversations + tool results + home/scene authority). Keeping
risk:high+needs:human-reviewfor a maintainer product pass.@atrovatois already requested.Remaining notes
- Trust warning coverage — the store install page warns for
type: "ai", but until the indexer acceptsai, GitHub/dev install is the practical path and shows no equivalent warning. Selection inAiProviderSettingsis immediate with no restated trust copy either (see inline).- AI page upsell — with Plus disconnected, the Gladys Plus upsell still dominates the AI page even when an external provider is selected and chat already works via
message.create(see inline).- Soft residuals (ok for human review, not blocking): instance-wide single provider for chat/intent/digest; companion indexer/SDK
onAiChatstill needed for catalog listing (graceful until then per Pierre’s note).No new brand-tied device categories/types in
server/utils/constants.js— onlyAI_PROVIDER+external-integration.ai.chat.Sent by Cursor Automation: Automatic PR review
…us upsell Cursor re-review follow-ups: - The trust warning shown on the store install screen never appears on the GitHub/dev install path, and selection is the moment conversations actually start flowing to the provider: the AI provider settings card now restates the warning whenever an external provider is selected - With an external provider active, the assistant already works without Gladys Plus: the Plus upsell and the Plus rate-limit note are hidden on the AI page in that case Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhBhvoV4jdZEhxgadWwXV8
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
front/src/routes/integration/all/openai/index.js (1)
141-141: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUpdate
externalProviderActiveafter provider selection.
getAiProvider()runs only incomponentDidMount().AiProviderSettings.updateProvider()updates the child state afterPOST /api/v1/ai_provider, butOpenAIGatewayreceives no update. Selecting an external provider therefore does not hide the upsell or rate-limit message on the current page. Resetting to Gladys Plus can leave those messages hidden until reload.Pass an
onProviderChangecallback toAiProviderSettings, or lift the selector state intoOpenAIGateway, and update it after the save succeeds.The supplied
front/src/routes/integration/all/openai/AiProviderSettings.jsx:13-52snippet shows that the child updates only its own state.🤖 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 `@front/src/routes/integration/all/openai/index.js` at line 141, Update OpenAIGateway’s provider-selection flow so externalProviderActive is refreshed after AiProviderSettings.updateProvider() successfully saves a provider. Pass an onProviderChange callback to AiProviderSettings or lift the state into OpenAIGateway, ensuring both external-provider selection and resetting to Gladys Plus immediately update the current page’s upsell and rate-limit messaging.
🤖 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 `@front/src/routes/integration/all/openai/index.js`:
- Around line 30-39: Update getAiProvider and the externalProviderActive
initialization to use null as the unknown state, preserving false only when the
provider response confirms no external provider. In the affected Gladys Plus
upsell and rate-limit render conditions, require externalProviderActive ===
false so messages remain hidden while the request is pending or fails, while
retaining the external-provider-active behavior.
---
Outside diff comments:
In `@front/src/routes/integration/all/openai/index.js`:
- Line 141: Update OpenAIGateway’s provider-selection flow so
externalProviderActive is refreshed after AiProviderSettings.updateProvider()
successfully saves a provider. Pass an onProviderChange callback to
AiProviderSettings or lift the state into OpenAIGateway, ensuring both
external-provider selection and resetting to Gladys Plus immediately update the
current page’s upsell and rate-limit messaging.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: eb8c09ac-7916-41e7-8be4-33710f1a231c
📒 Files selected for processing (9)
docs/specs/external-integrations.mdfront/src/config/i18n/de.jsonfront/src/config/i18n/en.jsonfront/src/config/i18n/fr.jsonfront/src/routes/integration/all/openai/AiProviderSettings.jsxfront/src/routes/integration/all/openai/index.jsserver/lib/external-integration/constants.jsserver/lib/external-integration/index.jsserver/test/lib/external-integration/testUtils.test.js
🚧 Files skipped from review as they are similar to previous changes (8)
- server/lib/external-integration/index.js
- server/test/lib/external-integration/testUtils.test.js
- server/lib/external-integration/constants.js
- front/src/config/i18n/en.json
- front/src/config/i18n/fr.json
- front/src/config/i18n/de.json
- front/src/routes/integration/all/openai/AiProviderSettings.jsx
- docs/specs/external-integrations.md
CodeRabbit follow-ups on the AI page: - externalProviderActive starts as null (unknown): the Plus upsell and rate-limit messages only render once the absence of an external provider is confirmed, instead of flashing while the request is pending or lying after a failed request - AiProviderSettings notifies the page through onProviderChange, so selecting or resetting the provider updates the messaging without a reload Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhBhvoV4jdZEhxgadWwXV8
There was a problem hiding this comment.
Re-review of bdbd983 (follow-up to the Cursor pass on f6898f6).
Verdict
Architecture still matches B.19 well: provider-agnostic OpenAI-compatible relay, agentic loop stays in core, explicit admin selection, no silent Gladys Plus fallback, uninstall clears AI_PROVIDER, AI stays admin-managed, non-admin list/detail RBAC still filters to communication only. Previous soft notes are fixed in ddd7a802 / bdbd983:
- trust warning restated under the provider select
- Plus upsell / rate-limit copy gated on confirmed absence of an external provider (
nullwhile loading/failed) - parent page kept in sync via
onProviderChange
CI is green (including Codecov patch). No brand-tied device categories/types — only AI_PROVIDER + external-integration.ai.chat.
Not approving. This remains the highest-trust external-integration class (conversations + tool results + home/scene authority via the tool-calling loop). Keeping risk:high + needs:human-review for a maintainer product pass. @atrovato is already requested.
Soft residual for human review
- Plus connected + external provider — when Gladys Plus is configured and an external provider is selected, the page still shows the Plus “chat enabled” success line,
AiQuotaDisplay, and Plus-only debug download. Quota especially is misleading once chat traffic no longer hits Plus (see inline). - Companion store indexer / SDK
onAiChatstill needed for catalog listing (graceful until then; GitHub/dev install remains the practical path). - Living-spec body: B.15–B.17 section titles still say “(design, phase 2)” while the banner correctly treats them as shipped — cosmetic inconsistency across parallel provider PRs, not a merge blocker for this change.
Sent by Cursor Automation: Automatic PR review
…tive With Gladys Plus connected and an external provider selected, the AI traffic no longer hits Plus: the Plus quota display and the 'chat enabled by default' banner were misleading in that state. Both are now gated on the confirmed absence of an external provider, like the upsell and rate-limit messages. The weekly digest and debug context download stay: their LLM calls follow the selected provider through gateway.aiChat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhBhvoV4jdZEhxgadWwXV8
…rovider-integration-oebx0g # Conflicts: # front/src/routes/integration/all/external-integration/discover-page/index.js
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
front/src/routes/integration/all/external-integration/discover-page/index.js (4)
133-149: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winKeep the post-create refresh on the originating page.
Capture
pageGenerationbefore the POST. If the selector changes while the POST is pending, return before callinggetDiscoveredDevices(). Otherwise, that method uses the new selector and updates the wrong page. After unmount, it can also callsetStatebefore its own generation guard runs.Proposed lifecycle guard
createDevice = async externalId => { + const generation = this.pageGeneration; const discoveredDevice = this.state.discoveredDevices.find(device => device.external_id === externalId); ... await this.props.httpClient.post('/api/v1/device', device); + if (generation !== this.pageGeneration) { + return; + } await this.getDiscoveredDevices(); };🤖 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 `@front/src/routes/integration/all/external-integration/discover-page/index.js` around lines 133 - 149, Update createDevice to capture the current pageGeneration before posting, then verify it is unchanged after the POST and return if the selector changed or the component is no longer current. Only call getDiscoveredDevices when the originating page generation remains valid, preventing refreshes against a different page or after unmount.
87-89: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not report success when the scan completion event is missing.
When the 60-second timer invokes
finishScan, the method setsRequestStatus.Successbut does not callgetDiscoveredDevices(). The page can show success with the old device list. A later POST failure is also ignored afterscanTokenchanges. Use an explicit timeout/error state, or perform an authoritative refresh before reporting success.Also applies to: 110-120
🤖 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 `@front/src/routes/integration/all/external-integration/discover-page/index.js` around lines 87 - 89, Update the scan timeout flow around finishScan so a missing scan completion event cannot set RequestStatus.Success with stale devices. On timeout, either set an explicit error/timeout status or fetch the authoritative device list with getDiscoveredDevices() before reporting success; ensure late POST failures after scanToken changes are not silently ignored.
20-22: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winCancel the debounced search when the page loses focus.
loadIntegrationPage()andcomponentWillUnmount()bumppageGenerationand clear the scan timer, but a pending debounce can still run and callsetStatewith the old selector. Usedebounce(...).clear()at those same points. If searching is page-scoped, resetdeviceSearchwhen the debounced call is cleared.🤖 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 `@front/src/routes/integration/all/external-integration/discover-page/index.js` around lines 20 - 22, Update loadIntegrationPage() and componentWillUnmount() to clear the pending debouncedSearchDevices invocation alongside the existing pageGeneration and scan-timer cleanup, using its debounce clear API. When clearing the page-scoped search, also reset deviceSearch so stale selector results cannot update state after focus loss or unmount.
58-74: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReject out-of-order device refresh responses.
pageGenerationchanges only when the selector changes. The websocket handler andcreateDevicecan start several requests for one generation. If an older request resolves after a newer request, Lines 67 and 73 overwrite the current list or status with stale data. Track a request sequence token and check it in both the success and error paths.Proposed request-order guard
this.scanToken = 0; + this.discoveredDevicesRequestToken = 0; getDiscoveredDevices = async () => { const generation = this.pageGeneration; + const requestToken = ++this.discoveredDevicesRequestToken; this.setState({ getDiscoveredDevicesStatus: RequestStatus.Getting }); ... - if (generation !== this.pageGeneration) { + if ( + generation !== this.pageGeneration || + requestToken !== this.discoveredDevicesRequestToken + ) { return; } ... - if (generation !== this.pageGeneration) { + if ( + generation !== this.pageGeneration || + requestToken !== this.discoveredDevicesRequestToken + ) { 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 `@front/src/routes/integration/all/external-integration/discover-page/index.js` around lines 58 - 74, Update the discovered-device refresh flow around the request in the component method containing pageGeneration to track a monotonically increasing request sequence token for every request, including websocket and createDevice-triggered refreshes within the same generation. Capture the token before awaiting httpClient.get, and require both the generation and token to still match before applying discoveredDevices or GetDiscoveredDevicesStatus in the success and error paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@front/src/routes/integration/all/external-integration/discover-page/index.js`:
- Around line 133-149: Update createDevice to capture the current pageGeneration
before posting, then verify it is unchanged after the POST and return if the
selector changed or the component is no longer current. Only call
getDiscoveredDevices when the originating page generation remains valid,
preventing refreshes against a different page or after unmount.
- Around line 87-89: Update the scan timeout flow around finishScan so a missing
scan completion event cannot set RequestStatus.Success with stale devices. On
timeout, either set an explicit error/timeout status or fetch the authoritative
device list with getDiscoveredDevices() before reporting success; ensure late
POST failures after scanToken changes are not silently ignored.
- Around line 20-22: Update loadIntegrationPage() and componentWillUnmount() to
clear the pending debouncedSearchDevices invocation alongside the existing
pageGeneration and scan-timer cleanup, using its debounce clear API. When
clearing the page-scoped search, also reset deviceSearch so stale selector
results cannot update state after focus loss or unmount.
- Around line 58-74: Update the discovered-device refresh flow around the
request in the component method containing pageGeneration to track a
monotonically increasing request sequence token for every request, including
websocket and createDevice-triggered refreshes within the same generation.
Capture the token before awaiting httpClient.get, and require both the
generation and token to still match before applying discoveredDevices or
GetDiscoveredDevicesStatus in the success and error paths.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 779faed4-9e1c-4afc-8dbf-49f61342e7c5
📒 Files selected for processing (8)
docs/specs/external-integrations.mdfront/src/config/i18n/de.jsonfront/src/config/i18n/en.jsonfront/src/config/i18n/fr.jsonfront/src/routes/integration/all/external-integration/discover-page/index.jsfront/src/routes/integration/all/openai/index.jsserver/lib/external-integration/constants.jsserver/utils/constants.js
🚧 Files skipped from review as they are similar to previous changes (6)
- server/lib/external-integration/constants.js
- front/src/config/i18n/de.json
- server/utils/constants.js
- docs/specs/external-integrations.md
- front/src/routes/integration/all/openai/index.js
- front/src/config/i18n/fr.json


External integrations can now declare type "ai" and serve as the AI
backend of the assistant instead of Gladys Plus. The contract is fully
provider-agnostic: the whole agentic loop (MCP tool calling, prompts,
history) stays in the core, and the integration only receives
OpenAI-compatible chat completion requests over the integration
WebSocket (external-integration.ai.chat, 120s ack) and answers with
OpenAI-compatible completions — a thin adapter to Claude, DeepSeek,
OpenAI, a local LLM or any other provider.
Server:
(chat loop, intent router, weekly digest) to the selected provider,
with the Gladys Plus model field stripped and no silent fallback
uninstall of the selected provider
Front:
Spec: docs/specs/external-integrations.md B.18 + C.1/C.4/C.5/C.8
updated in the same diff (spec-first rule).
Community request: https://community.gladysassistant.com/t/10421
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01XhBhvoV4jdZEhxgadWwXV8
Summary by CodeRabbit