mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 18:21:09 +00:00
702 lines
20 KiB
JavaScript
Executable File
702 lines
20 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const GlmtTransformer = require('../../dist/glmt/glmt-transformer').default;
|
|
const { DeltaAccumulator } = require('../../dist/glmt/delta-accumulator');
|
|
|
|
/**
|
|
* Token Counting Validation Tests
|
|
*
|
|
* Verifies:
|
|
* 1. message_delta includes both input_tokens and output_tokens
|
|
* 2. Token counts with simple prompts (no tools)
|
|
* 3. Token counts with tool calls
|
|
* 4. Token counts with thinking blocks + tools
|
|
* 5. Deferred finalization waits for usage data
|
|
* 6. Finalization happens when BOTH finish_reason AND usage are received
|
|
* 7. Graceful degradation if usage never arrives
|
|
* 8. No regressions in existing features
|
|
*/
|
|
|
|
class TestRunner {
|
|
constructor() {
|
|
this.tests = [];
|
|
this.passed = 0;
|
|
this.failed = 0;
|
|
}
|
|
|
|
test(name, fn) {
|
|
this.tests.push({ name, fn });
|
|
}
|
|
|
|
async run() {
|
|
console.log('\n=== Token Counting Validation Tests ===\n');
|
|
|
|
for (const { name, fn } of this.tests) {
|
|
try {
|
|
await fn();
|
|
console.log(`✓ ${name}`);
|
|
this.passed++;
|
|
} catch (error) {
|
|
console.error(`✗ ${name}`);
|
|
console.error(` Error: ${error.message}`);
|
|
if (error.stack) {
|
|
console.error(` Stack: ${error.stack.split('\n').slice(1, 3).join('\n')}`);
|
|
}
|
|
this.failed++;
|
|
}
|
|
}
|
|
|
|
console.log(`\n=== Results ===`);
|
|
console.log(`Passed: ${this.passed}/${this.tests.length}`);
|
|
console.log(`Failed: ${this.failed}/${this.tests.length}`);
|
|
|
|
return this.failed === 0;
|
|
}
|
|
}
|
|
|
|
// Assertion helpers
|
|
function assertEqual(actual, expected, message) {
|
|
if (actual !== expected) {
|
|
throw new Error(
|
|
`${message || 'Assertion failed'}\n` +
|
|
` Expected: ${JSON.stringify(expected)}\n` +
|
|
` Actual: ${JSON.stringify(actual)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertTrue(value, message) {
|
|
if (!value) {
|
|
throw new Error(message || 'Expected true');
|
|
}
|
|
}
|
|
|
|
function assertExists(value, message) {
|
|
if (value === undefined || value === null) {
|
|
throw new Error(message || 'Value should exist');
|
|
}
|
|
}
|
|
|
|
function assertDeepEqual(actual, expected, message) {
|
|
const actualStr = JSON.stringify(actual);
|
|
const expectedStr = JSON.stringify(expected);
|
|
if (actualStr !== expectedStr) {
|
|
throw new Error(
|
|
`${message || 'Deep equality failed'}\n` +
|
|
` Expected: ${expectedStr}\n` +
|
|
` Actual: ${actualStr}`
|
|
);
|
|
}
|
|
}
|
|
|
|
const runner = new TestRunner();
|
|
|
|
// ========================================
|
|
// Test 1: message_delta includes input_tokens and output_tokens
|
|
// ========================================
|
|
runner.test('message_delta includes both input_tokens and output_tokens', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
|
|
// Simulate usage data
|
|
accumulator.updateUsage({
|
|
prompt_tokens: 150,
|
|
completion_tokens: 75,
|
|
});
|
|
accumulator.finishReason = 'stop';
|
|
|
|
const events = transformer.finalizeDelta(accumulator);
|
|
|
|
// Find message_delta event
|
|
const messageDelta = events.find((e) => e.event === 'message_delta');
|
|
assertExists(messageDelta, 'message_delta event should exist');
|
|
assertExists(messageDelta.data.usage, 'usage should exist in message_delta');
|
|
assertEqual(messageDelta.data.usage.input_tokens, 150, 'input_tokens should be 150');
|
|
assertEqual(messageDelta.data.usage.output_tokens, 75, 'output_tokens should be 75');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 2: Token counting with simple prompts (no tools)
|
|
// ========================================
|
|
runner.test('token counting with simple prompt (no tools)', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const openaiResponse = {
|
|
id: 'chatcmpl-123',
|
|
model: 'GLM-4.6',
|
|
choices: [
|
|
{
|
|
message: {
|
|
role: 'assistant',
|
|
content: 'Simple response',
|
|
},
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: 10,
|
|
completion_tokens: 5,
|
|
total_tokens: 15,
|
|
},
|
|
};
|
|
|
|
const result = transformer.transformResponse(openaiResponse, {});
|
|
|
|
assertExists(result.usage, 'usage should exist');
|
|
assertEqual(result.usage.input_tokens, 10, 'input_tokens should be 10');
|
|
assertEqual(result.usage.output_tokens, 5, 'output_tokens should be 5');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 3: Token counting with tool calls
|
|
// ========================================
|
|
runner.test('token counting with tool calls', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const openaiResponse = {
|
|
id: 'chatcmpl-456',
|
|
model: 'GLM-4.6',
|
|
choices: [
|
|
{
|
|
message: {
|
|
role: 'assistant',
|
|
content: null,
|
|
tool_calls: [
|
|
{
|
|
id: 'call_1',
|
|
type: 'function',
|
|
function: {
|
|
name: 'get_weather',
|
|
arguments: '{"location":"London"}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
finish_reason: 'tool_calls',
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: 50,
|
|
completion_tokens: 25,
|
|
total_tokens: 75,
|
|
},
|
|
};
|
|
|
|
const result = transformer.transformResponse(openaiResponse, {});
|
|
|
|
assertExists(result.usage, 'usage should exist');
|
|
assertEqual(result.usage.input_tokens, 50, 'input_tokens should be 50');
|
|
assertEqual(result.usage.output_tokens, 25, 'output_tokens should be 25');
|
|
assertEqual(result.stop_reason, 'tool_use', 'stop_reason should be tool_use');
|
|
assertTrue(
|
|
result.content.some((b) => b.type === 'tool_use'),
|
|
'should have tool_use block'
|
|
);
|
|
});
|
|
|
|
// ========================================
|
|
// Test 4: Token counting with thinking blocks + tools
|
|
// ========================================
|
|
runner.test('token counting with thinking blocks and tool calls', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const openaiResponse = {
|
|
id: 'chatcmpl-789',
|
|
model: 'GLM-4.6',
|
|
choices: [
|
|
{
|
|
message: {
|
|
role: 'assistant',
|
|
reasoning_content: 'Let me analyze this request...',
|
|
content: 'I need to call a tool',
|
|
tool_calls: [
|
|
{
|
|
id: 'call_2',
|
|
type: 'function',
|
|
function: {
|
|
name: 'calculate',
|
|
arguments: '{"expression":"2+2"}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
finish_reason: 'tool_calls',
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: 100,
|
|
completion_tokens: 80,
|
|
total_tokens: 180,
|
|
},
|
|
};
|
|
|
|
const result = transformer.transformResponse(openaiResponse, {});
|
|
|
|
assertExists(result.usage, 'usage should exist');
|
|
assertEqual(result.usage.input_tokens, 100, 'input_tokens should be 100');
|
|
assertEqual(result.usage.output_tokens, 80, 'output_tokens should be 80');
|
|
assertTrue(
|
|
result.content.some((b) => b.type === 'thinking'),
|
|
'should have thinking block'
|
|
);
|
|
assertTrue(
|
|
result.content.some((b) => b.type === 'tool_use'),
|
|
'should have tool_use block'
|
|
);
|
|
});
|
|
|
|
// ========================================
|
|
// Test 5: Deferred finalization waits for usage data
|
|
// ========================================
|
|
runner.test('deferred finalization waits for usage data', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
|
|
// Simulate finish_reason arriving first
|
|
accumulator.finishReason = 'stop';
|
|
accumulator.messageStarted = true;
|
|
|
|
// Usage hasn't arrived yet - should NOT finalize
|
|
assertEqual(accumulator.usageReceived, false, 'usageReceived should be false initially');
|
|
|
|
// Simulate transformDelta with finish_reason but no usage
|
|
const openaiEvent1 = {
|
|
event: 'data',
|
|
data: {
|
|
choices: [
|
|
{
|
|
delta: {},
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const events1 = transformer.transformDelta(openaiEvent1, accumulator);
|
|
|
|
// Should NOT have message_stop event yet
|
|
const hasMessageStop1 = events1.some((e) => e.event === 'message_stop');
|
|
assertEqual(hasMessageStop1, false, 'should NOT finalize without usage');
|
|
assertEqual(accumulator.finalized, false, 'accumulator should NOT be finalized');
|
|
|
|
// Now usage arrives
|
|
const openaiEvent2 = {
|
|
event: 'data',
|
|
data: {
|
|
usage: {
|
|
prompt_tokens: 200,
|
|
completion_tokens: 100,
|
|
},
|
|
},
|
|
};
|
|
|
|
const events2 = transformer.transformDelta(openaiEvent2, accumulator);
|
|
|
|
// Should NOW finalize since we have both finish_reason AND usage
|
|
const hasMessageStop2 = events2.some((e) => e.event === 'message_stop');
|
|
assertEqual(hasMessageStop2, true, 'should finalize when usage arrives');
|
|
assertEqual(accumulator.finalized, true, 'accumulator should be finalized');
|
|
assertEqual(accumulator.usageReceived, true, 'usageReceived should be true');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 6: Finalization happens when BOTH finish_reason AND usage received
|
|
// ========================================
|
|
runner.test('finalization waits for BOTH finish_reason AND usage', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
accumulator.messageStarted = true;
|
|
|
|
// Test case A: Usage arrives first
|
|
const openaiEvent1 = {
|
|
event: 'data',
|
|
data: {
|
|
usage: {
|
|
prompt_tokens: 50,
|
|
completion_tokens: 30,
|
|
},
|
|
},
|
|
};
|
|
|
|
const events1 = transformer.transformDelta(openaiEvent1, accumulator);
|
|
assertEqual(accumulator.usageReceived, true, 'usage should be received');
|
|
assertEqual(accumulator.finalized, false, 'should NOT finalize with only usage');
|
|
|
|
// Test case B: finish_reason arrives second
|
|
const openaiEvent2 = {
|
|
event: 'data',
|
|
data: {
|
|
choices: [
|
|
{
|
|
delta: {},
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const events2 = transformer.transformDelta(openaiEvent2, accumulator);
|
|
assertEqual(accumulator.finishReason, 'stop', 'finish_reason should be set');
|
|
assertEqual(accumulator.finalized, true, 'should finalize when both present');
|
|
|
|
const messageDelta = events2.find((e) => e.event === 'message_delta');
|
|
assertExists(messageDelta, 'message_delta should exist');
|
|
assertEqual(messageDelta.data.usage.input_tokens, 50, 'input_tokens in message_delta');
|
|
assertEqual(messageDelta.data.usage.output_tokens, 30, 'output_tokens in message_delta');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 7: Graceful degradation if usage never arrives
|
|
// ========================================
|
|
runner.test('graceful degradation when usage never arrives', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
accumulator.messageStarted = true;
|
|
|
|
// finish_reason arrives
|
|
accumulator.finishReason = 'stop';
|
|
|
|
// Simulate [DONE] event without usage
|
|
const doneEvent = {
|
|
event: 'done',
|
|
};
|
|
|
|
const events = transformer.transformDelta(doneEvent, accumulator);
|
|
|
|
// Should finalize with zero tokens (graceful degradation)
|
|
assertEqual(accumulator.finalized, true, 'should finalize on [DONE]');
|
|
const messageDelta = events.find((e) => e.event === 'message_delta');
|
|
assertExists(messageDelta, 'message_delta should exist');
|
|
assertEqual(messageDelta.data.usage.input_tokens, 0, 'input_tokens should be 0');
|
|
assertEqual(messageDelta.data.usage.output_tokens, 0, 'output_tokens should be 0');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 8: No regression - thinking blocks still work
|
|
// ========================================
|
|
runner.test('no regression: thinking blocks still work', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
|
|
// Start message
|
|
const event1 = {
|
|
event: 'data',
|
|
data: {
|
|
model: 'GLM-4.6',
|
|
choices: [
|
|
{
|
|
delta: { role: 'assistant' },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
transformer.transformDelta(event1, accumulator);
|
|
|
|
// Thinking delta
|
|
const event2 = {
|
|
event: 'data',
|
|
data: {
|
|
choices: [
|
|
{
|
|
delta: {
|
|
reasoning_content: 'Analyzing the problem...',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const events2 = transformer.transformDelta(event2, accumulator);
|
|
|
|
// Check thinking block was created
|
|
const hasThinkingStart = events2.some(
|
|
(e) => e.event === 'content_block_start' && e.data.content_block.type === 'thinking'
|
|
);
|
|
assertEqual(hasThinkingStart, true, 'thinking block should start');
|
|
|
|
const hasThinkingDelta = events2.some(
|
|
(e) => e.event === 'content_block_delta' && e.data.delta.type === 'thinking_delta'
|
|
);
|
|
assertEqual(hasThinkingDelta, true, 'thinking delta should be emitted');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 9: No regression - tool calls execute correctly
|
|
// ========================================
|
|
runner.test('no regression: tool calls execute correctly', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
accumulator.messageStarted = true;
|
|
|
|
// Tool call delta
|
|
const event = {
|
|
event: 'data',
|
|
data: {
|
|
choices: [
|
|
{
|
|
delta: {
|
|
tool_calls: [
|
|
{
|
|
index: 0,
|
|
id: 'call_abc',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search',
|
|
arguments: '{"q":"test"}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const events = transformer.transformDelta(event, accumulator);
|
|
|
|
const toolUseStart = events.find(
|
|
(e) => e.event === 'content_block_start' && e.data.content_block.type === 'tool_use'
|
|
);
|
|
assertExists(toolUseStart, 'tool_use block should start');
|
|
assertEqual(toolUseStart.data.content_block.name, 'search', 'tool name should be search');
|
|
|
|
const inputJsonDelta = events.find(
|
|
(e) => e.event === 'content_block_delta' && e.data.delta.type === 'input_json_delta'
|
|
);
|
|
assertExists(inputJsonDelta, 'input_json_delta should be emitted');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 10: No regression - streaming still works
|
|
// ========================================
|
|
runner.test('no regression: streaming still works', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
|
|
// Message start
|
|
const event1 = {
|
|
event: 'data',
|
|
data: {
|
|
model: 'GLM-4.6',
|
|
choices: [{ delta: { role: 'assistant' } }],
|
|
},
|
|
};
|
|
const events1 = transformer.transformDelta(event1, accumulator);
|
|
assertTrue(
|
|
events1.some((e) => e.event === 'message_start'),
|
|
'message_start event'
|
|
);
|
|
|
|
// Text delta
|
|
const event2 = {
|
|
event: 'data',
|
|
data: {
|
|
choices: [{ delta: { content: 'Hello' } }],
|
|
},
|
|
};
|
|
const events2 = transformer.transformDelta(event2, accumulator);
|
|
assertTrue(
|
|
events2.some((e) => e.event === 'content_block_start'),
|
|
'content_block_start'
|
|
);
|
|
assertTrue(
|
|
events2.some((e) => e.event === 'content_block_delta'),
|
|
'content_block_delta'
|
|
);
|
|
|
|
// More text
|
|
const event3 = {
|
|
event: 'data',
|
|
data: {
|
|
choices: [{ delta: { content: ' world' } }],
|
|
},
|
|
};
|
|
const events3 = transformer.transformDelta(event3, accumulator);
|
|
const textDelta = events3.find((e) => e.data?.delta?.type === 'text_delta');
|
|
assertExists(textDelta, 'text_delta should exist');
|
|
assertEqual(textDelta.data.delta.text, ' world', 'delta text should be " world"');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 11: No regression - buffered mode still works
|
|
// ========================================
|
|
runner.test('no regression: buffered mode (non-streaming) works', () => {
|
|
const transformer = new GlmtTransformer();
|
|
|
|
const openaiResponse = {
|
|
id: 'chatcmpl-buffered',
|
|
model: 'GLM-4.6',
|
|
choices: [
|
|
{
|
|
message: {
|
|
role: 'assistant',
|
|
reasoning_content: 'Thinking step by step...',
|
|
content: 'Final answer',
|
|
},
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: 20,
|
|
completion_tokens: 15,
|
|
total_tokens: 35,
|
|
},
|
|
};
|
|
|
|
const result = transformer.transformResponse(openaiResponse, {});
|
|
|
|
assertEqual(result.type, 'message', 'type should be message');
|
|
assertEqual(result.role, 'assistant', 'role should be assistant');
|
|
assertTrue(
|
|
result.content.some((b) => b.type === 'thinking'),
|
|
'has thinking'
|
|
);
|
|
assertTrue(
|
|
result.content.some((b) => b.type === 'text'),
|
|
'has text'
|
|
);
|
|
assertEqual(result.usage.input_tokens, 20, 'input_tokens');
|
|
assertEqual(result.usage.output_tokens, 15, 'output_tokens');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 12: usageReceived flag is set correctly
|
|
// ========================================
|
|
runner.test('usageReceived flag is set when usage data arrives', () => {
|
|
const accumulator = new DeltaAccumulator();
|
|
|
|
assertEqual(accumulator.usageReceived, false, 'initial value should be false');
|
|
|
|
accumulator.updateUsage({
|
|
prompt_tokens: 100,
|
|
completion_tokens: 50,
|
|
});
|
|
|
|
assertEqual(accumulator.usageReceived, true, 'should be true after updateUsage');
|
|
assertEqual(accumulator.inputTokens, 100, 'inputTokens should be 100');
|
|
assertEqual(accumulator.outputTokens, 50, 'outputTokens should be 50');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 13: Double finalization protection
|
|
// ========================================
|
|
runner.test('double finalization protection works', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
accumulator.messageStarted = true;
|
|
accumulator.finishReason = 'stop';
|
|
accumulator.updateUsage({ prompt_tokens: 10, completion_tokens: 5 });
|
|
|
|
// First finalization
|
|
const events1 = transformer.finalizeDelta(accumulator);
|
|
assertTrue(events1.length > 0, 'should return events on first finalization');
|
|
assertEqual(accumulator.finalized, true, 'should be finalized');
|
|
|
|
// Second finalization attempt
|
|
const events2 = transformer.finalizeDelta(accumulator);
|
|
assertEqual(events2.length, 0, 'should return empty array on second call');
|
|
});
|
|
|
|
// ========================================
|
|
// Test 14: Token counts in streaming with thinking + text + tools
|
|
// ========================================
|
|
runner.test('streaming: token counts with thinking + text + tools', () => {
|
|
const transformer = new GlmtTransformer();
|
|
const accumulator = new DeltaAccumulator();
|
|
|
|
// Message start
|
|
transformer.transformDelta(
|
|
{
|
|
event: 'data',
|
|
data: {
|
|
model: 'GLM-4.6',
|
|
choices: [{ delta: { role: 'assistant' } }],
|
|
},
|
|
},
|
|
accumulator
|
|
);
|
|
|
|
// Thinking
|
|
transformer.transformDelta(
|
|
{
|
|
event: 'data',
|
|
data: {
|
|
choices: [{ delta: { reasoning_content: 'Thinking...' } }],
|
|
},
|
|
},
|
|
accumulator
|
|
);
|
|
|
|
// Text
|
|
transformer.transformDelta(
|
|
{
|
|
event: 'data',
|
|
data: {
|
|
choices: [{ delta: { content: 'Answer' } }],
|
|
},
|
|
},
|
|
accumulator
|
|
);
|
|
|
|
// Tool call
|
|
transformer.transformDelta(
|
|
{
|
|
event: 'data',
|
|
data: {
|
|
choices: [
|
|
{
|
|
delta: {
|
|
tool_calls: [
|
|
{
|
|
index: 0,
|
|
id: 'call_1',
|
|
type: 'function',
|
|
function: { name: 'tool', arguments: '{}' },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
accumulator
|
|
);
|
|
|
|
// Usage arrives
|
|
transformer.transformDelta(
|
|
{
|
|
event: 'data',
|
|
data: {
|
|
usage: { prompt_tokens: 300, completion_tokens: 200 },
|
|
},
|
|
},
|
|
accumulator
|
|
);
|
|
|
|
// finish_reason arrives
|
|
const finalEvents = transformer.transformDelta(
|
|
{
|
|
event: 'data',
|
|
data: {
|
|
choices: [{ delta: {}, finish_reason: 'tool_calls' }],
|
|
},
|
|
},
|
|
accumulator
|
|
);
|
|
|
|
// Verify message_delta has correct tokens
|
|
const messageDelta = finalEvents.find((e) => e.event === 'message_delta');
|
|
assertExists(messageDelta, 'message_delta should exist');
|
|
assertEqual(messageDelta.data.usage.input_tokens, 300, 'input_tokens should be 300');
|
|
assertEqual(messageDelta.data.usage.output_tokens, 200, 'output_tokens should be 200');
|
|
assertEqual(messageDelta.data.delta.stop_reason, 'tool_use', 'stop_reason should be tool_use');
|
|
});
|
|
|
|
// Run all tests
|
|
runner
|
|
.run()
|
|
.then((success) => {
|
|
process.exit(success ? 0 : 1);
|
|
})
|
|
.catch((error) => {
|
|
console.error('Test runner error:', error);
|
|
process.exit(1);
|
|
});
|