Files
wehub-resource-sync e115934061
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:08:12 +08:00

162 lines
5.1 KiB
JavaScript

/**
* Live host-layer verification: real Anthropic run through the actual
* getDefaultHandlers pipeline, asserting the SSE usage/context events the
* client consumes and their resume persistence.
*
* Run with:
* RUN_USAGE_LIVE_TESTS=1 ANTHROPIC_API_KEY=... npx jest usageEvents.live --runInBand
*/
const { HumanMessage } = require('@langchain/core/messages');
const { Run, Providers, GraphEvents } = require('@librechat/agents');
const { GenerationJobManager } = require('@librechat/api');
const { getDefaultHandlers } = require('~/server/controllers/agents/callbacks');
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mock-nanoid'),
}));
jest.mock('~/server/services/Files/Citations', () => ({
processFileCitations: jest.fn(),
}));
jest.mock('~/server/services/Files/Code/process', () => ({
processCodeOutput: jest.fn(),
runPreviewFinalize: jest.fn(),
}));
jest.mock('~/server/services/Files/process', () => ({
saveBase64Image: jest.fn(),
}));
const shouldRunLive =
process.env.RUN_USAGE_LIVE_TESTS === '1' &&
process.env.ANTHROPIC_API_KEY != null &&
process.env.ANTHROPIC_API_KEY !== '';
const describeIfLive = shouldRunLive ? describe : describe.skip;
const modelName = process.env.ANTHROPIC_USAGE_LIVE_MODEL ?? 'claude-haiku-4-5';
const hasContextUsageEvent = GraphEvents.ON_CONTEXT_USAGE != null;
const charCounter = (msg) => {
const content = msg.content;
if (typeof content === 'string') {
return Math.ceil(content.length / 4) + 3;
}
if (Array.isArray(content)) {
let length = 3;
for (const part of content) {
if (typeof part === 'string') {
length += Math.ceil(part.length / 4);
} else if (typeof part?.text === 'string') {
length += Math.ceil(part.text.length / 4);
}
}
return length;
}
return 3;
};
function createMockRes() {
const events = [];
return {
events,
headersSent: true,
writableEnded: false,
write(payload) {
for (const line of String(payload).split('\n')) {
if (line.startsWith('data: ')) {
events.push(JSON.parse(line.slice(6)));
}
}
return true;
},
};
}
describeIfLive('live usage events through the host pipeline', () => {
jest.setTimeout(120000);
afterAll(async () => {
await GenerationJobManager.destroy();
});
test('streams real provider usage and persists it for resume', async () => {
const streamId = `usage-live-${Date.now()}`;
await GenerationJobManager.createJob(streamId, 'user-live', 'convo-live');
/** streamId mode routes events through the job emitter — capture them
* as a subscribed resumable client would, not via res.write */
const res = createMockRes();
await GenerationJobManager.subscribe(streamId, (event) => {
res.events.push(event);
});
const collectedUsage = [];
const handlers = getDefaultHandlers({
res,
aggregateContent: () => {},
toolEndCallback: () => {},
collectedUsage,
streamId,
});
const run = await Run.create({
runId: 'usage-live-response',
graphConfig: {
type: 'standard',
llmConfig: {
provider: Providers.ANTHROPIC,
model: modelName,
apiKey: process.env.ANTHROPIC_API_KEY,
temperature: 0,
maxTokens: 64,
streaming: true,
streamUsage: true,
},
instructions: 'You are concise. Reply with one short sentence.',
maxContextTokens: 8000,
},
returnContent: true,
customHandlers: handlers,
tokenCounter: charCounter,
indexTokenCountMap: {},
});
await run.processStream(
{ messages: [new HumanMessage('Say hello in five words or fewer.')] },
{
configurable: { thread_id: 'usage-live-thread', user_id: 'user-live' },
streamMode: 'values',
version: 'v2',
},
);
const usageEvents = res.events.filter((e) => e.event === 'on_token_usage');
expect(usageEvents).toHaveLength(1);
const usage = usageEvents[0].data;
expect(usage.input_tokens).toBeGreaterThan(0);
expect(usage.output_tokens).toBeGreaterThan(0);
expect(usage.provider).toBe(Providers.ANTHROPIC);
expect(usage.model).toBe(modelName);
expect(collectedUsage).toHaveLength(1);
expect(usage.input_tokens).toBe(collectedUsage[0].input_tokens);
if (hasContextUsageEvent) {
const contextEvents = res.events.filter((e) => e.event === 'on_context_usage');
expect(contextEvents).toHaveLength(1);
const snapshot = contextEvents[0].data;
expect(snapshot.breakdown.maxContextTokens).toBe(8000);
const estimatedUsed = snapshot.contextBudget - snapshot.remainingContextTokens;
expect(estimatedUsed).toBeGreaterThan(0);
expect(estimatedUsed / usage.input_tokens).toBeGreaterThan(0.2);
expect(estimatedUsed / usage.input_tokens).toBeLessThan(5);
}
const resumeState = await GenerationJobManager.getResumeState(streamId);
expect(resumeState.collectedUsage).toHaveLength(1);
expect(resumeState.collectedUsage[0].input_tokens).toBe(usage.input_tokens);
if (hasContextUsageEvent) {
expect(resumeState.contextUsage.breakdown.maxContextTokens).toBe(8000);
}
});
});