chore: initialize clean metapi repository

This commit is contained in:
cita
2026-02-28 00:36:28 +08:00
commit 2ecc23730d
199 changed files with 46078 additions and 0 deletions
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { getAccountsAddPanelStyle } from './accountsPanelStyle.js';
describe('getAccountsAddPanelStyle', () => {
it('preserves spacing and raises stacking context so select dropdown is clickable', () => {
const style = getAccountsAddPanelStyle();
expect(style.padding).toBe(20);
expect(style.marginBottom).toBe(16);
expect(style.position).toBe('relative');
expect(style.zIndex).toBeGreaterThan(0);
expect(style.overflow).toBe('visible');
});
});
@@ -0,0 +1,9 @@
export function getAccountsAddPanelStyle() {
return {
padding: 20,
marginBottom: 16,
position: 'relative' as const,
zIndex: 10,
overflow: 'visible' as const,
};
}
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import {
formatCheckinLogTime,
formatDateLocal,
formatDateTimeMinuteLocal,
} from './checkinLogTime.js';
describe('formatCheckinLogTime', () => {
it('formats sqlite UTC timestamp as local display time', () => {
const formatted = formatCheckinLogTime('2026-02-25 03:51:58', 'en-US', 'UTC');
expect(formatted).toBe('02/25/2026, 03:51:58');
});
it('keeps original value for invalid timestamp', () => {
expect(formatCheckinLogTime('invalid-time', 'en-US', 'UTC')).toBe('invalid-time');
});
it('formats local date with minute precision helpers', () => {
expect(formatDateLocal('2026-02-25 03:51:58', 'en-US', 'UTC')).toBe('02/25/2026');
expect(formatDateTimeMinuteLocal('2026-02-25 03:51:58', 'en-US', 'UTC')).toBe('02/25/2026, 03:51');
});
});
+89
View File
@@ -0,0 +1,89 @@
function parseServerUtcDate(value: string | null | undefined): { date: Date | null; raw: string } {
if (!value) return { date: null, raw: '' };
const raw = String(value).trim();
if (!raw) return { date: null, raw };
let normalized = raw;
if (!normalized.includes('T') && normalized.includes(' ')) {
normalized = normalized.replace(' ', 'T');
}
const hasTimeZone = /[zZ]$|[+-]\d{2}:\d{2}$/.test(normalized);
if (!hasTimeZone) {
normalized = `${normalized}Z`;
}
const date = new Date(normalized);
if (Number.isNaN(date.getTime())) {
return { date: null, raw };
}
return { date, raw };
}
function formatWithParts(
value: string | null | undefined,
options: Intl.DateTimeFormatOptions,
locale = 'zh-CN',
timeZone?: string,
): string {
const { date, raw } = parseServerUtcDate(value);
if (!date) return raw || '-';
return new Intl.DateTimeFormat(locale, {
...options,
...(timeZone ? { timeZone } : {}),
}).format(date);
}
export function formatDateTimeLocal(
value: string | null | undefined,
locale = 'zh-CN',
timeZone?: string,
): string {
return formatWithParts(value, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}, locale, timeZone);
}
export function formatDateTimeMinuteLocal(
value: string | null | undefined,
locale = 'zh-CN',
timeZone?: string,
): string {
return formatWithParts(value, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
}, locale, timeZone);
}
export function formatDateLocal(
value: string | null | undefined,
locale = 'zh-CN',
timeZone?: string,
): string {
return formatWithParts(value, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}, locale, timeZone);
}
export function formatCheckinLogTime(
value: string | null | undefined,
locale?: string,
timeZone?: string,
): string {
return formatDateTimeLocal(value, locale, timeZone);
}
@@ -0,0 +1,264 @@
import { describe, expect, it } from 'vitest';
import {
DEBUG_TABS,
DEFAULT_INPUTS,
DEFAULT_PARAMETER_ENABLED,
MESSAGE_STATUS,
buildApiPayload,
collectModelTesterModelNames,
countConversationTurns,
filterModelTesterModelNames,
parseCustomRequestBody,
parseModelTesterSession,
serializeModelTesterSession,
syncMessagesToCustomRequestBody,
toApiMessages,
type ModelTesterSessionState,
} from './modelTesterSession.js';
describe('modelTesterSession', () => {
it('counts only user messages as turns', () => {
const turns = countConversationTurns([
{ id: '1', role: 'user', content: 'hello', createAt: 1 },
{ id: '2', role: 'assistant', content: 'hi', createAt: 2 },
{ id: '3', role: 'system', content: 'meta', createAt: 3 },
{ id: '4', role: 'user', content: 'again', createAt: 4 },
]);
expect(turns).toBe(2);
});
it('serializes and parses full playground session state', () => {
const state: ModelTesterSessionState = {
input: 'draft',
inputs: {
...DEFAULT_INPUTS,
model: 'gpt-4o-mini',
temperature: 0.6,
top_p: 0.9,
max_tokens: 2048,
frequency_penalty: 0.1,
presence_penalty: 0.2,
seed: 12,
stream: true,
},
parameterEnabled: {
...DEFAULT_PARAMETER_ENABLED,
max_tokens: true,
seed: true,
},
messages: [
{ id: 'm1', role: 'user', content: 'hello', createAt: 1 },
{ id: 'm2', role: 'assistant', content: 'hi', createAt: 2, status: MESSAGE_STATUS.COMPLETE },
],
pendingPayload: {
model: 'gpt-4o-mini',
stream: true,
temperature: 0.6,
top_p: 0.9,
messages: [{ role: 'user', content: 'hello' }],
},
pendingJobId: 'job-1',
customRequestMode: true,
customRequestBody: '{"model":"gpt-4o-mini","messages":[]}',
showDebugPanel: true,
activeDebugTab: DEBUG_TABS.REQUEST,
};
const serialized = serializeModelTesterSession(state);
const restored = parseModelTesterSession(serialized);
expect(restored).toEqual(state);
});
it('supports parsing legacy session format', () => {
const restored = parseModelTesterSession(JSON.stringify({
model: 'gpt-4o',
temperature: 0.5,
input: 'legacy',
messages: [{ role: 'user', content: 'hello' }],
pendingPayload: null,
}));
expect(restored?.inputs.model).toBe('gpt-4o');
expect(restored?.inputs.temperature).toBe(0.5);
expect(restored?.parameterEnabled).toEqual(DEFAULT_PARAMETER_ENABLED);
});
it('returns null for malformed or missing session payload', () => {
expect(parseModelTesterSession(null)).toBeNull();
expect(parseModelTesterSession('not-json')).toBeNull();
expect(parseModelTesterSession(JSON.stringify({ messages: [] }))).toBeNull();
});
it('sanitizes invalid message entries and invalid pending payload', () => {
const restored = parseModelTesterSession(JSON.stringify({
inputs: { model: 'gpt-4o', temperature: 99 },
parameterEnabled: {},
input: 123,
messages: [
{ role: 'user', content: 'ok' },
{ role: 'assistant', content: 123 },
{ role: 'unknown', content: 'bad role' },
{ content: 'missing role' },
],
pendingPayload: {
model: 123,
messages: [{ role: 'user', content: 'still here' }],
},
}));
expect(restored?.inputs.model).toBe('gpt-4o');
expect(restored?.input).toBe('');
expect(restored?.pendingPayload).toBeNull();
expect(restored?.messages).toHaveLength(1);
expect(restored?.messages[0]).toMatchObject({ role: 'user', content: 'ok' });
});
it('finalizes loading messages if no pending job remains', () => {
const restored = parseModelTesterSession(JSON.stringify({
inputs: { ...DEFAULT_INPUTS, model: 'gpt-4o' },
parameterEnabled: DEFAULT_PARAMETER_ENABLED,
input: '',
messages: [{
id: 'm-loading',
role: 'assistant',
content: '<think>reasoning...</think>final',
createAt: 1,
status: 'loading',
}],
pendingPayload: null,
pendingJobId: null,
}));
expect(restored?.messages[0].status).toBe(MESSAGE_STATUS.COMPLETE);
expect(restored?.messages[0].content).toBe('final');
});
it('drops loading assistant placeholders when building API payload messages', () => {
const payloadMessages = toApiMessages([
{ id: '1', role: 'user', content: 'hello', createAt: 1 },
{ id: '2', role: 'assistant', content: '', createAt: 2, status: MESSAGE_STATUS.LOADING },
{ id: '3', role: 'assistant', content: 'done', createAt: 3, status: MESSAGE_STATUS.COMPLETE },
]);
expect(payloadMessages).toEqual([
{ role: 'user', content: 'hello' },
{ role: 'assistant', content: 'done' },
]);
});
it('builds payload with parameter enable switches', () => {
const payload = buildApiPayload(
[{ id: 'u1', role: 'user', content: 'hello', createAt: 1 }],
{
...DEFAULT_INPUTS,
model: 'gpt-4o-mini',
temperature: 0.5,
top_p: 0.8,
max_tokens: 200,
frequency_penalty: 0.2,
presence_penalty: 0.3,
seed: 42,
stream: true,
},
{
temperature: true,
top_p: false,
max_tokens: true,
frequency_penalty: true,
presence_penalty: false,
seed: true,
},
);
expect(payload).toEqual({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'hello' }],
targetFormat: 'openai',
stream: true,
temperature: 0.5,
max_tokens: 200,
frequency_penalty: 0.2,
seed: 42,
});
});
it('parses and syncs custom request body', () => {
const parsed = parseCustomRequestBody('{"model":"gpt-4o","messages":[{"role":"user","content":"x"}],"stream":true}');
expect(parsed?.model).toBe('gpt-4o');
expect(parsed?.stream).toBe(true);
const synced = syncMessagesToCustomRequestBody(
'{"model":"legacy"}',
[{ id: '1', role: 'user', content: 'new', createAt: 1 }],
{ ...DEFAULT_INPUTS, model: 'gpt-4o' },
);
const syncedJson = JSON.parse(synced);
expect(syncedJson.model).toBe('legacy');
expect(syncedJson.messages).toEqual([{ role: 'user', content: 'new' }]);
});
it('merges marketplace models with exact enabled route models for tester options', () => {
const modelNames = collectModelTesterModelNames(
{
models: [
{ name: 'gpt-4o-mini' },
{ name: 'bge-large-en-v1.5' },
],
},
[
{ modelPattern: 'BAAI/bge-large-en-v1.5', enabled: true },
{ modelPattern: 'claude-*', enabled: true },
{ modelPattern: 'gemini-2.5-pro', enabled: false },
],
);
expect(modelNames).toEqual([
'gpt-4o-mini',
'bge-large-en-v1.5',
'BAAI/bge-large-en-v1.5',
]);
});
it('deduplicates repeated model names when merging model sources', () => {
const modelNames = collectModelTesterModelNames(
{
models: [
{ name: 'gpt-4o-mini' },
{ name: 'gpt-4o-mini' },
],
},
[
{ modelPattern: 'gpt-4o-mini', enabled: true },
],
);
expect(modelNames).toEqual(['gpt-4o-mini']);
});
it('filters models by keyword and keeps best matches first', () => {
const filtered = filterModelTesterModelNames(
[
'BAAI/bge-large-en-v1.5',
'text-embedding-3-large',
'bge-m3',
],
'bge',
);
expect(filtered).toEqual([
'bge-m3',
'BAAI/bge-large-en-v1.5',
]);
});
it('returns all models unchanged when keyword is empty', () => {
const filtered = filterModelTesterModelNames(
['gpt-4o', 'claude-3-5-haiku-20241022'],
' ',
);
expect(filtered).toEqual(['gpt-4o', 'claude-3-5-haiku-20241022']);
});
});
+544
View File
@@ -0,0 +1,544 @@
export const MESSAGE_STATUS = {
LOADING: 'loading',
INCOMPLETE: 'incomplete',
COMPLETE: 'complete',
ERROR: 'error',
} as const;
export const DEBUG_TABS = {
PREVIEW: 'preview',
REQUEST: 'request',
RESPONSE: 'response',
} as const;
type MessageStatus = typeof MESSAGE_STATUS[keyof typeof MESSAGE_STATUS];
export type DebugTab = typeof DEBUG_TABS[keyof typeof DEBUG_TABS];
type ChatRole = 'user' | 'assistant' | 'system';
export type TestTargetFormat = 'openai' | 'claude';
export type ChatMessage = {
id: string;
role: ChatRole;
content: string;
createAt: number;
status?: MessageStatus;
reasoningContent?: string | null;
isReasoningExpanded?: boolean;
isThinkingComplete?: boolean;
hasAutoCollapsed?: boolean;
};
type ApiChatMessage = {
role: ChatRole;
content: string;
};
export type ModelTesterInputs = {
model: string;
targetFormat: TestTargetFormat;
temperature: number;
top_p: number;
max_tokens: number;
frequency_penalty: number;
presence_penalty: number;
seed: number | null;
stream: boolean;
};
export type ParameterEnabled = {
temperature: boolean;
top_p: boolean;
max_tokens: boolean;
frequency_penalty: boolean;
presence_penalty: boolean;
seed: boolean;
};
export type TestChatPayload = {
model: string;
messages: ApiChatMessage[];
targetFormat?: TestTargetFormat;
stream?: boolean;
temperature?: number;
top_p?: number;
max_tokens?: number;
frequency_penalty?: number;
presence_penalty?: number;
seed?: number;
};
export type ModelTesterSessionState = {
input: string;
inputs: ModelTesterInputs;
parameterEnabled: ParameterEnabled;
messages: ChatMessage[];
pendingPayload: TestChatPayload | null;
pendingJobId?: string | null;
customRequestMode: boolean;
customRequestBody: string;
showDebugPanel: boolean;
activeDebugTab: DebugTab;
};
export const MODEL_TESTER_STORAGE_KEY = 'metapi:model-tester:session:v4';
export const DEFAULT_INPUTS: ModelTesterInputs = {
model: '',
targetFormat: 'openai',
temperature: 0.7,
top_p: 1,
max_tokens: 4096,
frequency_penalty: 0,
presence_penalty: 0,
seed: null,
stream: false,
};
export const DEFAULT_PARAMETER_ENABLED: ParameterEnabled = {
temperature: true,
top_p: true,
max_tokens: false,
frequency_penalty: true,
presence_penalty: true,
seed: false,
};
const THINK_TAG_REGEX = /<think>([\s\S]*?)<\/think>/g;
const VALID_ROLES: ReadonlySet<string> = new Set(['user', 'assistant', 'system']);
const VALID_STATUS: ReadonlySet<string> = new Set(Object.values(MESSAGE_STATUS));
const VALID_DEBUG_TABS: ReadonlySet<string> = new Set(Object.values(DEBUG_TABS));
let messageCounter = 0;
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;
const toFiniteNumber = (value: unknown, fallback: number): number =>
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
const toBoolean = (value: unknown, fallback: boolean): boolean =>
typeof value === 'boolean' ? value : fallback;
const toNullableFiniteNumber = (value: unknown): number | null => {
if (value === null || value === undefined || value === '') return null;
if (typeof value === 'number' && Number.isFinite(value)) return value;
return null;
};
const isExactModelPattern = (modelPattern: string): boolean => !/[\*\?\[]/.test(modelPattern);
export const collectModelTesterModelNames = (
marketplace: { models?: Array<{ name?: unknown }>; } | null | undefined,
routes: Array<{ modelPattern?: unknown; enabled?: unknown; }> | null | undefined,
): string[] => {
const result: string[] = [];
const seen = new Set<string>();
const appendModel = (rawName: unknown) => {
if (typeof rawName !== 'string') return;
const name = rawName.trim();
if (!name || seen.has(name)) return;
seen.add(name);
result.push(name);
};
for (const item of marketplace?.models || []) {
appendModel(item?.name);
}
for (const route of routes || []) {
if (!route || route.enabled === false) continue;
if (typeof route.modelPattern !== 'string') continue;
const modelPattern = route.modelPattern.trim();
if (!modelPattern || !isExactModelPattern(modelPattern)) continue;
appendModel(modelPattern);
}
return result;
};
export const filterModelTesterModelNames = (models: string[], query: string): string[] => {
const keyword = query.trim().toLowerCase();
if (!keyword) return [...models];
return models
.map((name, index) => {
const matchIndex = name.toLowerCase().indexOf(keyword);
if (matchIndex === -1) return null;
return { name, matchIndex, index };
})
.filter((item): item is { name: string; matchIndex: number; index: number } => item !== null)
.sort((a, b) => {
if (a.matchIndex !== b.matchIndex) return a.matchIndex - b.matchIndex;
if (a.name.length !== b.name.length) return a.name.length - b.name.length;
return a.index - b.index;
})
.map((item) => item.name);
};
const createMessageId = (): string => {
messageCounter += 1;
return `msg-${Date.now()}-${messageCounter}`;
};
export const createMessage = (role: ChatRole, content: string, extra: Partial<ChatMessage> = {}): ChatMessage => ({
id: createMessageId(),
role,
content,
createAt: Date.now(),
...extra,
});
export const createLoadingAssistantMessage = (): ChatMessage =>
createMessage('assistant', '', {
status: MESSAGE_STATUS.LOADING,
reasoningContent: '',
isReasoningExpanded: true,
isThinkingComplete: false,
hasAutoCollapsed: false,
});
const parseMessage = (value: unknown, index: number): ChatMessage | null => {
if (!isRecord(value)) return null;
if (typeof value.role !== 'string' || !VALID_ROLES.has(value.role)) return null;
if (typeof value.content !== 'string') return null;
const parsed: ChatMessage = {
id: typeof value.id === 'string' && value.id.trim().length > 0
? value.id
: `legacy-${index}-${Date.now()}`,
role: value.role as ChatRole,
content: value.content,
createAt: typeof value.createAt === 'number' && Number.isFinite(value.createAt)
? value.createAt
: Date.now(),
};
if (typeof value.status === 'string' && VALID_STATUS.has(value.status)) {
parsed.status = value.status as MessageStatus;
}
if (typeof value.reasoningContent === 'string') {
parsed.reasoningContent = value.reasoningContent;
} else if (value.reasoningContent === null) {
parsed.reasoningContent = null;
}
if (typeof value.isReasoningExpanded === 'boolean') {
parsed.isReasoningExpanded = value.isReasoningExpanded;
}
if (typeof value.isThinkingComplete === 'boolean') {
parsed.isThinkingComplete = value.isThinkingComplete;
}
if (typeof value.hasAutoCollapsed === 'boolean') {
parsed.hasAutoCollapsed = value.hasAutoCollapsed;
}
return parsed;
};
const sanitizeMessages = (value: unknown): ChatMessage[] => {
if (!Array.isArray(value)) return [];
return value
.map((item, index) => parseMessage(item, index))
.filter((item): item is ChatMessage => item !== null);
};
const parseInputs = (value: unknown, fallbackModel = ''): ModelTesterInputs => {
if (!isRecord(value)) {
return {
...DEFAULT_INPUTS,
model: fallbackModel,
};
}
const model = typeof value.model === 'string' && value.model.trim().length > 0
? value.model
: fallbackModel;
return {
model,
targetFormat: value.targetFormat === 'claude' ? 'claude' : DEFAULT_INPUTS.targetFormat,
temperature: toFiniteNumber(value.temperature, DEFAULT_INPUTS.temperature),
top_p: toFiniteNumber(value.top_p, DEFAULT_INPUTS.top_p),
max_tokens: toFiniteNumber(value.max_tokens, DEFAULT_INPUTS.max_tokens),
frequency_penalty: toFiniteNumber(value.frequency_penalty, DEFAULT_INPUTS.frequency_penalty),
presence_penalty: toFiniteNumber(value.presence_penalty, DEFAULT_INPUTS.presence_penalty),
seed: toNullableFiniteNumber(value.seed),
stream: toBoolean(value.stream, DEFAULT_INPUTS.stream),
};
};
const parseParameterEnabled = (value: unknown): ParameterEnabled => {
if (!isRecord(value)) {
return { ...DEFAULT_PARAMETER_ENABLED };
}
return {
temperature: toBoolean(value.temperature, DEFAULT_PARAMETER_ENABLED.temperature),
top_p: toBoolean(value.top_p, DEFAULT_PARAMETER_ENABLED.top_p),
max_tokens: toBoolean(value.max_tokens, DEFAULT_PARAMETER_ENABLED.max_tokens),
frequency_penalty: toBoolean(value.frequency_penalty, DEFAULT_PARAMETER_ENABLED.frequency_penalty),
presence_penalty: toBoolean(value.presence_penalty, DEFAULT_PARAMETER_ENABLED.presence_penalty),
seed: toBoolean(value.seed, DEFAULT_PARAMETER_ENABLED.seed),
};
};
const parsePendingPayload = (value: unknown): TestChatPayload | null => {
if (!isRecord(value)) return null;
if (typeof value.model !== 'string' || value.model.trim().length === 0) return null;
const payloadMessages = sanitizeMessages(value.messages).map((message) => ({
role: message.role,
content: message.content,
}));
if (payloadMessages.length === 0) return null;
const payload: TestChatPayload = {
model: value.model,
messages: payloadMessages,
};
if (value.targetFormat === 'claude' || value.targetFormat === 'openai') {
payload.targetFormat = value.targetFormat;
}
if (typeof value.stream === 'boolean') payload.stream = value.stream;
if (typeof value.temperature === 'number' && Number.isFinite(value.temperature)) payload.temperature = value.temperature;
if (typeof value.top_p === 'number' && Number.isFinite(value.top_p)) payload.top_p = value.top_p;
if (typeof value.max_tokens === 'number' && Number.isFinite(value.max_tokens)) payload.max_tokens = value.max_tokens;
if (typeof value.frequency_penalty === 'number' && Number.isFinite(value.frequency_penalty)) payload.frequency_penalty = value.frequency_penalty;
if (typeof value.presence_penalty === 'number' && Number.isFinite(value.presence_penalty)) payload.presence_penalty = value.presence_penalty;
if (typeof value.seed === 'number' && Number.isFinite(value.seed)) payload.seed = value.seed;
return payload;
};
export const serializeModelTesterSession = (state: ModelTesterSessionState): string =>
JSON.stringify(state);
export const processThinkTags = (content: string, reasoningContent = ''): { content: string; reasoningContent: string } => {
if (!content || !content.includes('<think>')) {
return { content, reasoningContent };
}
const thoughts: string[] = [];
const replyParts: string[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
THINK_TAG_REGEX.lastIndex = 0;
while ((match = THINK_TAG_REGEX.exec(content)) !== null) {
replyParts.push(content.substring(lastIndex, match.index));
thoughts.push(match[1]);
lastIndex = match.index + match[0].length;
}
replyParts.push(content.substring(lastIndex));
const processedContent = replyParts.join('').replace(/<\/?think>/g, '').trim();
const thoughtsCombined = thoughts.join('\n\n---\n\n');
return {
content: processedContent,
reasoningContent: reasoningContent && thoughtsCombined
? `${reasoningContent}\n\n---\n\n${thoughtsCombined}`
: (reasoningContent || thoughtsCombined),
};
};
const processIncompleteThinkTags = (content: string, reasoningContent = ''): { content: string; reasoningContent: string } => {
if (!content) return { content: '', reasoningContent };
const lastOpenThinkIndex = content.lastIndexOf('<think>');
if (lastOpenThinkIndex === -1) {
return processThinkTags(content, reasoningContent);
}
const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex);
if (!fragmentAfterLastOpen.includes('</think>')) {
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
const cleanContent = content.substring(0, lastOpenThinkIndex);
const mergedReasoning = unclosedThought
? (reasoningContent ? `${reasoningContent}\n\n---\n\n${unclosedThought}` : unclosedThought)
: reasoningContent;
return processThinkTags(cleanContent, mergedReasoning);
}
return processThinkTags(content, reasoningContent);
};
export const finalizeIncompleteMessage = (message: ChatMessage): ChatMessage => {
if (message.status !== MESSAGE_STATUS.LOADING && message.status !== MESSAGE_STATUS.INCOMPLETE) {
return message;
}
const processed = processIncompleteThinkTags(message.content || '', message.reasoningContent || '');
return {
...message,
content: processed.content || message.content,
reasoningContent: processed.reasoningContent || null,
status: MESSAGE_STATUS.COMPLETE,
isThinkingComplete: true,
};
};
const buildFallbackInputsFromLegacy = (value: Record<string, unknown>): ModelTesterInputs => {
const legacyModel = typeof value.model === 'string' ? value.model : '';
const inputs = parseInputs(value.inputs, legacyModel);
if (!value.inputs) {
inputs.targetFormat = value.targetFormat === 'claude' ? 'claude' : inputs.targetFormat;
inputs.temperature = toFiniteNumber(value.temperature, inputs.temperature);
}
return inputs;
};
export const parseModelTesterSession = (raw: string | null): ModelTesterSessionState | null => {
if (typeof raw !== 'string' || raw.trim().length === 0) return null;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return null;
}
if (!isRecord(parsed)) return null;
const inputs = buildFallbackInputsFromLegacy(parsed);
if (!inputs.model) return null;
const state: ModelTesterSessionState = {
input: typeof parsed.input === 'string' ? parsed.input : '',
inputs,
parameterEnabled: parseParameterEnabled(parsed.parameterEnabled),
messages: sanitizeMessages(parsed.messages),
pendingPayload: parsePendingPayload(parsed.pendingPayload),
customRequestMode: toBoolean(parsed.customRequestMode, false),
customRequestBody: typeof parsed.customRequestBody === 'string' ? parsed.customRequestBody : '',
showDebugPanel: toBoolean(parsed.showDebugPanel, false),
activeDebugTab: typeof parsed.activeDebugTab === 'string' && VALID_DEBUG_TABS.has(parsed.activeDebugTab)
? parsed.activeDebugTab as DebugTab
: DEBUG_TABS.PREVIEW,
};
if (typeof parsed.pendingJobId === 'string' && parsed.pendingJobId.trim().length > 0) {
state.pendingJobId = parsed.pendingJobId;
} else if (parsed.pendingJobId === null) {
state.pendingJobId = null;
}
if (!state.pendingJobId && state.messages.length > 0) {
state.messages = state.messages.map((message) => finalizeIncompleteMessage(message));
}
return state;
};
export const toApiMessages = (messages: ChatMessage[]): ApiChatMessage[] =>
messages
.filter((message) => {
if (message.role !== 'assistant') return true;
return message.status !== MESSAGE_STATUS.LOADING && message.status !== MESSAGE_STATUS.INCOMPLETE;
})
.map((message) => ({
role: message.role,
content: message.content,
}));
export const buildApiPayload = (
messages: ChatMessage[],
inputs: ModelTesterInputs,
parameterEnabled: ParameterEnabled,
): TestChatPayload => {
const payload: TestChatPayload = {
model: inputs.model,
messages: toApiMessages(messages),
targetFormat: inputs.targetFormat,
stream: inputs.stream,
};
const mapping: Array<{ key: keyof ParameterEnabled; field: keyof ModelTesterInputs }> = [
{ key: 'temperature', field: 'temperature' },
{ key: 'top_p', field: 'top_p' },
{ key: 'max_tokens', field: 'max_tokens' },
{ key: 'frequency_penalty', field: 'frequency_penalty' },
{ key: 'presence_penalty', field: 'presence_penalty' },
{ key: 'seed', field: 'seed' },
];
for (const item of mapping) {
const enabled = parameterEnabled[item.key];
if (!enabled) continue;
const value = inputs[item.field];
if (item.field === 'seed') {
if (typeof value === 'number' && Number.isFinite(value)) {
payload.seed = value;
}
continue;
}
if (typeof value === 'number' && Number.isFinite(value)) {
(payload as any)[item.field] = value;
}
}
return payload;
};
export const parseCustomRequestBody = (raw: string): TestChatPayload | null => {
if (!raw.trim()) return null;
try {
const parsed = JSON.parse(raw);
return parsePendingPayload(parsed);
} catch {
return null;
}
};
export const syncMessagesToCustomRequestBody = (
currentBody: string,
messages: ChatMessage[],
inputs: ModelTesterInputs,
): string => {
let payload: Record<string, unknown>;
try {
payload = JSON.parse(currentBody || '{}');
} catch {
payload = {};
}
payload.model = typeof payload.model === 'string' && payload.model.trim().length > 0
? payload.model
: inputs.model;
payload.targetFormat = payload.targetFormat === 'claude' ? 'claude' : inputs.targetFormat;
payload.stream = payload.stream !== undefined ? payload.stream : inputs.stream;
payload.messages = toApiMessages(messages);
return JSON.stringify(payload, null, 2);
};
export const syncCustomRequestBodyToMessages = (raw: string): ChatMessage[] | null => {
const parsed = parseCustomRequestBody(raw);
if (!parsed?.messages || parsed.messages.length === 0) return null;
return parsed.messages.map((item, index) => createMessage(item.role, item.content, {
id: `custom-${index}-${Date.now()}`,
}));
};
export const findLastLoadingAssistantIndex = (messages: ChatMessage[]): number => {
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (
message.role === 'assistant' &&
(message.status === MESSAGE_STATUS.LOADING || message.status === MESSAGE_STATUS.INCOMPLETE)
) {
return index;
}
}
return -1;
};
export const countConversationTurns = (messages: ChatMessage[]): number =>
messages.reduce((turns, message) => turns + (message.role === 'user' ? 1 : 0), 0);
@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest';
import {
mergeMarketplaceMetadata,
shouldHydrateMarketplaceMetadata,
type MarketplaceModelMetadataShape,
} from './modelsMarketplaceMetadata.js';
interface TestModel extends MarketplaceModelMetadataShape {
name: string;
accountCount: number;
}
function createModel(overrides: Partial<TestModel>): TestModel {
return {
name: 'gpt-4o',
accountCount: 1,
description: null,
tags: [],
supportedEndpointTypes: [],
pricingSources: [],
...overrides,
};
}
describe('modelsMarketplaceMetadata', () => {
it('identifies when metadata hydration is needed', () => {
const models = [
createModel({ name: 'gpt-4o', description: 'base model' }),
createModel({ name: 'claude-sonnet-4' }),
];
expect(shouldHydrateMarketplaceMetadata(models)).toBe(true);
});
it('identifies when metadata is already complete', () => {
const models = [
createModel({
name: 'gpt-4o',
description: 'base model',
tags: ['chat'],
supportedEndpointTypes: ['chat'],
pricingSources: [{ source: 'site-a' }],
}),
createModel({
name: 'claude-sonnet-4',
description: 'anthropic model',
tags: ['reasoning'],
supportedEndpointTypes: ['chat'],
pricingSources: [{ source: 'site-b' }],
}),
];
expect(shouldHydrateMarketplaceMetadata(models)).toBe(false);
});
it('merges metadata into current models without overriding non-metadata fields', () => {
const base = [
createModel({ name: 'GPT-4O', accountCount: 3 }),
createModel({ name: 'claude-sonnet-4', accountCount: 5 }),
];
const detail = [
createModel({
name: 'gpt-4o',
accountCount: 99,
description: 'OpenAI flagship model',
tags: ['openai'],
supportedEndpointTypes: ['chat', 'responses'],
pricingSources: [{ source: 'site-a' }],
}),
];
const merged = mergeMarketplaceMetadata(base, detail);
expect(merged).toEqual([
{
...base[0],
description: 'OpenAI flagship model',
tags: ['openai'],
supportedEndpointTypes: ['chat', 'responses'],
pricingSources: [{ source: 'site-a' }],
},
base[1],
]);
});
});
@@ -0,0 +1,46 @@
export interface MarketplaceModelMetadataShape {
description: string | null;
tags: string[];
supportedEndpointTypes: string[];
pricingSources: unknown[];
}
interface MarketplaceModelLike extends MarketplaceModelMetadataShape {
name: string;
}
function normalizeModelName(name: string): string {
return name.trim().toLowerCase();
}
function hasMarketplaceMetadata(model: MarketplaceModelMetadataShape): boolean {
if (typeof model.description === 'string' && model.description.trim().length > 0) return true;
if (Array.isArray(model.tags) && model.tags.length > 0) return true;
if (Array.isArray(model.supportedEndpointTypes) && model.supportedEndpointTypes.length > 0) return true;
if (Array.isArray(model.pricingSources) && model.pricingSources.length > 0) return true;
return false;
}
export function shouldHydrateMarketplaceMetadata<T extends MarketplaceModelMetadataShape>(models: T[]): boolean {
return models.some((model) => !hasMarketplaceMetadata(model));
}
export function mergeMarketplaceMetadata<T extends MarketplaceModelLike>(baseModels: T[], detailedModels: T[]): T[] {
const detailedByName = new Map<string, T>();
for (const model of detailedModels) {
detailedByName.set(normalizeModelName(model.name), model);
}
return baseModels.map((model) => {
const detail = detailedByName.get(normalizeModelName(model.name));
if (!detail) return model;
return {
...model,
description: detail.description ?? null,
tags: Array.isArray(detail.tags) ? detail.tags : [],
supportedEndpointTypes: Array.isArray(detail.supportedEndpointTypes) ? detail.supportedEndpointTypes : [],
pricingSources: Array.isArray(detail.pricingSources) ? detail.pricingSources : [],
};
});
}
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import {
ROUTING_PROFILE_PRESETS,
applyRoutingProfilePreset,
resolveRoutingProfilePreset,
} from './routingProfiles.js';
describe('routingProfiles', () => {
it('applies stable preset weights', () => {
const stable = applyRoutingProfilePreset('stable');
expect(stable).toEqual(ROUTING_PROFILE_PRESETS.stable);
});
it('resolves known profile by exact weights', () => {
const preset = resolveRoutingProfilePreset(ROUTING_PROFILE_PRESETS.cost);
expect(preset).toBe('cost');
});
it('returns custom when weights do not match presets', () => {
const preset = resolveRoutingProfilePreset({
baseWeightFactor: 0.61,
valueScoreFactor: 0.39,
costWeight: 0.22,
balanceWeight: 0.41,
usageWeight: 0.37,
});
expect(preset).toBe('custom');
});
});
+58
View File
@@ -0,0 +1,58 @@
export type RoutingWeights = {
baseWeightFactor: number;
valueScoreFactor: number;
costWeight: number;
balanceWeight: number;
usageWeight: number;
};
type RoutingProfilePresetId = 'balanced' | 'stable' | 'cost' | 'custom';
export const ROUTING_PROFILE_PRESETS: Record<Exclude<RoutingProfilePresetId, 'custom'>, RoutingWeights> = {
balanced: {
baseWeightFactor: 0.5,
valueScoreFactor: 0.5,
costWeight: 0.4,
balanceWeight: 0.3,
usageWeight: 0.3,
},
stable: {
baseWeightFactor: 0.7,
valueScoreFactor: 0.3,
costWeight: 0.2,
balanceWeight: 0.6,
usageWeight: 0.2,
},
cost: {
baseWeightFactor: 0.35,
valueScoreFactor: 0.65,
costWeight: 0.75,
balanceWeight: 0.15,
usageWeight: 0.1,
},
};
const EPSILON = 1e-6;
export function applyRoutingProfilePreset(presetId: Exclude<RoutingProfilePresetId, 'custom'>): RoutingWeights {
return {
...ROUTING_PROFILE_PRESETS[presetId],
};
}
function weightsAlmostEqual(a: RoutingWeights, b: RoutingWeights): boolean {
return (
Math.abs(a.baseWeightFactor - b.baseWeightFactor) <= EPSILON
&& Math.abs(a.valueScoreFactor - b.valueScoreFactor) <= EPSILON
&& Math.abs(a.costWeight - b.costWeight) <= EPSILON
&& Math.abs(a.balanceWeight - b.balanceWeight) <= EPSILON
&& Math.abs(a.usageWeight - b.usageWeight) <= EPSILON
);
}
export function resolveRoutingProfilePreset(weights: RoutingWeights): RoutingProfilePresetId {
if (weightsAlmostEqual(weights, ROUTING_PROFILE_PRESETS.balanced)) return 'balanced';
if (weightsAlmostEqual(weights, ROUTING_PROFILE_PRESETS.stable)) return 'stable';
if (weightsAlmostEqual(weights, ROUTING_PROFILE_PRESETS.cost)) return 'cost';
return 'custom';
}
+48
View File
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { buildSiteSaveAction } from './sitesEditor.js';
describe('buildSiteSaveAction', () => {
it('returns add action in add mode', () => {
const action = buildSiteSaveAction(
{ mode: 'add' },
{ name: 'site-a', url: 'https://a.example.com/', platform: 'new-api', apiKey: 'sk-1' },
);
expect(action).toEqual({
kind: 'add',
payload: {
name: 'site-a',
url: 'https://a.example.com/',
platform: 'new-api',
apiKey: 'sk-1',
},
});
});
it('returns update action in edit mode with site id', () => {
const action = buildSiteSaveAction(
{ mode: 'edit', editingSiteId: 12 },
{ name: 'site-b', url: 'https://b.example.com', platform: 'one-api', apiKey: '' },
);
expect(action).toEqual({
kind: 'update',
id: 12,
payload: {
name: 'site-b',
url: 'https://b.example.com',
platform: 'one-api',
apiKey: '',
},
});
});
it('throws when edit mode has no site id', () => {
expect(() =>
buildSiteSaveAction(
{ mode: 'edit' },
{ name: 'site-c', url: 'https://c.example.com', platform: '', apiKey: '' },
),
).toThrow('editingSiteId is required in edit mode');
});
});
+37
View File
@@ -0,0 +1,37 @@
export type SiteForm = {
name: string;
url: string;
platform: string;
apiKey: string;
};
export type SiteEditorState =
| { mode: 'add' }
| { mode: 'edit'; editingSiteId: number };
type SiteSaveAction =
| { kind: 'add'; payload: SiteForm }
| { kind: 'update'; id: number; payload: SiteForm };
export function emptySiteForm(): SiteForm {
return { name: '', url: '', platform: '', apiKey: '' };
}
export function siteFormFromSite(site: Partial<SiteForm>): SiteForm {
return {
name: site.name ?? '',
url: site.url ?? '',
platform: site.platform ?? '',
apiKey: site.apiKey ?? '',
};
}
export function buildSiteSaveAction(editor: SiteEditorState, form: SiteForm): SiteSaveAction {
if (editor.mode === 'edit') {
if (!Number.isFinite(editor.editingSiteId)) {
throw new Error('editingSiteId is required in edit mode');
}
return { kind: 'update', id: editor.editingSiteId, payload: form };
}
return { kind: 'add', payload: form };
}