mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-30 06:47:42 +00:00
🎯 refactor: Infer Agents Endpoint for Model Specs Naming an Agent (#14889)
* 🎯 fix: Infer Agents Endpoint for Model Specs Naming an Agent A model spec whose preset names an `agent_id` but omits `endpoint` was unusable. `isModelSpecEndpointMatch` compares the request's endpoint to `preset.endpoint` by strict equality, so an undefined endpoint matched nothing and every request selecting the spec was rejected with a bare `Model spec mismatch` — an error naming neither the spec nor the missing field. The selector had the matching half of the same gap: `handleSelectSpec` read `preset.endpoint` directly, so it sent no endpoint and skipped assigning `agent_id` to `model`. Fixing only the server would leave the request malformed, so the resolution is shared between both. - Add `resolveModelSpecEndpoint` to `librechat-data-provider`, inferring the agents endpoint when a preset names an agent and none is set. An explicit `endpoint` always wins, so configured specs are unaffected. - Use it for endpoint matching and in the selector, so the menu and the request pipeline resolve a spec identically. * 🔁 refactor: Materialize Inferred Spec Endpoints at Config Load The review showed the lazy-resolver approach was unsound end to end: config validation rejected an endpoint-less spec before the resolver could ever run (`tPresetSchema` requires the `endpoint` key), and the resolver was applied at 2 of ~8 read sites, leaving selection handlers, startup presets, access filters, and provider-key reachability reading the raw preset. Materialize once at the boundary instead: - `tModelSpecPresetSchema` now makes `endpoint` optional (`nullish`). This is barely a widening — `endpoint: null` already validated — and only for model-spec presets; `tPresetSchema` is untouched. - `materializeModelSpecEndpoints` writes each spec's resolved endpoint back onto its preset. `createAppConfigService` applies it at both effective-config assembly points — YAML base load and DB-override merge — so admin-panel specs stored in override documents are covered. Identity-preserving, so cached configs see no new references when nothing needs filling in. - Every consumer now reads complete specs; the client's lazy resolve in `handleSelectSpec` is reverted to a raw read. `getModelSpecPreset` and the two hand-rolled preset constructions resolve the endpoint explicitly, which the narrowed preset type now enforces at compile time for any `TPreset`-shaped destination. - `isModelSpecEndpointMatch` keeps the resolver as request-time defense. * 🩹 fix: Materialize Spec Endpoints Before the YAML Missing-Endpoint Guard `processModelSpecs` warns and skips any spec whose preset lacks an endpoint, and it runs inside `loadBaseConfig` — so the previous commit's materialization received a YAML list from which the inferable spec had already been dropped. Only DB-override specs (merged after the guard) actually benefited. - Materialize at the entry of `processModelSpecs`, so inference happens before the guard and YAML agent specs survive it. The guard keeps skipping genuinely endpoint-less specs. The `createAppConfigService` calls stay: the base-path one guards alternate `loadBaseConfig` implementations, the merged-path one covers override documents, and both are identity-preserving no-ops when specs are already complete. - Constrain the widened schema: omitting `endpoint` is only legal when the preset names an `agent_id`. A preset with neither validated as a hard error before the key became optional, and silently accepting it would trade that startup-time error for a dead spec. An explicit `endpoint: null` (valid before this PR) keeps validating. * 🩹 fix: Infer Only From Non-Empty Agent IDs, Never Over Explicit Null Two edge cases in the inference contract: - `agent_id: ''` (what a form-backed writer persists for an untouched field) passed the nullish checks, validating and materializing a spec that names no agent. Both the refinement and the resolver now require a non-empty id, so such config fails validation loudly instead of producing a selectable spec that cannot work. - `endpoint: null` alongside an `agent_id` was treated as inferable, silently activating a spec that validated — and was skipped — before this PR. An explicit null is a statement, not an omission: the resolver now infers only when the key is absent, preserving prior behavior for previously valid configs.
This commit is contained in:
parent
ed081964d6
commit
bce93f9c55
13 changed files with 437 additions and 29 deletions
|
|
@ -204,13 +204,15 @@ export function ModelSelectorProvider({ children, startupConfig }: ModelSelector
|
|||
(spec: t.TModelSpec) => {
|
||||
let model = spec.preset.model ?? null;
|
||||
onSelectSpec?.(spec);
|
||||
if (isAgentsEndpoint(spec.preset.endpoint)) {
|
||||
/** Specs arrive with `preset.endpoint` materialized at config load. */
|
||||
const endpoint = spec.preset.endpoint ?? null;
|
||||
if (isAgentsEndpoint(endpoint)) {
|
||||
model = spec.preset.agent_id ?? '';
|
||||
} else if (isAssistantsEndpoint(spec.preset.endpoint)) {
|
||||
} else if (isAssistantsEndpoint(endpoint)) {
|
||||
model = spec.preset.assistant_id ?? '';
|
||||
}
|
||||
setSelectedValues({
|
||||
endpoint: spec.preset.endpoint,
|
||||
endpoint,
|
||||
model,
|
||||
modelSpec: spec.name,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
LocalStorageKeys,
|
||||
PermissionTypes,
|
||||
Permissions,
|
||||
resolveModelSpecEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TStartupConfig, TUser } from 'librechat-data-provider';
|
||||
import { useMCPToolsQuery, useMCPServersQuery } from '~/data-provider';
|
||||
|
|
@ -77,6 +78,7 @@ export default function useAppStartup({
|
|||
|
||||
setDefaultPreset({
|
||||
...defaultSpec.preset,
|
||||
endpoint: resolveModelSpecEndpoint(defaultSpec) ?? null,
|
||||
iconURL: defaultSpec.iconURL,
|
||||
spec: defaultSpec.name,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
isAgentsEndpoint,
|
||||
getConfigDefaults,
|
||||
isAssistantsEndpoint,
|
||||
resolveModelSpecEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { MentionOption } from '~/common';
|
||||
|
|
@ -202,6 +203,7 @@ export default function useMentions({
|
|||
icon: EndpointIcon({
|
||||
conversation: {
|
||||
...modelSpec.preset,
|
||||
endpoint: resolveModelSpecEndpoint(modelSpec) ?? null,
|
||||
iconURL: modelSpec.iconURL,
|
||||
},
|
||||
endpointsConfig,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
isAgentsEndpoint,
|
||||
isEphemeralAgentId,
|
||||
isAssistantsEndpoint,
|
||||
resolveModelSpecEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import type { LocalizeFunction, IconsRecord } from '~/common';
|
||||
|
|
@ -514,6 +515,12 @@ export function getModelSpecPreset(modelSpec?: t.TModelSpec) {
|
|||
}
|
||||
return {
|
||||
...modelSpec.preset,
|
||||
/**
|
||||
* Specs are materialized at config load, but a preset flowing into
|
||||
* `TPreset` contexts must carry an endpoint decision either way — resolve
|
||||
* here so startup and URL flows never receive an endpoint-less preset.
|
||||
*/
|
||||
endpoint: resolveModelSpecEndpoint(modelSpec) ?? null,
|
||||
spec: modelSpec.name,
|
||||
iconURL: getModelSpecIconURL(modelSpec),
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue