🏗️ 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:
Danny Avila 2026-07-22 11:41:18 -04:00
parent d5583dfd7f
commit b9f7033c9b
5 changed files with 205 additions and 93 deletions

View file

@ -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);
}
/**

View file

@ -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

View 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);
}
}

View file

@ -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';

View file

@ -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';