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:
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user