Skip to content

feat(librechat): full API integration — server-side conversations, search, tags, sharing, memories, presets#28

Open
itsablabla wants to merge 10 commits intomainfrom
devin/1776097663-librechat-full-integration
Open

feat(librechat): full API integration — server-side conversations, search, tags, sharing, memories, presets#28
itsablabla wants to merge 10 commits intomainfrom
devin/1776097663-librechat-full-integration

Conversation

@itsablabla
Copy link
Copy Markdown
Owner

@itsablabla itsablabla commented Apr 13, 2026

Summary

Replaces localStorage-based conversation persistence with full LibreChat server-side API integration across all 3 tiers. This is the single biggest quality improvement — conversations now sync across devices, survive cache clears, and include full tool call history in MongoDB.

Tier 1: Server-side persistence (core)

  • Conversations: store.js loads from /api/convos instead of localStorage. ChatView.vue loads message history from /api/messages/{id} on conversation switch.
  • Search: SearchView.vue uses debounced server-side search via /api/messages?search= (MeiliSearch full-text) plus instant local search for conversations/tools/workspaces.
  • JWT Auth: OpenClawService.php implements dual auth — API key for Agents API, JWT for internal APIs. Tokens cached with 14min TTL, auto-refresh on 401.
  • Sidebar: Conversations grouped by Today/Yesterday/This Week/Older with relative timestamps and "Load more..." pagination.

Tier 2: New capabilities (backend fully wired)

  • File attachments via /api/files
  • Conversation sharing via /api/share
  • Agent memory via /api/memories
  • Tags via /api/tags (workspace mapping)

Tier 3: Polish (backend fully wired)

  • Presets via /api/presets
  • Archive conversations via /api/convos/archive
  • Auto-generated titles via /api/convos/gen_title

Infrastructure

  • 30+ new PHP routes in routes.php
  • 15+ new proxy methods in AgentController.php
  • Complete frontend API client in api.js
  • Fixed JWT 401 retry bug (was corrupting Content-Type header via array_search returning false)
  • Service account credentials externalized to Nextcloud app config (admin settings UI)
  • Version bump to 0.5.0

Devin Review fixes (0743d62)

  1. Hardcoded credentials removedSERVICE_EMAIL/SERVICE_PASSWORD constants replaced with librechat_service_email/librechat_service_password app config keys, configurable via Admin → Jada Agent settings
  2. array_search false bug fixed — 401 retry path now safely checks $authIndex !== false before array assignment, with fallback to append
  3. Per-user isolation documented — see Known Limitations below

Known Limitations

  • Single service account: All Nextcloud users share one LibreChat identity (jada@nextcloud.local). This means conversation data is not isolated per-user. For single-admin setups this is fine. Multi-user isolation requires either per-user LibreChat account provisioning or OIDC SSO integration (planned).
  • Tier 2-3 frontend: Backend routes are fully wired but frontend UI components (file upload, sharing button, memory viewer, presets picker) are not yet built.
  • Admin must configure credentials: After deploying, the admin must set the LibreChat service password in Nextcloud Admin → Jada Agent settings before JWT-authed endpoints work.

Review & Testing Checklist for Human

  • Set service password in admin settings — Navigate to Nextcloud Admin → Jada Agent, enter the LibreChat service account email and password
  • Verify conversation list loads from server — Open Jada Agent → sidebar should show conversations from LibreChat
  • Verify send + persist — Send a message → reload page → conversation should appear in sidebar and messages should reload from server
  • Verify search — Type in Search view → should show server-side results plus local results
  • Test on mobile — Sidebar conversation list, chat input, and streaming should all work on iOS Safari

Notes

  • The service account (jada@nextcloud.local) must exist in LibreChat's MongoDB with role: ADMIN and email verified
  • Tier 2 and 3 frontend components will be built in a follow-up PR once Tier 1 is verified working end-to-end

Link to Devin session: https://app.devin.ai/sessions/f4e16f12bfa34fc6bdc3d73a833a5d91
Requested by: @itsablabla


Open with Devin

- PHP proxy now calls LibreChat's OpenAI-compatible API at /api/agents/v1/chat/completions
- Model changed from 'hermes-agent' to 'agent_jada_nextcloud' (LibreChat agent with 118 MCP tools)
- Default URL changed to http://LibreChat:3080 (Docker DNS on nextcloud-aio network)
- Vue SSE parser handles standard OpenAI delta.tool_calls format (LibreChat)
- Tool names stripped of _mcp_serverName suffix for display (e.g. nc_webdav_list_directory_mcp_nextcloud → nc_webdav_list_directory)
- Backward compat: legacy hermes.tool.progress events still parsed
- Version bumped to 0.4.0
- Updated info.xml description to reflect LibreChat architecture
Fixes Devin Review finding: /_mcp_[^_]+$/ fails for server names
containing underscores (e.g. my_server). Changed to /_mcp_.+$/ which
greedily matches everything after the _mcp_ delimiter.
- getSkills(): return empty list (LibreChat has no skills endpoint)
- getModels(): proxy to LibreChat's /api/agents/v1/models
- getConfig(): return stub with engine info (config managed via LibreChat admin UI)
- getSessions(): return empty list (LibreChat manages sessions internally)

Fixes Devin Review finding: these 4 methods still used Hermes-specific
/api/v1/* paths that don't exist on LibreChat.
- AdminSettings.php: default URL http://localhost:18789 → http://LibreChat:3080
- SettingsController.php: same default URL update
- templates/admin.php: placeholder URL + heading updated to LibreChat

Fixes Devin Review finding: admin UI showed stale Hermes defaults.
…ations, search, tags, sharing, memories, presets

Tier 1: Server-side persistence
- Replace localStorage with LibreChat /api/convos + /api/messages
- JWT auth with 14min TTL caching and auto-refresh on 401
- Service account (jada@nextcloud.local) for internal API access
- Full-text search via /api/messages?search= (MeiliSearch)
- Conversation list grouped by Today/Yesterday/This Week/Older

Tier 2: New capabilities (backend wired)
- File attachments via /api/files
- Conversation sharing via /api/share
- Agent memory via /api/memories
- Tags via /api/tags

Tier 3: Polish (backend wired)
- Presets via /api/presets
- Archive conversations via /api/convos/archive
- Auto-generated titles via /api/convos/gen_title

Changes:
- OpenClawService.php: dual auth (API key + JWT), token caching, 401 retry
- AgentController.php: 15+ new proxy methods for all LibreChat APIs
- routes.php: 30+ new routes organized by domain
- api.js: complete frontend API client for all endpoints
- store.js: server-side state management replacing localStorage
- ChatView.vue: loads messages from server, no localStorage
- SearchView.vue: debounced server-side + instant local search
- App.vue: grouped conversation sidebar with relative timestamps
- Version bump to 0.5.0
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread lib/Service/OpenClawService.php Outdated
Comment on lines +200 to +201
$result = $this->openClaw->jwtGet('/api/convos?' . $params);
return new JSONResponse($result);
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.

🔴 All Nextcloud users share a single LibreChat service account — no per-user data isolation for JWT-authed endpoints

All 21 JWT-authed endpoints (conversations, messages, search, tags, sharing, memories, presets) authenticate to LibreChat using a single shared service account JWT (jada@nextcloud.local). Unlike the API-key-authed endpoints (health, chat, models) which pass X-Nextcloud-User headers via getUserHeaders(), none of the jwtGet/jwtPost/jwtDelete calls include any user identity. This means LibreChat treats all requests as the same user, so all Nextcloud users see each other's conversations, messages, memories, tags, and presets. This directly contradicts the stated feature of "Per-User Isolation".

Example: getConversations passes no user identity

In lib/Controller/AgentController.php:200, jwtGet is called without user headers:

$result = $this->openClaw->jwtGet('/api/convos?' . $params);

Compare with the API-key-authed health() at line 53 which does include user headers:

$result = $this->openClaw->get($apiPath . '/models', $this->getUserHeaders());
Prompt for agents
The fundamental issue is that all JWT-authenticated LibreChat API calls go through a single shared service account with no per-user scoping. Every call to jwtGet/jwtPost/jwtDelete in AgentController.php needs to include user identity so LibreChat can scope data per user. There are two main approaches to fix this:

1. Per-user JWT approach: Instead of a single shared JWT, obtain and cache a separate JWT for each Nextcloud user. This requires creating individual LibreChat accounts per Nextcloud user (or mapping them). The JWT cache in app config would need to be keyed per user.

2. Shared JWT + user header approach: If LibreChat supports user scoping via request headers (similar to how X-Nextcloud-User works for the Agents API), pass getUserHeaders() as extra headers in all jwtGet/jwtPost/jwtDelete calls. This would require the controller methods to call e.g. $this->openClaw->jwtGet('/api/convos?' . $params, $this->getUserHeaders()) instead of $this->openClaw->jwtGet('/api/convos?' . $params).

All 21 JWT-authed endpoints in AgentController.php are affected: getConversations, getConversation, deleteConversation, updateConversation, archiveConversation, genTitle, getMessages, getConversationMessages, searchMessages, getTags, addTag, removeTag, getSharedLinks, createShareLink, deleteShareLink, getMemories, deleteMemory, getPresets, createPreset, deletePreset, getConversationToolCalls.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Valid architectural concern. This is a known limitation of the current "shared service account" approach — all Nextcloud users share one LibreChat identity (jada@nextcloud.local).

Why it's this way for now: LibreChat's internal APIs (/api/convos, /api/messages, etc.) scope data by the authenticated user's JWT. There's no header-based user impersonation like the Agents API supports. To get true per-user isolation, we need one of:

  1. Per-user LibreChat accounts — auto-provision a LibreChat user per Nextcloud user on first login, cache per-user JWTs keyed by NC uid. Most correct but requires user provisioning logic.
  2. OIDC SSO — connect Nextcloud as an OIDC provider to LibreChat so each NC user gets their own LibreChat session natively. This is the planned long-term fix.
  3. MongoDB query scoping — add a custom header that LibreChat reads for data scoping (would require LibreChat core changes, not viable).

For single-admin setups (which is the current deployment), this is a non-issue. For multi-user, option 1 is the pragmatic next step and option 2 is the proper solution. I'll note this in the PR description as a known limitation.

Comment thread lib/Service/OpenClawService.php Outdated
…ation, array_search bug

1. Move hardcoded service account credentials to Nextcloud app config
   - Removed SERVICE_EMAIL and SERVICE_PASSWORD constants
   - Credentials now read from librechat_service_email and librechat_service_password app config keys
   - Exposed in AdminSettings.php and SettingsController.php for admin UI configuration
   - Changing password auto-clears cached JWT to force re-auth

2. Fix array_search returning false on 401 retry
   - When original token was empty, array_search returned false (coerced to 0)
   - This overwrote Content-Type header instead of adding Authorization
   - Now uses safe $authIndex !== false check with fallback to array append

3. Per-user isolation noted as architectural limitation
   - Single service account JWT means all Nextcloud users share one LibreChat user
   - Documented as known limitation; proper fix requires per-user LibreChat accounts
     or OIDC SSO integration (planned for future)
devin-ai-integration[bot]

This comment was marked as resolved.

Under strict_types=1, curl_exec returning false would throw TypeError
when passed to json_decode(). Added the same false guard that exists
in the main request flow.
devin-ai-integration[bot]

This comment was marked as resolved.

…sthrough

1. Admin template + JS: Added form fields for librechat_service_email and
   librechat_service_password so JWT auth is configurable without DB access.
   Updated labels from OpenClaw to LibreChat.

2. chatSSE: Pass conversationId from request body to LibreChat payload so
   messages append to existing conversations instead of creating new ones.
devin-ai-integration[bot]

This comment was marked as resolved.

ChatView watcher on activeConversationId already calls loadServerMessages(),
so calling loadConversationMessages() in selectConversation caused two
identical network requests per conversation selection.
devin-ai-integration[bot]

This comment was marked as resolved.

1. ICredentialsManager for service password — stored encrypted at rest
   instead of plaintext in appconfig. Auto-migrates legacy plaintext on
   first read. AdminSettings + SettingsController updated to match.

2. skipNextWatcherLoad flag — prevents ChatView watcher from re-fetching
   messages when SSE streaming just created a new conversation. The watcher
   would race against server indexing and overwrite visible messages with
   empty/incomplete data.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 23 additional findings in Devin Review.

Open in Devin Review

Comment on lines +57 to +58
$agentName = $models[0]['name'] ?? 'Jada';
$provider = $models[0]['provider'] ?? 'Gemini';
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.

🟡 Accessing $models[0] on empty array generates PHP 8 warnings in health endpoint

When the LibreChat /models endpoint returns no models (e.g., backend is down, misconfigured, or API key is missing), $models is []. Lines 57-58 access $models[0]['name'] and $models[0]['provider'] directly, which generates two PHP 8 warnings: "Undefined array key 0" followed by "Trying to access array offset on null". The ?? fallback values work correctly so the response is fine, but the warnings pollute logs and will fire on every health check (called every 30 seconds by the frontend).

Note that $agentName is assigned but never used — only $provider feeds into the response at line 68.

Suggested change
$agentName = $models[0]['name'] ?? 'Jada';
$provider = $models[0]['provider'] ?? 'Gemini';
$firstModel = $models[0] ?? [];
$agentName = $firstModel['name'] ?? 'Jada';
$provider = $firstModel['provider'] ?? 'Gemini';
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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