feat: Add normalized types and utility functions for handling chat formats

- Introduced `NormalizedContentBlock`, `NormalizedUsage`, and related types for structured data handling.
- Implemented utility functions for creating and merging normalized usage statistics.
- Added a new module for shared transformers to manage chat formats.

feat: Implement app local state management

- Created functions to clear application installation state and manage local storage keys.
- Added constants for theme and user profile storage keys.

test: Add tests for BrandIcon component rendering

- Implemented tests to ensure BrandIcon does not force a colored background for recognized brands.

test: Enhance ModelAnalysisPanel tests

- Added tests for rendering token summaries with compact units in ModelAnalysisPanel.

test: Add tests for ModernSelect component

- Implemented tests to verify icon rendering for selected options in ModernSelect.

test: Add number formatting tests

- Created tests for `formatCompactTokenMetric` to validate token formatting logic.

feat: Implement account creation intent handling

- Added logic to handle session and API key creation intents in the Accounts page.

test: Add tests for dashboard performance card

- Implemented tests to verify rendering of performance metrics in the dashboard.

feat: Implement default connection segment resolution

- Added logic to determine initial connection segments based on platform types.

test: Add tests for settings factory reset functionality

- Implemented tests to verify the factory reset process and local state clearing.

test: Add tests for site creation redirect logic

- Implemented tests to verify redirection based on site capabilities during creation.

test: Add tests for token routes refresh decision action

- Implemented tests to verify refresh decision actions in TokenRoutes.

test: Add tests for token routes snapshot load

- Implemented tests to verify loading of cached snapshot data in TokenRoutes.
This commit is contained in:
cita
2026-03-08 12:08:57 +08:00
parent ed05b37626
commit 23922a5965
115 changed files with 18597 additions and 4411 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,110 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import {
normalizeStopReason,
normalizeUpstreamFinalResponse,
pullSseEventsWithDone,
serializeFinalResponse,
type NormalizedFinalResponse,
} from './normalized.js';
describe('shared normalized helpers', () => {
it('does not depend on route-level chatFormats helpers', () => {
const source = readFileSync(new URL('./normalized.ts', import.meta.url), 'utf8');
expect(source).not.toContain('routes/proxy/chatFormats');
expect(source).not.toContain('chatFormats.js');
});
it('parses SSE events and keeps the trailing partial block', () => {
const pulled = pullSseEventsWithDone([
'event: message',
'data: {"id":"1"}',
'',
'data: [DONE]',
'',
'data: {"partial":true}',
].join('\n'));
expect(pulled.events).toEqual([
{ event: 'message', data: '{"id":"1"}' },
{ event: '', data: '[DONE]' },
]);
expect(pulled.rest).toBe('data: {"partial":true}');
});
it('normalizes responses payloads with tool calls', () => {
expect(normalizeUpstreamFinalResponse({
object: 'response',
id: 'resp_1',
model: 'gpt-test',
created: 123,
output: [
{
type: 'message',
content: [{ type: 'output_text', text: 'hello' }],
},
{
type: 'function_call',
call_id: 'call_1',
name: 'lookup',
arguments: '{"q":"x"}',
},
],
status: 'completed',
}, 'fallback-model')).toEqual({
id: 'resp_1',
model: 'gpt-test',
created: 123,
content: 'hello',
reasoningContent: '',
finishReason: 'tool_calls',
toolCalls: [{
id: 'call_1',
name: 'lookup',
arguments: '{"q":"x"}',
}],
});
});
it('serializes normalized final responses for claude', () => {
const normalized: NormalizedFinalResponse = {
id: 'chatcmpl-1',
model: 'claude-test',
created: 456,
content: 'done',
reasoningContent: 'thinking',
finishReason: 'tool_calls',
toolCalls: [{ id: 'tool_1', name: 'lookup', arguments: '{"q":"x"}' }],
};
expect(serializeFinalResponse('claude', normalized, {
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
})).toEqual({
id: 'msg_chatcmpl-1',
type: 'message',
role: 'assistant',
model: 'claude-test',
content: [
{ type: 'thinking', thinking: 'thinking' },
{ type: 'text', text: 'done' },
{ type: 'tool_use', id: 'tool_1', name: 'lookup', input: { q: 'x' } },
],
stop_reason: 'tool_use',
stop_sequence: null,
usage: {
input_tokens: 10,
output_tokens: 5,
},
});
});
it('normalizes known stop reasons', () => {
expect(normalizeStopReason('max_output_tokens')).toBe('length');
expect(normalizeStopReason('tool_use')).toBe('tool_calls');
expect(normalizeStopReason('completed')).toBe('stop');
expect(normalizeStopReason('mystery')).toBeNull();
});
});
@@ -0,0 +1,136 @@
export {
buildSyntheticOpenAiChunks,
createClaudeDownstreamContext,
createStreamTransformContext,
normalizeStopReason,
normalizeUpstreamFinalResponse,
normalizeUpstreamStreamEvent,
parseDownstreamChatRequest,
pullSseEventsWithDone,
serializeFinalResponse,
serializeNormalizedStreamEvent,
serializeStreamDone,
toClaudeStopReason,
type ClaudeDownstreamContext,
type DownstreamFormat,
type NormalizedFinalResponse,
type NormalizedStreamEvent,
type ParsedDownstreamChatRequest,
type ParsedSseEvent,
type StreamTransformContext,
} from './chatFormatsCore.js';
export type NormalizedContentBlockType =
| 'text'
| 'image_url'
| 'image_inline'
| 'input_audio'
| 'output_audio'
| 'tool_call'
| 'tool_result'
| 'function_response'
| 'reasoning'
| 'redacted_reasoning';
export type NormalizedContentBlock = {
type: NormalizedContentBlockType;
role?: string | null;
text?: string | null;
mimeType?: string | null;
url?: string | null;
data?: string | null;
toolName?: string | null;
toolCallId?: string | null;
argumentsText?: string | null;
result?: unknown;
metadata?: Record<string, unknown> | null;
};
export type NormalizedUsage = {
promptTokens?: number | null;
completionTokens?: number | null;
totalTokens?: number | null;
cachedTokens?: number | null;
reasoningTokens?: number | null;
audioInputTokens?: number | null;
audioOutputTokens?: number | null;
acceptedPredictionTokens?: number | null;
rejectedPredictionTokens?: number | null;
};
export type ParsedDownstreamChatRequestResult = {
value?: import('./chatFormatsCore.js').ParsedDownstreamChatRequest;
error?: { statusCode: number; payload: unknown };
};
export type TransformerMetadata = {
include?: unknown;
maxToolCalls?: number | null;
promptCacheRetention?: unknown;
includeObfuscation?: boolean | null;
citations?: unknown;
annotations?: unknown;
geminiSafetySettings?: unknown;
geminiImageConfig?: unknown;
thoughtSignature?: string | null;
passthrough?: Record<string, unknown>;
};
export type NormalizedRequest = {
protocol: import('./chatFormatsCore.js').DownstreamFormat | 'responses' | 'gemini';
model: string;
stream: boolean;
rawBody: unknown;
parsed: import('./chatFormatsCore.js').ParsedDownstreamChatRequest | null;
contentBlocks?: NormalizedContentBlock[];
metadata?: TransformerMetadata;
};
export type NormalizedResponseEnvelope = {
protocol: import('./chatFormatsCore.js').DownstreamFormat | 'responses' | 'gemini';
model: string;
final: import('./chatFormatsCore.js').NormalizedFinalResponse;
usage?: unknown;
contentBlocks?: NormalizedContentBlock[];
metadata?: TransformerMetadata;
};
export type NormalizedStreamEnvelope = {
protocol: import('./chatFormatsCore.js').DownstreamFormat | 'responses' | 'gemini';
model: string;
event: import('./chatFormatsCore.js').NormalizedStreamEvent;
metadata?: TransformerMetadata;
};
export function createEmptyNormalizedUsage(): NormalizedUsage {
return {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
cachedTokens: 0,
reasoningTokens: 0,
audioInputTokens: 0,
audioOutputTokens: 0,
acceptedPredictionTokens: 0,
rejectedPredictionTokens: 0,
};
}
export function mergeNormalizedUsage(
base: NormalizedUsage | undefined,
next: NormalizedUsage | undefined,
): NormalizedUsage {
const merged = { ...createEmptyNormalizedUsage(), ...(base || {}) };
const incoming = next || {};
return {
promptTokens: (merged.promptTokens || 0) + (incoming.promptTokens || 0),
completionTokens: (merged.completionTokens || 0) + (incoming.completionTokens || 0),
totalTokens: (merged.totalTokens || 0) + (incoming.totalTokens || 0),
cachedTokens: (merged.cachedTokens || 0) + (incoming.cachedTokens || 0),
reasoningTokens: (merged.reasoningTokens || 0) + (incoming.reasoningTokens || 0),
audioInputTokens: (merged.audioInputTokens || 0) + (incoming.audioInputTokens || 0),
audioOutputTokens: (merged.audioOutputTokens || 0) + (incoming.audioOutputTokens || 0),
acceptedPredictionTokens: (merged.acceptedPredictionTokens || 0) + (incoming.acceptedPredictionTokens || 0),
rejectedPredictionTokens: (merged.rejectedPredictionTokens || 0) + (incoming.rejectedPredictionTokens || 0),
};
}