Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/codegraph-select.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ permissions:
contents: read
pull-requests: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
select:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- **Agent execution host**: the protocol-neutral module that owns run admission, disconnect cancellation, provider-start fencing, and terminal settlement. Protocol implementations execute behind its callback interface; HTTP adapters retain validation and final stream rendering.
- **Agent execution enrollment**: the durable, protocol-neutral lifecycle authority for an admitted Agent run. It is created under the authenticated user and tenant before user-owned initialization, rechecks the shared owner-deletion admission fence after registration, exposes the only provider abort signal, fences exact provider start, terminalizes the run, waits for every trailing usage, artifact, and stored-response write, and acknowledges provider drain last. A transient terminalization failure is reconciled after trailing writes; provider drain is never acknowledged while the exact job remains nonterminal. Delete-all holds the owner fence, drains every owner run before selecting its first persistence snapshot, and repeats both the drain and an idempotent owner-persistence sweep after any recovered fence lapse before releasing admission. Exact-conversation deletion additionally performs an unconditional idempotent cleanup over its immutable deleted-ID set because a fully drained run may leave the active index after racing the first delete; only the explicit empty result is benign, while storage failures remain fatal. Chat Completions, Responses, Channels, and future ingress adapters share this authority without moving LibreChat persistence policy into the Agents SDK.
- **Agent turn execution plan**: the immutable, request-local decision compiled once after authentication, agent resolution, and tool initialization. It records the trusted turn origin, conversation lineage, pause capability, binding/action context, and the preferred checkpoint, history, or fresh state-loading strategy without executing the model or owning persistence. Checkpoint failure falls back to durable history within the same Agents lifecycle.
- **Turn delivery routing**: the per-agent, request-local value that decides how each attachment reaches the model on one turn (`provider`, `text`, or `none`). Initialization settles it once, after the provider swap and the Responses API decision, under the endpoint's own name and the media dialect its config declares. Every reader of a turn route consumes that one value rather than deriving it from the agent. A stored route is an upload-time inference that this value resolves again for the turn; a destination the user chose stands.
- **Effective agent selection**: the resolved endpoint and agent identity after an enforced model spec is applied. Authorization and agent loading must consume this same identity before the Agent run envelope is initialized.
- **MCP runtime request body**: trusted chat identifiers supplied only while an MCP server handles an agent request. It enables request-scoped header placeholders without retaining user-specific request data on a shared server definition.
- **MCP direct OpenID bearer**: an operator-trusted remote MCP credential mode that resolves the logged-in user's live OpenID access token into an Authorization header. It may replace one rejected connection after a forced session refresh, but it never replays the rejected tool invocation automatically.
Expand Down
75 changes: 27 additions & 48 deletions api/app/clients/BaseClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
sanitizeFileForTransmit,
extractFileContext,
getReferencedQuotes,
applyTurnDelivery,
encodeAndFormatAudios,
encodeAndFormatVideos,
getTransactionsConfig,
Expand All @@ -33,17 +34,14 @@ const {
isCompactedLeaf,
excludedKeys,
EModelEndpoint,
mergeFileConfig,
isParamEndpoint,
isAgentsEndpoint,
isEphemeralAgentId,
supportsBalanceCheck,
isBedrockDocumentType,
HITL_MESSAGE_FILTER_FIELDS,
getEndpointFileConfig,
stripReasoningLabelMetadata,
resolveUploadLLMDeliveryPath,
isSpeechProviderConfigured,
resolveTurnLLMDeliveryPath,
resolveUseResponsesApi,
} = require('librechat-data-provider');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
Expand Down Expand Up @@ -241,10 +239,6 @@ class BaseClient {
this.currentMessages = [];
/** @type {import('librechat-data-provider').VisionModes | undefined} */
this.visionMode;
/** @type {import('librechat-data-provider').FileConfig | undefined} */
this._mergedFileConfig;
/** @type {import('librechat-data-provider').EndpointFileConfig | undefined} */
this._endpointFileConfig;
}

setOptions() {
Expand Down Expand Up @@ -831,9 +825,8 @@ class BaseClient {
if (this.options.resendFiles !== false && this.authorizedHistoricalFiles == null) {
const historicalFileState = collectModelBoundHistoricalFileIdState(modelBoundStoredMessages);
this.modelBoundHistoricalFileIdsOverflowed ||= historicalFileState.overflowed;
const files = await getOwnerHistoricalFiles(
historicalFileState.fileIds,
this.options.req?.user,
const files = this.resolveTurnAttachments(
await getOwnerHistoricalFiles(historicalFileState.fileIds, this.options.req?.user),
);
this.authorizedHistoricalFiles = new Map(
files
Expand Down Expand Up @@ -1780,8 +1773,8 @@ class BaseClient {
* @param {MongoFile[]} attachments - Array of file attachments
* @returns {Promise<void>}
*/
async addFileContextToMessage(message, attachments) {
const textAttachments = this.getTextContextAttachments(attachments);
async addFileContextToMessage(message, attachments, fileConsumers) {
const textAttachments = this.getTextContextAttachments(attachments, fileConsumers);
const fileContext = await extractFileContext({
attachments: textAttachments,
req: this.options?.req,
Expand All @@ -1793,45 +1786,29 @@ class BaseClient {
}
}

getTextContextAttachments(attachments) {
getTextContextAttachments(attachments, fileConsumers) {
return attachments.filter((file) => {
const deliveryPath = this.getAttachmentDeliveryPath(file);
const deliveryPath = this.getAttachmentDeliveryPath(file, fileConsumers);
/* Records predating delivery paths keep legacy extraction. Current routing is
* authoritative for inferred uploads, so native provider bytes are not also
* injected as extracted text after a provider handoff. */
return deliveryPath == null || deliveryPath === 'text';
});
}

/** Re-resolves an inferred upload route against the provider handling this turn. */
getAttachmentDeliveryPath(file) {
if (!this._mergedFileConfig) {
this._mergedFileConfig = mergeFileConfig(this.options.req?.config?.fileConfig);
/* Agent file policy is configured under the endpoint it names, not the client
* family initialization may rewrite it to. */
const agentEndpoint = this.options.agent?.endpoint ?? this.options.agent?.provider;
this._deliveryEndpoint = agentEndpoint ?? this.options.endpoint;
this._endpointFileConfig = getEndpointFileConfig({
fileConfig: this._mergedFileConfig,
endpoint: this._deliveryEndpoint,
endpointType: agentEndpoint != null ? undefined : this.options.endpointType,
});
}
/** The turn's view of stored records, applied before admission at every load. */
resolveTurnAttachments(files, fileConsumers = this.options.agent?.fileConsumers) {
return applyTurnDelivery(files, {
routing: this.options.agent?.deliveryRouting,
consumers: fileConsumers,
});
}

return file.llmDeliveryPath == null || file.metadata?.destinationChosen === true
? file.llmDeliveryPath
: resolveUploadLLMDeliveryPath({
/* Conversion changes the stored type, so use the type routing originally saw. */
mimeType: file.metadata?.routingMimeType ?? file.type,
endpointConfig: this._endpointFileConfig,
fileConfig: this._mergedFileConfig,
endpoint: this._deliveryEndpoint,
useResponsesApi: this.usesResponsesApi(),
sttConfigured: isSpeechProviderConfigured(this.options.req?.config?.speech?.stt),
});
getAttachmentDeliveryPath(file, fileConsumers = this.options.agent?.fileConsumers) {
return resolveTurnLLMDeliveryPath(this.options.agent?.deliveryRouting, file, fileConsumers);
}

async processAttachments(message, attachments) {
async processAttachments(message, attachments, fileConsumers) {
const categorizedAttachments = {
images: [],
videos: [],
Expand All @@ -1842,6 +1819,7 @@ class BaseClient {
const allFiles = [];
const provider = this.options.agent?.provider ?? this.options.endpoint;
const isBedrock = provider === EModelEndpoint.bedrock;
const deliveryRouting = this.options.agent?.deliveryRouting;

/* The stored path records what upload time inferred from the endpoint it saw, and this
* turn may be running somewhere else: audio stored as `provider` under Google reaches
Expand All @@ -1855,7 +1833,7 @@ class BaseClient {
allFiles.push(file);
continue;
}
const deliveryPath = this.getAttachmentDeliveryPath(file);
const deliveryPath = this.getAttachmentDeliveryPath(file, fileConsumers);
if (deliveryPath === 'text' || deliveryPath === 'none') {
allFiles.push(file);
continue;
Expand Down Expand Up @@ -1890,9 +1868,11 @@ class BaseClient {
allFiles.push(file);
} else if (
file.type &&
this._mergedFileConfig &&
this._endpointFileConfig?.supportedMimeTypes &&
this._mergedFileConfig.checkType(file.type, this._endpointFileConfig.supportedMimeTypes)
deliveryRouting?.endpointConfig.supportedMimeTypes &&
deliveryRouting.fileConfig.checkType(
file.type,
deliveryRouting.endpointConfig.supportedMimeTypes,
)
) {
categorizedAttachments.documents.push(file);
allFiles.push(file);
Expand Down Expand Up @@ -1954,9 +1934,8 @@ class BaseClient {
const historicalFileState = collectModelBoundHistoricalFileIdState(_messages);
this.modelBoundHistoricalFileIdsOverflowed ||= historicalFileState.overflowed;
const authorizedFilesById = new Map();
const files = await getOwnerHistoricalFiles(
historicalFileState.fileIds,
this.options.req?.user,
const files = this.resolveTurnAttachments(
await getOwnerHistoricalFiles(historicalFileState.fileIds, this.options.req?.user),
);
const nonSteerReplayFileIds = collectModelBoundHistoricalFileIdState(
_messages.map((message) => ({
Expand Down
Loading
Loading