Skip to content

feat(web-search): Keenable searchProfile end-to-end (config + per-user dialog)#2

Open
dtaylor072 wants to merge 20 commits into
mainfrom
feat/keenable-search-profile
Open

feat(web-search): Keenable searchProfile end-to-end (config + per-user dialog)#2
dtaylor072 wants to merge 20 commits into
mainfrom
feat/keenable-search-profile

Conversation

@dtaylor072

Copy link
Copy Markdown

Summary

  • Adds a searchProfile selector that flows from the LibreChat frontend through to Keenable's POST /v1/search as the profile body field, picking which upstream search engine Keenable uses.
  • Server-default via librechat.yaml, per-user override via the existing search settings dialog.
  • Companion to keenableai/agents PR for feat/search-profile.

Profiles supported

default, keenable-gq, google, bing, brave, brave_llm, exa, exa_instant, tavily, tavily_fast, tavily_ultra_fast, perplexity, perplexity_pro, parallel, parallel_advanced, yandex — wire values verified live (all 16 return 200). Fork-incompatible kscraper variants intentionally excluded (verified to 400/500/timeout server-side).

Files

Schema / backend:

  • packages/data-provider/src/config.ts: SearchProfiles enum + searchProfile field on webSearchSchema (z.string with ${KEENABLE_SEARCH_PROFILE} env-var default so per-user overrides plumb through extractWebSearchEnvVars).
  • packages/data-schemas/src/app/web.ts: searchProfile registered as optional under webSearchAuth.providers.keenable so the standard loadAuthValues flow resolves the user's stored value.
  • packages/data-schemas/src/types/web.ts: 'searchProfile' added to TWebSearchKeys.
  • packages/data-schemas/src/app/web.ts: loadWebSearchConfig defaults searchProfile to the env-var ref.
  • packages/api/src/web/web.ts: dispatcher now propagates authResult.searchProfile via the standard category iteration (earlier ad-hoc propagation block removed).

Frontend dialog:

  • client/src/hooks/Plugins/useAuthSearchTool.ts: searchProfile added to SearchApiKeyFormData and the install auth dict.
  • client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx: new "Search Profile" InputSection (renders only when searchProvider is keenable); setValue prop wires the dropdown into the RHF form so the selected value is submitted.
  • client/src/components/SidePanel/Agents/Search/Action.tsx and client/src/components/Chat/Input/ToolDialogs.tsx: pass setValue down.

Yaml docs:

  • librechat.example.yaml: documents the new searchProfile field.

Test plan

  • After agents PR merges and @librechat/agents@3.1.68-keenable.X is published: bump dep in this repo's api/package.json.
  • Build and rebuild image: docker compose pull && up -d on the librechat GCE VM.
  • Verify live: open dialog with searchProvider: keenable, see new "Search Profile" dropdown, switch values, confirm subsequent searches send the selected profile body field.

🤖 Generated with Claude Code

…r dialog)

Adds a `searchProfile` selector that flows from the LibreChat frontend
through to Keenable's POST /v1/search as the `profile` body field, picking
which upstream search engine Keenable uses (default, google, bing, brave,
exa, exa_instant, tavily, tavily_fast, tavily_ultra_fast, perplexity,
perplexity_pro, parallel, parallel_advanced, brave_llm, keenable-gq,
yandex). Wire values verified live against api.keenable.ai (all 16 return
200). Fork-incompatible kscraper variants intentionally excluded.

Schema / backend:
- packages/data-provider/src/config.ts: SearchProfiles enum + searchProfile
  field on webSearchSchema (z.string with KEENABLE_SEARCH_PROFILE env-var
  default so per-user overrides plumb through extractWebSearchEnvVars).
- packages/data-schemas/src/app/web.ts: searchProfile registered as
  optional (0) under webSearchAuth.providers.keenable so the standard
  loadAuthValues flow resolves the user's stored value.
- packages/data-schemas/src/types/web.ts: 'searchProfile' added to
  TWebSearchKeys.
- packages/data-schemas/src/app/web.ts: loadWebSearchConfig defaults
  searchProfile to the env-var ref.
- packages/api/src/web/web.ts: existing dispatcher now propagates
  authResult.searchProfile via the standard category iteration; the
  earlier custom propagation block is removed.

Frontend dialog:
- client/src/hooks/Plugins/useAuthSearchTool.ts: searchProfile added to
  SearchApiKeyFormData and the install auth dict.
- client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx: new
  "Search Profile" InputSection that renders only when searchProvider is
  keenable; setValue prop wires the dropdown into the RHF form so the
  selected value is submitted.
- client/src/components/SidePanel/Agents/Search/Action.tsx and
  client/src/components/Chat/Input/ToolDialogs.tsx: pass setValue down.

Yaml docs:
- librechat.example.yaml: documents the new searchProfile field.

Companion change in @librechat/agents (keenableai/agents#feat/search-profile)
adds searchProfile through SearchConfig -> createKeenableSearchAPI and
includes it as `profile` in the POST body when set.
@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add Keenable searchProfile end-to-end configuration and UI

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Adds searchProfile selector for Keenable upstream search engine selection
• Flows from frontend dialog through backend config to Keenable API profile field
• Supports 16 verified search profiles (google, bing, brave, exa, tavily, perplexity, etc.)
• Server-default via librechat.yaml, per-user override via search settings dialog
Diagram
flowchart LR
  A["Frontend Dialog"] -->|"searchProfile selection"| B["useAuthSearchTool Hook"]
  B -->|"SearchApiKeyFormData"| C["ApiKeyDialog Component"]
  C -->|"setValue propagation"| D["React Hook Form"]
  D -->|"searchProfile value"| E["Backend Config"]
  E -->|"env-var default"| F["webSearchSchema"]
  F -->|"loadWebSearchConfig"| G["Keenable API"]
  G -->|"profile field"| H["Upstream Engine Selection"]
Loading

Grey Divider

File Changes

1. packages/data-provider/src/config.ts ✨ Enhancement +27/-0

Define SearchProfiles enum and schema

• Added SearchProfiles enum with 16 supported upstream search engines (default, google, bing,
 brave, exa, tavily, perplexity, parallel, yandex, etc.)
• Added searchProfile field to webSearchSchema with ${KEENABLE_SEARCH_PROFILE} env-var default
• Added searchProfile optional field to TStartupConfig.webSearch type

packages/data-provider/src/config.ts


2. packages/data-schemas/src/app/web.ts ✨ Enhancement +4/-0

Register searchProfile in web auth config

• Registered searchProfile as optional field under webSearchAuth.providers.keenable
• Added searchProfile to loadWebSearchConfig function with env-var default fallback
• Included searchProfile in returned config object

packages/data-schemas/src/app/web.ts


3. packages/data-schemas/src/types/web.ts ✨ Enhancement +2/-1

Add searchProfile to web search keys type

• Added 'searchProfile' to TWebSearchKeys union type

packages/data-schemas/src/types/web.ts


View more (5)
4. client/src/hooks/Plugins/useAuthSearchTool.ts ✨ Enhancement +3/-0

Add searchProfile to form data type

• Added searchProfile field to SearchApiKeyFormData type
• Included searchProfile in the install auth dictionary spread

client/src/hooks/Plugins/useAuthSearchTool.ts


5. client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx ✨ Enhancement +41/-2

Implement searchProfile dropdown UI component

• Imported SearchProfiles enum and UseFormSetValue from react-hook-form
• Added setValue prop to component interface (optional)
• Added selectedProfile state initialized from config or default
• Created profileOptions array from SearchProfiles enum values
• Added profile to dropdownOpen state object
• Implemented handleProfileChange handler with setValue call
• Added useEffect to sync selectedProfile to form via setValue
• Added conditional "Search Profile" InputSection that renders only when provider is keenable

client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx


6. client/src/components/Chat/Input/ToolDialogs.tsx ✨ Enhancement +1/-0

Pass setValue to search dialog

• Added setValue={searchMethods.setValue} prop to SearchApiKeyDialog component

client/src/components/Chat/Input/ToolDialogs.tsx


7. client/src/components/SidePanel/Agents/Search/Action.tsx ✨ Enhancement +1/-0

Pass setValue to API key dialog

• Added setValue={keyFormMethods.setValue} prop to ApiKeyDialog component

client/src/components/SidePanel/Agents/Search/Action.tsx


8. librechat.example.yaml 📝 Documentation +6/-0

Document searchProfile YAML configuration

• Added documentation for searchProfile configuration field
• Listed all 16 valid profile values with examples
• Noted that field is optional and only meaningful when searchProvider: keenable
• Documented that value is forwarded as profile field on Keenable API POST request

librechat.example.yaml


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Hardcoded Search Profile title 📘 Rule violation ⚙ Maintainability
Description
The new dialog section title is hardcoded (Search Profile) instead of using useLocalize(), which
breaks localization requirements for user-facing text.
Code

client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[R266-270]

+              {/* Search Profile Section (only meaningful when provider is keenable) */}
+              {selectedProvider === SearchProviders.KEENABLE && (
+                <InputSection
+                  title="Search Profile"
+                  selectedKey={selectedProfile}
Evidence
PR Compliance ID 16 requires all user-facing UI text to use useLocalize(). The added
InputSection uses a hardcoded title string rather than a localization key.

CLAUDE.md
client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[266-270]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new user-facing UI string (`Search Profile`) is hardcoded instead of being routed through `useLocalize()`.

## Issue Context
This dialog already uses `useLocalize()` for other titles; the new section should follow the same localization pattern and add/update the English source-of-truth key.

## Fix Focus Areas
- client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[266-270]
- client/src/locales/en/translation.json

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Literal YAML profile ignored 🐞 Bug ≡ Correctness
Description
The example config documents searchProfile: 'google', but the web search auth resolver only loads
values when the config string is an env-var placeholder (${...}), so a literal profile will not be
included in authResult and won’t be propagated.
Code

librechat.example.yaml[R588-593]

+#   # Keenable search profile (optional, only when searchProvider: keenable).
+#   # Forwarded as the `profile` field on POST /v1/search.
+#   # Valid: default, keenable-gq, google, bing, brave, brave_llm, exa,
+#   #        exa_instant, tavily, tavily_fast, tavily_ultra_fast, perplexity,
+#   #        perplexity_pro, parallel, parallel_advanced, yandex
+#   searchProfile: 'google'
Evidence
The YAML example suggests a literal value. However extractWebSearchEnvVars only collects auth
fields when extractVariableName() can parse a ${ENV_VAR} placeholder, and
extractVariableName() returns null for literals like google; therefore searchProfile won’t be
loaded via loadAuthValues and won’t appear in the resolved auth output.

librechat.example.yaml[574-594]
packages/api/src/web/web.ts[49-74]
packages/data-provider/src/utils.ts[27-35]
packages/data-schemas/src/app/web.ts[80-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`librechat.example.yaml` documents `searchProfile: 'google'`, but the backend web search auth/config resolution only recognizes env-var placeholders (`${KEENABLE_SEARCH_PROFILE}` style). As a result, operators following the example will not get a `searchProfile` value propagated.

### Issue Context
`extractWebSearchEnvVars()` uses `extractVariableName()` which only matches `${ENV_VAR}`; literals (e.g., `google`) are ignored.

### Fix Focus Areas
- librechat.example.yaml[574-594]
- packages/api/src/web/web.ts[49-74]
- packages/data-provider/src/utils.ts[27-35]

### Suggested fix options (pick one)
1) **Documentation-only** (minimal): change the YAML example to use `${KEENABLE_SEARCH_PROFILE}` and instruct setting `KEENABLE_SEARCH_PROFILE=google` via environment.
2) **Support literals in backend** (behavioral): extend the web-search config/auth resolution so optional fields like `searchProfile` can be taken directly from `webSearchConfig.searchProfile` when it’s a non-placeholder literal, while still allowing per-user overrides when placeholders are used.

Implementing option (2) should include tests to cover both `${KEENABLE_SEARCH_PROFILE}` and literal `google` configs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Placeholder profile saved🐞 Bug ≡ Correctness
Description
ApiKeyDialog copies startup config’s searchProfile into RHF via setValue, so when the startup
config contains the default placeholder "${KEENABLE_SEARCH_PROFILE}", submitting can store that
literal string as the user override and later send it as an invalid Keenable profile.
Code

client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[R197-199]

+  useEffect(() => {
+    setValue?.('searchProfile', selectedProfile);
+  }, [setValue, selectedProfile]);
Evidence
Startup config defaults searchProfile to the env-var placeholder string, and the dialog
initializes state from that config value and writes it into the form state; this makes the
placeholder eligible to be persisted as user auth input.

packages/data-provider/src/config.ts[953-969]
packages/data-schemas/src/app/web.ts[80-112]
client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[45-56]
client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[192-200]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ApiKeyDialog` initializes `selectedProfile` from `config.webSearch.searchProfile` and then writes it into react-hook-form via `setValue`. Because the startup config commonly defaults this field to the placeholder string `${KEENABLE_SEARCH_PROFILE}`, the dialog can persist that literal placeholder as a per-user override.

### Issue Context
Other webSearch config fields use `${ENV_VAR}` placeholders, but those placeholders are never directly written into user-submitted form fields. The new dropdown is different: it turns the placeholder into a user value.

### Fix Focus Areas
- client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[45-56]
- client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[192-200]
- packages/data-provider/src/config.ts[953-969]

### Suggested fix approach
- Treat `${...}` values from startup config as "system default" (i.e., do **not** submit them as a user override).
- Add an explicit dropdown option like `key: ''` / label "System default".
- Initialize `selectedProfile` to `''` when `config.webSearch.searchProfile` is an env placeholder.
- Ensure the submitted value is `''` for system default (so it’s filtered out by the existing `if (value)` reducer), and only submit real profile strings when the user explicitly selects one.
- Do not auto-write `${...}` placeholders into RHF via `setValue`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Enum JSDoc is narrational 📘 Rule violation ⚙ Maintainability
Description
The new SearchProfiles enum JSDoc includes runtime verification details (`verified live, all 16
return 200`), which is narrational and likely to become stale.
Code

packages/data-provider/src/config.ts[R922-927]

+/**
+ * Keenable search profiles. Forwarded as the `profile` field on POST /v1/search
+ * when `searchProvider` is `keenable`. Each value selects a different upstream
+ * search engine on the Keenable backend; values match the wire format the
+ * Keenable API expects (verified live, all 16 return 200).
+ */
Evidence
PR Compliance ID 13 discourages excessive/needless JSDoc and narrational comments. The added JSDoc
contains non-essential, time-sensitive verification info rather than documentation needed for
IntelliSense or complex logic.

CLAUDE.md
packages/data-provider/src/config.ts[922-927]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New JSDoc for `SearchProfiles` includes narrational/time-sensitive statements that can become stale.

## Issue Context
Keep JSDoc focused on what the enum is and how it maps to the API, without embedding live-test results.

## Fix Focus Areas
- packages/data-provider/src/config.ts[922-927]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Narrational searchProfile comment 📘 Rule violation ⚙ Maintainability
Description
A new standalone comment explains obvious mapping details in SearchApiKeyFormData, adding noise
and maintenance burden.
Code

client/src/hooks/Plugins/useAuthSearchTool.ts[R20-21]

+  // Keenable upstream-engine selector (sent as `profile` on POST /v1/search)
+  searchProfile: string;
Evidence
PR Compliance ID 13 discourages narrational inline comments when code is self-explanatory. The added
comment restates behavior that can be inferred from the field name and surrounding code paths.

CLAUDE.md
client/src/hooks/Plugins/useAuthSearchTool.ts[20-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new standalone comment is narrational and may be unnecessary.

## Issue Context
Prefer self-documenting names; only keep documentation if it adds non-obvious constraints/usage.

## Fix Focus Areas
- client/src/hooks/Plugins/useAuthSearchTool.ts[20-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

…branch

Pins to commit a6d89c8 on keenableai/agents so the searchProfile plumbing
ships in this image build. The fork's prepare script builds dist/ when
installed from a git URL, so no npm publish is required.

Once a 3.1.68-keenable.X is published to npm with this work merged,
this pin should be replaced with a normal semver dependency.
Comment on lines +266 to +270
{/* Search Profile Section (only meaningful when provider is keenable) */}
{selectedProvider === SearchProviders.KEENABLE && (
<InputSection
title="Search Profile"
selectedKey={selectedProfile}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Hardcoded search profile title 📘 Rule violation ⚙ Maintainability

The new dialog section title is hardcoded (Search Profile) instead of using useLocalize(), which
breaks localization requirements for user-facing text.
Agent Prompt
## Issue description
A new user-facing UI string (`Search Profile`) is hardcoded instead of being routed through `useLocalize()`.

## Issue Context
This dialog already uses `useLocalize()` for other titles; the new section should follow the same localization pattern and add/update the English source-of-truth key.

## Fix Focus Areas
- client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx[266-270]
- client/src/locales/en/translation.json

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx
Comment thread librechat.example.yaml
Comment on lines +588 to +593
# # Keenable search profile (optional, only when searchProvider: keenable).
# # Forwarded as the `profile` field on POST /v1/search.
# # Valid: default, keenable-gq, google, bing, brave, brave_llm, exa,
# # exa_instant, tavily, tavily_fast, tavily_ultra_fast, perplexity,
# # perplexity_pro, parallel, parallel_advanced, yandex
# searchProfile: 'google'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Literal yaml profile ignored 🐞 Bug ≡ Correctness

The example config documents searchProfile: 'google', but the web search auth resolver only loads
values when the config string is an env-var placeholder (${...}), so a literal profile will not be
included in authResult and won’t be propagated.
Agent Prompt
### Issue description
`librechat.example.yaml` documents `searchProfile: 'google'`, but the backend web search auth/config resolution only recognizes env-var placeholders (`${KEENABLE_SEARCH_PROFILE}` style). As a result, operators following the example will not get a `searchProfile` value propagated.

### Issue Context
`extractWebSearchEnvVars()` uses `extractVariableName()` which only matches `${ENV_VAR}`; literals (e.g., `google`) are ignored.

### Fix Focus Areas
- librechat.example.yaml[574-594]
- packages/api/src/web/web.ts[49-74]
- packages/data-provider/src/utils.ts[27-35]

### Suggested fix options (pick one)
1) **Documentation-only** (minimal): change the YAML example to use `${KEENABLE_SEARCH_PROFILE}` and instruct setting `KEENABLE_SEARCH_PROFILE=google` via environment.
2) **Support literals in backend** (behavioral): extend the web-search config/auth resolution so optional fields like `searchProfile` can be taken directly from `webSearchConfig.searchProfile` when it’s a non-placeholder literal, while still allowing per-user overrides when placeholders are used.

Implementing option (2) should include tests to cover both `${KEENABLE_SEARCH_PROFILE}` and literal `google` configs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

David Taylor added 18 commits April 30, 2026 17:27
…-authenticated

The ApiKeyDialog trigger (key icon next to Web Search in agent builder) was
gated solely on isUserProvided — true only when at least one auth category
needs user input. With KEENABLE_API_KEY in env, all categories are
SYSTEM_DEFINED and the icon never rendered, leaving no way for users to
open the new Search Profile dropdown.

Now the icon also shows when searchProvider is keenable so the profile
picker is reachable. No change for non-keenable providers.
…ovider

Mirrors the Action.tsx gating fix: the chat-input ToolsDropdown's gear
icon next to "Web Search" was hidden when all auth categories were
SYSTEM_DEFINED, blocking access to the new searchProfile picker. Now
also shows when searchProvider is keenable so users can open the dialog
and pick a profile from chat.
Cerebras/Baseten emit usage on every streaming chunk; langchain-core
warns each time it can't merge total_tokens/completion_tokens, producing
hundreds of harmless 'field[X] already exists in this message chunk and
value has unsupported type' lines per response. Patch console.warn at
api entrypoint to drop just those.
…r-user

Bug: with multiple tabs open, the searchProfile selected in one tab
clobbered the others because the dialog wrote to per-user plugin auth
(KEENABLE_SEARCH_PROFILE). loadWebSearchAuth then read the user-level
value back regardless of which tab sent the request.

Fix: profile lives on TEphemeralAgent (per-conversation, recoil atom
keyed by conversationId) and rides in each chat-send payload. Backend
prefers ephemeralAgent.web_search_profile over the user-level default.

Changes:
- packages/data-provider/src/types.ts: web_search_profile?: string on
  TEphemeralAgent
- packages/data-provider/src/config.ts: LAST_WEB_SEARCH_PROFILE_ in
  LocalStorageKeys for per-conversation persistence across refresh
- client/src/Providers/BadgeRowContext.tsx: hydrate web_search_profile
  into the conversation's ephemeralAgent atom on mount alongside the
  other tool toggles
- client/src/components/SidePanel/Agents/Search/ApiKeyDialog.tsx: write
  selected profile to ephemeralAgent (per-conversation) + localStorage
  on every change; initialize from ephemeralAgent if set
- client/src/hooks/Plugins/useAuthSearchTool.ts: stop sending
  searchProfile in the install plugin-auth payload — it is no longer
  per-user state
- api/app/clients/tools/util/handleTools.js: override
  authResult.searchProfile from req.body.ephemeralAgent.web_search_profile
  before createSearchTool is invoked
Adds a checkbox in the search settings dialog that lets each
conversation pick between full pro mode (scrape top N URLs + rerank
highlights, ~1-3s) and snippet-only fast path (just /v1/search results,
~200-400ms).

Companion change in @librechat/agents (b86372e): when topResults=0 is
passed to createSourceProcessor, processSources short-circuits and
returns the search response without scraping.

- TEphemeralAgent.web_search_pro_mode (per-conversation, default true)
- LocalStorageKeys.LAST_WEB_SEARCH_PRO_MODE_ for refresh persistence
- BadgeRowContext hydrates from localStorage alongside the other tools
- ApiKeyDialog: Pro Mode checkbox shown when provider is keenable;
  writes to ephemeralAgent + localStorage on change
- handleTools.js: when ephemeralAgent.web_search_pro_mode === false,
  pass topResults: 0 to createSearchTool, propagating to the agents
  short-circuit
- api/package.json: bump @librechat/agents pin to b86372e
…void stale state

Bug: ApiKeyDialog held selectedProfile/proMode in useState initialized
from ephemeralAgent on mount. BadgeRowContext hydrates ephemeralAgent
from localStorage in a useEffect — which fires AFTER the dialog has
already snapshotted the initial empty atom into local state. Result:
the dialog's UI shows 'default' while the actual ephemeralAgent (and
the value sent to the backend on chat-send) holds the real persisted
value. The user appears to make no change in the dialog so nothing is
written, and the stale persisted profile keeps getting used.

Fix: derive both values directly from the recoil atom each render. The
atom IS the source of truth; the dialog just reflects it. Mutations go
through setEphemeralAgent + localStorage and the dialog updates next
render automatically.
…uuid transition

When user toggles Debug Mode in a 'new' conversation and then sends the
first message, the conversation gets a real UUID. The dialog wrote
debug_mode under atom['new'] but the rendered assistant message has
conversationId=<uuid>, so the footer's lookup against atom[<uuid>] was
empty and the footer rendered null.

Fix: also consult BadgeRowContext's conversationId-keyed atom and the
'new' atom, taking whichever has debug_mode set.
DebugFooter was only added to MessageParts, which only renders for the
'assistants' endpoint. Custom endpoints (Azure azureOpenAI, Cerebras,
Baseten, Fireworks) route through MessageContent → ContentRender, and
some pure-text messages go through Message → MessageRender. Adding the
footer to all three paths so it shows regardless of endpoint.
… show it

BaseClient was setting responseMessage.tokenCount from the model's output
usage, queueing the DB save, then deleting tokenCount from the in-memory
object before returning. The DB record was correct, but the streamed
final event sent to the client had no tokenCount, so the Debug Mode
footer (and any other consumer) saw 'out: —' until a manual conversation
refetch rehydrated the saved message.

Removing the delete; tokenCount now rides on the streamed response.
Affects all clients that go through BaseClient.sendMessage, including
the agents pipeline.
- Backend: aggregate input/output tokens across every LLM generation in
  an agent run (LangFuse-style sum) by adding `getAggregateUsage` to
  AgentClient and overriding `getStreamUsage` to prefer the cross-step
  aggregate over `firstUsage`-only `this.usage`. BaseClient now stashes
  `responseMessage.promptTokens` from the chosen usage so the streamed
  response carries input-token totals to the client.

- Frontend: capture submittedAt / firstTokenAt / firstVisibleAt /
  finishedAt in a new `messageMetricsByIdAtom` (recoil atomFamily).
  TTFT bumps on the first event of any kind (content delta, step,
  tool call). TTFVT bumps only on visible text content. After
  finalHandler/cancelHandler, metrics are copied from the placeholder
  initialResponse messageId to the server-assigned responseMessage id
  so the renderer can find them.

- DebugFooter shows TTFT, TTFVT, e2e (all derived from local timings),
  plus in: (promptTokens) and out: (tokenCount) from the streamed
  response.
…mptTokens

- Frontend: agent text streams via stepHandler ON_MESSAGE_DELTA, not
  contentHandler. Bump TTFVT when the message_delta carries a TEXT
  contentPart. (Tool calls, thinking, agent_update events still bump
  TTFT only.)
- Backend: assign responseMessage.promptTokens with explicit numeric
  fallback chain (aggregate → single-call estimate → 0) so the field
  is always a finite number. Replace debug log with info-level log
  surfacing aggregateInput / promptTokens / pickedInput so we can see
  why a value is null in production logs.
Mongoose strict mode was silently dropping responseMessage.promptTokens
on save (and any subsequent fetch), so the field never reached the
client even though BaseClient was setting it correctly on the in-memory
response. Add promptTokens to both the Mongoose schema and the TS type
so the field survives persistence.

Also keep the inline info log for one more cycle to confirm the fix
end-to-end with real values.
ilya-bogin-keenable pushed a commit that referenced this pull request Jul 1, 2026
* 🏗️ refactor: Derive App Version from Root package.json + Add buildInfo Schema

The hardcoded `Constants.VERSION` in `data-provider` is now replaced at
rollup build time via `@rollup/plugin-replace`, sourcing from the root
`package.json` so version bumps are a single-file change.

Adds the shape needed by the rest of the series:
- `interface.buildInfo` boolean flag (default `true`) — lets self-hosters
  opt out of exposing commit/branch/date.
- `buildInfo` on `TStartupConfig` — commit/commitShort/branch/buildDate.
- `SettingsTabValues.ABOUT` — new settings tab enum value.

Ref: danny-avila#12406

* 🛠️ feat: Add Build Metadata Resolver and Expose via /api/config

Adds `resolveBuildInfo()` in `@librechat/api` that surfaces commit SHA,
branch, and build date from (in order) `BUILD_*` env vars, then local git
metadata. Result is cached per-process.

`/api/config` includes a `buildInfo` field on both authenticated and
anonymous responses when `interface.buildInfo !== false` and at least one
resolver field is populated. Omitted entirely otherwise.

Designed so pre-built Docker images carry metadata via build-arg while
source installs pick it up from `.git` — no manual version tracking.

Ref: danny-avila#12406

* ℹ️ feat: Add Settings → About Panel with Diagnostics Copy

New Settings tab that renders the running build's version, commit (short
SHA), branch, and build date in a monospaced block alongside a "Copy
diagnostics" button that emits a preformatted text blob for pasting into
support issues.

Tab is hidden when `interface.buildInfo` is set to `false`. Reads from
`startupConfig.buildInfo` provided by `/api/config`.

Ref: danny-avila#12406

* 🐳 ci: Inject BUILD_COMMIT/BRANCH/DATE into Docker Images

Adds optional `BUILD_COMMIT`, `BUILD_BRANCH`, `BUILD_DATE` ARGs to both
`Dockerfile` and `Dockerfile.multi`, wired as `ENV` vars in the runtime
stage so the backend's `resolveBuildInfo` picks them up.

All image-publishing workflows (`tag`, `main`, `dev`, `dev-branch`,
`dev-staging`) now compute `${github.sha}`, `${github.ref_name}`, and a
UTC timestamp, then pass them to `docker/build-push-action` as
`build-args`.

Defaults are empty — non-CI builds (local `docker build`) still work,
and the backend falls back to local `.git` metadata if ARGs aren't set.

Ref: danny-avila#12406

* 📝 docs: Direct Bug Reporters to Settings → About for Version Info

The previous instructions (`docker images | grep librechat`,
`git rev-parse HEAD`) only worked for a subset of deployments and
rarely produced a commit SHA for users pulling pre-built images.

Point users to the new in-app Settings → About panel's
"Copy diagnostics" button, which captures version, commit, branch,
build date, and user agent in a single preformatted block. Fallback
instructions preserved for older installs.

Ref: danny-avila#12406

* 🐳 fix: Move BUILD_* ENV to End of Docker Stages to Preserve Layer Cache

Per-commit BUILD_COMMIT/BUILD_DATE changes were being promoted to ENV
before `npm ci` / `npm run frontend` (single-stage) and before
`npm ci --omit=dev` (multi-stage api-build), which invalidated the cache
for every subsequent layer on every CI run.

Move the ARG/ENV block below the heavy install and build steps in both
Dockerfiles. Metadata is still available in the runtime image but no
longer busts layer reuse.

Addresses codex review on danny-avila#12756.

* 🔧 fix: Propagate interface.buildInfo=false to Unauthenticated /api/config

The unauthenticated branch of `/api/config` was emitting an `interface`
object only when `privacyPolicy` or `termsOfService` was set, which
meant an admin's explicit `interface.buildInfo: false` opt-out was never
visible to anonymous/guest clients. `Settings.tsx` gates the About tab
on `startupConfig?.interface?.buildInfo !== false`, so a missing field
fell through as "enabled" for those clients.

Include `interface.buildInfo: false` in the unauth payload whenever it's
explicitly disabled. Keep the implicit default (true) absent to preserve
the minimal-unauth-payload convention.

Addresses codex review on danny-avila#12756.

* 🔀 ci: Trigger Dev Image Workflows on Root package.json + Dockerfile Changes

The baked `Constants.VERSION` now reads from the root `package.json` via
rollup-plugin-replace, but the `dev-images.yml` and `dev-branch-images.yml`
path filters only matched `api/**`, `client/**`, `packages/**`. A release
commit that only bumps root `package.json` would not trigger a rebuild,
leaving `latest` dev images with stale Footer/About version metadata.

Include `package.json`, `package-lock.json`, and both Dockerfiles in the
path filters so dependency changes (lockfile rebuilds) and image build
tweaks also rebuild dev images.

Addresses codex review on danny-avila#12756.

* 🧽 fix: Harden About Panel Lifecycle, A11y, and Loading Gate

Review follow-ups on danny-avila#12756:

- #1 timer leak: stash the copy-state `setTimeout` in a ref and clear it
  from a `useEffect` cleanup so unmounting the Settings dialog mid-toast
  doesn't fire `setCopied(false)` on an unmounted component.
- danny-avila#3 flash of About tab: gate `aboutEnabled` on `startupConfig != null`
  so the tab stays hidden until `/api/config` returns. For admins who
  disabled `interface.buildInfo`, the tab no longer briefly appears and
  vanishes on page load.
- danny-avila#6 aria-live placement: move the live region off the interactive
  button onto a dedicated `<span role="status" aria-live="polite">` so
  screen readers announce the copied state, not the full button content
  on every re-render.
- #2 missing coverage: add `About.spec.tsx` exercising populated/empty
  buildInfo rendering, invalid-date handling, diagnostics clipboard
  payload, copy-state toggling, unmount cleanup, and the live region.

* ⚡ perf: Eagerly Resolve Build Info at Module Load

Review follow-up danny-avila#4 on danny-avila#12756: `resolveBuildInfo()` calls `execFileSync`
with a 2s timeout on source installs without `BUILD_*` env vars. Paying
this cost on the first HTTP request blocks the event loop mid-flight.

Call `resolveBuildInfo()` once at config route module load so the
resolver's cache is warm before any request arrives. Docker images with
the BUILD_* env vars set sidestep the git path entirely, so this only
affects the edge case of source installs.

* 📝 docs: Document rollup Version Placeholder Contract

Review follow-ups danny-avila#5 / danny-avila#8 on danny-avila#12756. The `__LIBRECHAT_VERSION__`
placeholder relies on a substring replacement rule that only works
because the token appears inside a string literal, and the substitution
only runs during `npm run build:data-provider`.

- Expand the `Constants.VERSION` JSDoc to spell out that consumers read
  the placeholder through the built dist bundle; source-level test
  imports would see the raw placeholder.
- Add a NOTE above the rollup `replace` config warning future
  contributors not to repurpose the token as a bare identifier without
  switching to a quoted replacement value.

Non-functional; prevents future contributors from stepping on a subtle
constraint.

* 🪪 fix: Only Toast "Copied" When Clipboard Copy Actually Succeeds

Codex R5 on danny-avila#12756. `copy-to-clipboard` returns a boolean indicating
whether the underlying `execCommand('copy')` / fallback prompt actually
wrote to the clipboard. The previous handler flipped to the "Copied"
state unconditionally, which in hardened browsers or when the
permission prompt is dismissed would mislead users into filing bug
reports without the diagnostics blob attached.

Gate the state/timer/live-region on the boolean return; silently no-op
on failure rather than showing a false positive. Adds a test asserting
the button label stays at "Copy diagnostics" when the clipboard call
fails.

* 🐳 fix: Derive main image metadata from checkout

* 🪪 fix: Keep About enabled until disabled

* ✅ test: Avoid literal Settings mock text

* 🧱 refactor: Rename Build Info Module
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