mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🏗️ refactor: Codex Round 5 — Extract Label Host Logic, Report Usage, Icon Strip
- Move provider/model resolution, usage-metadata mapping, and the settle loop into packages/api (activityLabels/host.ts); client.js keeps only thin delegations, per the repo's TypeScript-implementation convention. - Fold label usage into the response rollup with an 'activity-label' tag (subagent precedent) so metadata.usage and the live cost gauge account for it; tagged, so it stays out of PRIMARY usage/context pairing. - Narrow tool metadata once in ToolCallGroup so THINK parts in a labeled block no longer render phantom generic icons in the stacked strip. - Import the activity-label helpers by deep path in GenerationJobManager: the package barrel now reaches provider-config/cache modules that import back into the stream layer, and the cycle broke suite loading. Declined: resetting steerOffsetState before HITL resume — resume builds a FRESH AgentClient via initializeClient (initialize.js:978), so the offset is already zero; the seed wrapper alone accounts for pre-pause parts.
This commit is contained in:
parent
d5583dfd7f
commit
b9f7033c9b
5 changed files with 205 additions and 93 deletions
|
|
@ -52,6 +52,9 @@ const {
|
|||
stampSteerPartMedia,
|
||||
createActivityLabelWiring,
|
||||
isActivityLabelPocEnabled,
|
||||
mapCollectedMetadataToUsage,
|
||||
resolveActivityLabelModel,
|
||||
settlePendingLabelFills,
|
||||
stripActivityLabelParts,
|
||||
getRequestMemories,
|
||||
getMemoryAgentId,
|
||||
|
|
@ -347,90 +350,40 @@ class AgentClient extends BaseClient {
|
|||
* provider as the agent).
|
||||
*/
|
||||
async resolveActivityLabelLLM() {
|
||||
const { req, agent } = this.options;
|
||||
const appConfig = req.config;
|
||||
const endpoint = agent.endpoint;
|
||||
const providerConfig = getProviderConfig({ provider: endpoint, appConfig });
|
||||
/** @type {TEndpoint | undefined} */
|
||||
const endpointConfig =
|
||||
appConfig.endpoints?.all ??
|
||||
appConfig.endpoints?.[endpoint] ??
|
||||
providerConfig.customEndpointConfig;
|
||||
const model =
|
||||
process.env.ACTIVITY_LABEL_MODEL ||
|
||||
(endpointConfig?.titleModel && endpointConfig.titleModel !== Constants.CURRENT_MODEL
|
||||
? endpointConfig.titleModel
|
||||
: agent.model || agent.model_parameters.model);
|
||||
const options = await providerConfig.getOptions({
|
||||
req,
|
||||
endpoint,
|
||||
model_parameters: { model },
|
||||
db: {
|
||||
getUserKey: db.getUserKey,
|
||||
getUserKeyValues: db.getUserKeyValues,
|
||||
},
|
||||
});
|
||||
let provider = options.provider ?? providerConfig.overrideProvider ?? agent.provider;
|
||||
if (
|
||||
endpoint === EModelEndpoint.azureOpenAI &&
|
||||
options.llmConfig?.azureOpenAIApiInstanceName == null
|
||||
) {
|
||||
provider = Providers.OPENAI;
|
||||
} else if (
|
||||
endpoint === EModelEndpoint.azureOpenAI &&
|
||||
options.llmConfig?.azureOpenAIApiInstanceName != null &&
|
||||
provider !== Providers.AZURE
|
||||
) {
|
||||
provider = Providers.AZURE;
|
||||
}
|
||||
const clientOptions = { ...options.llmConfig };
|
||||
if (options.configOptions) {
|
||||
clientOptions.configuration = options.configOptions;
|
||||
}
|
||||
/** Resolve request-based header placeholders across provider-specific
|
||||
* header locations, mirroring titleConvo — proxies that key on
|
||||
* conversation/user metadata need them on label calls too. */
|
||||
resolveConfigHeaders({
|
||||
llmConfig: clientOptions,
|
||||
user: createSafeUser(this.options.req?.user),
|
||||
body: {
|
||||
return resolveActivityLabelModel({
|
||||
req: this.options.req,
|
||||
agent: this.options.agent,
|
||||
ids: {
|
||||
messageId: this.responseMessageId,
|
||||
conversationId: this.conversationId,
|
||||
parentMessageId: this.parentMessageId,
|
||||
},
|
||||
db: { getUserKey: db.getUserKey, getUserKeyValues: db.getUserKeyValues },
|
||||
});
|
||||
return { provider, clientOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the current activity block's context for the label payload:
|
||||
* reasoning excerpts since the last text part, plus the assistant's last
|
||||
* text (~200 chars) as intent. Deliberately NO human messages — see the
|
||||
* activity-label design notes. Called synchronously at claim time so the
|
||||
* snapshot can't include parts from the next block.
|
||||
* Bills the label call and folds its usage into the response rollup with
|
||||
* an `activity-label` tag (subagent precedent) so `metadata.usage` and the
|
||||
* live cost gauge reflect it. Tagged, so it is not a PRIMARY usage event
|
||||
* and cannot disturb the context-snapshot pairing in buildResponseMetadata.
|
||||
*/
|
||||
/** Maps aggregated LLM metadata into recorded usage, mirroring titleConvo. */
|
||||
async recordActivityLabelUsage(collectedMetadata, model) {
|
||||
const appConfig = this.options.req?.config;
|
||||
const collectedUsage = collectedMetadata.map((item) => {
|
||||
let input_tokens, output_tokens;
|
||||
if (item.usage) {
|
||||
input_tokens =
|
||||
item.usage.prompt_tokens || item.usage.input_tokens || item.usage.inputTokens;
|
||||
output_tokens =
|
||||
item.usage.completion_tokens || item.usage.output_tokens || item.usage.outputTokens;
|
||||
} else if (item.tokenUsage) {
|
||||
input_tokens = item.tokenUsage.promptTokens;
|
||||
output_tokens = item.tokenUsage.completionTokens;
|
||||
} else if (item.usage_metadata) {
|
||||
input_tokens = item.usage_metadata.input_tokens;
|
||||
output_tokens = item.usage_metadata.output_tokens;
|
||||
}
|
||||
return { input_tokens, output_tokens };
|
||||
});
|
||||
const collectedUsage = mapCollectedMetadataToUsage(collectedMetadata);
|
||||
if (collectedUsage.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const usage of collectedUsage) {
|
||||
this.usageEmitSink?.push({
|
||||
input_tokens: usage.input_tokens,
|
||||
output_tokens: usage.output_tokens,
|
||||
model,
|
||||
usage_type: 'activity-label',
|
||||
runId: this.responseMessageId,
|
||||
seq: this.collectedUsage.length,
|
||||
});
|
||||
}
|
||||
await this.recordCollectedUsage({
|
||||
collectedUsage,
|
||||
context: 'activity-label',
|
||||
|
|
@ -488,24 +441,14 @@ class AgentClient extends BaseClient {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounded wait for in-flight label fills so a label resolving during the
|
||||
* final batch still reaches the durable log and the saved message before
|
||||
* the job completes. Never delays finalization past the bound; fills that
|
||||
* lose the race leave the counts-only placeholder, which renders fine.
|
||||
*/
|
||||
/** Bounded settle for in-flight label fills before finalization. */
|
||||
async settleActivityLabels(timeoutMs = 3000) {
|
||||
const pending = this.pendingActivityLabelFills;
|
||||
if (!pending || pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.pendingActivityLabelFills = [];
|
||||
let timerId;
|
||||
const timeout = new Promise((resolve) => {
|
||||
timerId = setTimeout(resolve, timeoutMs);
|
||||
});
|
||||
await Promise.race([Promise.allSettled(pending), timeout]);
|
||||
clearTimeout(timerId);
|
||||
await settlePendingLabelFills(pending, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -112,19 +112,24 @@ export default function ToolCallGroup({
|
|||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
const cancelLayoutReconcileRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const toolMetadata = useMemo(() => parts.map((p) => getToolMeta(p.part)), [parts]);
|
||||
/** Labeled activity blocks also contain THINK parts (meta null) — those
|
||||
* never have output; only tool entries participate in completion. */
|
||||
const count = useMemo(() => toolMetadata.filter(Boolean).length, [toolMetadata]);
|
||||
/** Labeled activity blocks also contain THINK parts, which yield null
|
||||
* metadata. Narrow to tool entries once: they alone drive the count, the
|
||||
* completion check, and the icon strip — passing a null-derived empty
|
||||
* name to StackedToolIcons would render a phantom generic tool icon. */
|
||||
const toolMetadata = useMemo(
|
||||
() => parts.map((p) => getToolMeta(p.part)).filter((m): m is ToolMeta => m != null),
|
||||
[parts],
|
||||
);
|
||||
const count = toolMetadata.length;
|
||||
const allCompleted = useMemo(
|
||||
() => toolMetadata.every((m) => m == null || m.hasOutput === true),
|
||||
() => toolMetadata.every((m) => m.hasOutput === true),
|
||||
[toolMetadata],
|
||||
);
|
||||
const activityLabel = getActivityLabelPart(labelPart?.part);
|
||||
const activityLabelText = getActivityLabelText(activityLabel, localize);
|
||||
const activityFailed = activityLabel?.status === 'failed' || activityLabel?.status === 'partial';
|
||||
const toolNames = useMemo(() => toolMetadata.map((m) => m?.name ?? ''), [toolMetadata]);
|
||||
const iconToolNames = useMemo(() => toolMetadata.map((m) => m?.iconName ?? ''), [toolMetadata]);
|
||||
const toolNames = useMemo(() => toolMetadata.map((m) => m.name), [toolMetadata]);
|
||||
const iconToolNames = useMemo(() => toolMetadata.map((m) => m.iconName), [toolMetadata]);
|
||||
|
||||
/** Subagent tool calls get their own label verb ("Running/Ran N agents")
|
||||
* since "Used N tools" reads oddly when the "tools" are actually child
|
||||
|
|
|
|||
153
packages/api/src/agents/activityLabels/host.ts
Normal file
153
packages/api/src/agents/activityLabels/host.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { Providers } from '@librechat/agents';
|
||||
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { AppConfig, TEndpoint } from 'librechat-data-provider';
|
||||
import type { ClientOptions } from '@librechat/agents';
|
||||
import type { ActivityLabelLLM } from './runtime';
|
||||
import { getProviderConfig } from '~/endpoints/config/providers';
|
||||
import { resolveConfigHeaders } from '~/utils/headers';
|
||||
import { createSafeUser } from '~/utils/env';
|
||||
|
||||
/** Aggregated LLM metadata entries (shape varies by provider SDK). */
|
||||
export interface CollectedMetadataEntry {
|
||||
usage?: {
|
||||
prompt_tokens?: number;
|
||||
input_tokens?: number;
|
||||
inputTokens?: number;
|
||||
completion_tokens?: number;
|
||||
output_tokens?: number;
|
||||
outputTokens?: number;
|
||||
};
|
||||
tokenUsage?: { promptTokens?: number; completionTokens?: number };
|
||||
usage_metadata?: { input_tokens?: number; output_tokens?: number };
|
||||
}
|
||||
|
||||
export interface ActivityLabelUsage {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes provider-specific aggregated metadata into the usage shape
|
||||
* `recordCollectedUsage` expects. Mirrors the title path's inline mapping.
|
||||
*/
|
||||
export function mapCollectedMetadataToUsage(
|
||||
collected: CollectedMetadataEntry[],
|
||||
): ActivityLabelUsage[] {
|
||||
return collected.map((item) => {
|
||||
let input_tokens: number | undefined;
|
||||
let output_tokens: number | undefined;
|
||||
if (item.usage) {
|
||||
input_tokens = item.usage.prompt_tokens ?? item.usage.input_tokens ?? item.usage.inputTokens;
|
||||
output_tokens =
|
||||
item.usage.completion_tokens ?? item.usage.output_tokens ?? item.usage.outputTokens;
|
||||
} else if (item.tokenUsage) {
|
||||
input_tokens = item.tokenUsage.promptTokens;
|
||||
output_tokens = item.tokenUsage.completionTokens;
|
||||
} else if (item.usage_metadata) {
|
||||
input_tokens = item.usage_metadata.input_tokens;
|
||||
output_tokens = item.usage_metadata.output_tokens;
|
||||
}
|
||||
return { input_tokens, output_tokens };
|
||||
});
|
||||
}
|
||||
|
||||
export interface ResolveActivityLabelModelParams {
|
||||
req: {
|
||||
config?: AppConfig;
|
||||
user?: Record<string, unknown>;
|
||||
};
|
||||
agent: {
|
||||
endpoint?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
model_parameters?: { model?: string };
|
||||
};
|
||||
/** Request-scoped ids for header placeholder resolution. */
|
||||
ids: { messageId?: string; conversationId?: string; parentMessageId?: string };
|
||||
db: { getUserKey: unknown; getUserKeyValues: unknown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves provider + client options for the label model, mirroring
|
||||
* `titleConvo`'s resolution minus the title-specific branches. Precedence:
|
||||
* `ACTIVITY_LABEL_MODEL` env > the endpoint's `titleModel` > the agent's own
|
||||
* model, always on the agent's endpoint credentials.
|
||||
*/
|
||||
export async function resolveActivityLabelModel({
|
||||
req,
|
||||
agent,
|
||||
ids,
|
||||
db,
|
||||
}: ResolveActivityLabelModelParams): Promise<ActivityLabelLLM> {
|
||||
const appConfig = req.config as AppConfig;
|
||||
const endpoint = agent.endpoint as string;
|
||||
const providerConfig = getProviderConfig({ provider: endpoint, appConfig });
|
||||
const endpointConfig: TEndpoint | undefined =
|
||||
appConfig?.endpoints?.all ??
|
||||
appConfig?.endpoints?.[endpoint as keyof typeof appConfig.endpoints] ??
|
||||
providerConfig.customEndpointConfig;
|
||||
const model =
|
||||
process.env.ACTIVITY_LABEL_MODEL ||
|
||||
(endpointConfig?.titleModel != null && endpointConfig.titleModel !== Constants.CURRENT_MODEL
|
||||
? endpointConfig.titleModel
|
||||
: (agent.model ?? agent.model_parameters?.model));
|
||||
const options = await providerConfig.getOptions({
|
||||
req,
|
||||
endpoint,
|
||||
model_parameters: { model },
|
||||
db,
|
||||
});
|
||||
let provider = (options.provider ??
|
||||
providerConfig.overrideProvider ??
|
||||
agent.provider) as Providers;
|
||||
if (
|
||||
endpoint === EModelEndpoint.azureOpenAI &&
|
||||
options.llmConfig?.azureOpenAIApiInstanceName == null
|
||||
) {
|
||||
provider = Providers.OPENAI;
|
||||
} else if (
|
||||
endpoint === EModelEndpoint.azureOpenAI &&
|
||||
options.llmConfig?.azureOpenAIApiInstanceName != null &&
|
||||
provider !== Providers.AZURE
|
||||
) {
|
||||
provider = Providers.AZURE;
|
||||
}
|
||||
const clientOptions = { ...options.llmConfig } as ClientOptions & {
|
||||
configuration?: unknown;
|
||||
};
|
||||
if (options.configOptions) {
|
||||
clientOptions.configuration = options.configOptions;
|
||||
}
|
||||
/** Resolve request-based header placeholders across provider-specific
|
||||
* header locations, mirroring titleConvo — proxies that key on
|
||||
* conversation/user metadata need them on label calls too. */
|
||||
resolveConfigHeaders({
|
||||
llmConfig: clientOptions,
|
||||
user: createSafeUser(req.user),
|
||||
body: ids,
|
||||
});
|
||||
return { provider, clientOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounded wait for in-flight label fills so a label resolving during the
|
||||
* final batch still reaches the durable log and the saved message before the
|
||||
* job completes. Never delays finalization past the bound; fills that lose
|
||||
* the race leave the counts-only placeholder, which renders fine.
|
||||
*/
|
||||
export async function settlePendingLabelFills(
|
||||
pending: Array<Promise<void>>,
|
||||
timeoutMs = 3000,
|
||||
): Promise<void> {
|
||||
if (pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
let timerId: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<void>((resolve) => {
|
||||
timerId = setTimeout(resolve, timeoutMs);
|
||||
});
|
||||
await Promise.race([Promise.allSettled(pending), timeout]);
|
||||
if (timerId != null) {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,3 +16,13 @@ export {
|
|||
synthesizeActivityLabelGapEvents,
|
||||
} from './wiring';
|
||||
export type { ActivityLabelHostDeps, LooseContentPart } from './wiring';
|
||||
export {
|
||||
mapCollectedMetadataToUsage,
|
||||
resolveActivityLabelModel,
|
||||
settlePendingLabelFills,
|
||||
} from './host';
|
||||
export type {
|
||||
ActivityLabelUsage,
|
||||
CollectedMetadataEntry,
|
||||
ResolveActivityLabelModelParams,
|
||||
} from './host';
|
||||
|
|
|
|||
|
|
@ -37,10 +37,11 @@ import {
|
|||
toPendingSteer,
|
||||
synthesizeAppliedSteerEvents,
|
||||
} from './SteeringLifecycle';
|
||||
import {
|
||||
isActivityLabelPocEnabled,
|
||||
synthesizeActivityLabelGapEvents,
|
||||
} from '~/agents/activityLabels';
|
||||
/** Deep imports (not the package barrel): the barrel pulls provider-config
|
||||
* and cache modules that import back into the stream layer, and the cycle
|
||||
* breaks module initialization at load time. */
|
||||
import { isActivityLabelPocEnabled } from '~/agents/activityLabels/runtime';
|
||||
import { synthesizeActivityLabelGapEvents } from '~/agents/activityLabels/wiring';
|
||||
import { isPendingActionStale, isPendingActionExpired } from './interfaces/IJobStore';
|
||||
import { InMemoryEventTransport } from './implementations/InMemoryEventTransport';
|
||||
import { InMemoryJobStore } from './implementations/InMemoryJobStore';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue