mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-18 19:41:31 +00:00
refactor(providers): centralize message handling in provider module
Move provider-specific normalizeMessage and fetchHistory logic out of the legacy server/providers adapters and into the refactored provider classes so callers can depend on the main provider contract instead of parallel adapter plumbing. Add a providers service to resolve concrete providers through the registry and delegate message normalization/history loading from realtime handlers and the unified messages route. Add shared TypeScript message/history types and normalized message helpers so provider implementations and callers use the same contract. Remove the old adapter registry/files now that Claude, Codex, Cursor, and Gemini implement the required behavior directly.
This commit is contained in:
@@ -1,5 +1,31 @@
|
||||
import { getCodexSessionMessages } from '@/projects.js';
|
||||
import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
|
||||
import { CodexMcpProvider } from '@/modules/providers/list/codex/codex-mcp.provider.js';
|
||||
import type { FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
||||
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
|
||||
|
||||
const PROVIDER = 'codex';
|
||||
|
||||
type RawProviderMessage = Record<string, any>;
|
||||
|
||||
type CodexHistoryResult =
|
||||
| RawProviderMessage[]
|
||||
| {
|
||||
messages?: RawProviderMessage[];
|
||||
total?: number;
|
||||
hasMore?: boolean;
|
||||
tokenUsage?: unknown;
|
||||
};
|
||||
|
||||
const loadCodexSessionMessages = getCodexSessionMessages as unknown as (
|
||||
sessionId: string,
|
||||
limit: number | null,
|
||||
offset: number,
|
||||
) => Promise<CodexHistoryResult>;
|
||||
|
||||
function readRawProviderMessage(raw: unknown): RawProviderMessage | null {
|
||||
return readObjectRecord(raw) as RawProviderMessage | null;
|
||||
}
|
||||
|
||||
export class CodexProvider extends AbstractProvider {
|
||||
readonly mcp = new CodexMcpProvider();
|
||||
@@ -7,4 +33,300 @@ export class CodexProvider extends AbstractProvider {
|
||||
constructor() {
|
||||
super('codex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a persisted Codex JSONL entry.
|
||||
*
|
||||
* Live Codex SDK events are transformed before they reach normalizeMessage(),
|
||||
* while history entries already use a compact message/tool shape from projects.js.
|
||||
*/
|
||||
private normalizeHistoryEntry(raw: RawProviderMessage, sessionId: string | null): NormalizedMessage[] {
|
||||
const ts = raw.timestamp || new Date().toISOString();
|
||||
const baseId = raw.uuid || generateMessageId('codex');
|
||||
|
||||
if (raw.message?.role === 'user') {
|
||||
const content = typeof raw.message.content === 'string'
|
||||
? raw.message.content
|
||||
: Array.isArray(raw.message.content)
|
||||
? raw.message.content
|
||||
.map((part: string | RawProviderMessage) => typeof part === 'string' ? part : part?.text || '')
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
: String(raw.message.content || '');
|
||||
if (!content.trim()) {
|
||||
return [];
|
||||
}
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'text',
|
||||
role: 'user',
|
||||
content,
|
||||
})];
|
||||
}
|
||||
|
||||
if (raw.message?.role === 'assistant') {
|
||||
const content = typeof raw.message.content === 'string'
|
||||
? raw.message.content
|
||||
: Array.isArray(raw.message.content)
|
||||
? raw.message.content
|
||||
.map((part: string | RawProviderMessage) => typeof part === 'string' ? part : part?.text || '')
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
: '';
|
||||
if (!content.trim()) {
|
||||
return [];
|
||||
}
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'text',
|
||||
role: 'assistant',
|
||||
content,
|
||||
})];
|
||||
}
|
||||
|
||||
if (raw.type === 'thinking' || raw.isReasoning) {
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'thinking',
|
||||
content: raw.message?.content || '',
|
||||
})];
|
||||
}
|
||||
|
||||
if (raw.type === 'tool_use' || raw.toolName) {
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_use',
|
||||
toolName: raw.toolName || 'Unknown',
|
||||
toolInput: raw.toolInput,
|
||||
toolId: raw.toolCallId || baseId,
|
||||
})];
|
||||
}
|
||||
|
||||
if (raw.type === 'tool_result') {
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_result',
|
||||
toolId: raw.toolCallId || '',
|
||||
content: raw.output || '',
|
||||
isError: Boolean(raw.isError),
|
||||
})];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes either a Codex history entry or a transformed live SDK event.
|
||||
*/
|
||||
normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
|
||||
const raw = readRawProviderMessage(rawMessage);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (raw.message?.role) {
|
||||
return this.normalizeHistoryEntry(raw, sessionId);
|
||||
}
|
||||
|
||||
const ts = raw.timestamp || new Date().toISOString();
|
||||
const baseId = raw.uuid || generateMessageId('codex');
|
||||
|
||||
if (raw.type === 'item') {
|
||||
switch (raw.itemType) {
|
||||
case 'agent_message':
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'text',
|
||||
role: 'assistant',
|
||||
content: raw.message?.content || '',
|
||||
})];
|
||||
case 'reasoning':
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'thinking',
|
||||
content: raw.message?.content || '',
|
||||
})];
|
||||
case 'command_execution':
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_use',
|
||||
toolName: 'Bash',
|
||||
toolInput: { command: raw.command },
|
||||
toolId: baseId,
|
||||
output: raw.output,
|
||||
exitCode: raw.exitCode,
|
||||
status: raw.status,
|
||||
})];
|
||||
case 'file_change':
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_use',
|
||||
toolName: 'FileChanges',
|
||||
toolInput: raw.changes,
|
||||
toolId: baseId,
|
||||
status: raw.status,
|
||||
})];
|
||||
case 'mcp_tool_call':
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_use',
|
||||
toolName: raw.tool || 'MCP',
|
||||
toolInput: raw.arguments,
|
||||
toolId: baseId,
|
||||
server: raw.server,
|
||||
result: raw.result,
|
||||
error: raw.error,
|
||||
status: raw.status,
|
||||
})];
|
||||
case 'web_search':
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_use',
|
||||
toolName: 'WebSearch',
|
||||
toolInput: { query: raw.query },
|
||||
toolId: baseId,
|
||||
})];
|
||||
case 'todo_list':
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_use',
|
||||
toolName: 'TodoList',
|
||||
toolInput: { items: raw.items },
|
||||
toolId: baseId,
|
||||
})];
|
||||
case 'error':
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'error',
|
||||
content: raw.message?.content || 'Unknown error',
|
||||
})];
|
||||
default:
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'tool_use',
|
||||
toolName: raw.itemType || 'Unknown',
|
||||
toolInput: raw.item || raw,
|
||||
toolId: baseId,
|
||||
})];
|
||||
}
|
||||
}
|
||||
|
||||
if (raw.type === 'turn_complete') {
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'complete',
|
||||
})];
|
||||
}
|
||||
if (raw.type === 'turn_failed') {
|
||||
return [createNormalizedMessage({
|
||||
id: baseId,
|
||||
sessionId,
|
||||
timestamp: ts,
|
||||
provider: PROVIDER,
|
||||
kind: 'error',
|
||||
content: raw.error?.message || 'Turn failed',
|
||||
})];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Codex JSONL history and keeps token usage metadata when projects.js
|
||||
* provides it.
|
||||
*/
|
||||
async fetchHistory(
|
||||
sessionId: string,
|
||||
options: FetchHistoryOptions = {},
|
||||
): Promise<FetchHistoryResult> {
|
||||
const { limit = null, offset = 0 } = options;
|
||||
|
||||
let result: CodexHistoryResult;
|
||||
try {
|
||||
result = await loadCodexSessionMessages(sessionId, limit, offset);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[CodexProvider] Failed to load session ${sessionId}:`, message);
|
||||
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
||||
}
|
||||
|
||||
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
||||
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
||||
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
||||
const tokenUsage = Array.isArray(result) ? undefined : result.tokenUsage;
|
||||
|
||||
const normalized: NormalizedMessage[] = [];
|
||||
for (const raw of rawMessages) {
|
||||
normalized.push(...this.normalizeHistoryEntry(raw, sessionId));
|
||||
}
|
||||
|
||||
const toolResultMap = new Map<string, NormalizedMessage>();
|
||||
for (const msg of normalized) {
|
||||
if (msg.kind === 'tool_result' && msg.toolId) {
|
||||
toolResultMap.set(msg.toolId, msg);
|
||||
}
|
||||
}
|
||||
for (const msg of normalized) {
|
||||
if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
|
||||
const toolResult = toolResultMap.get(msg.toolId);
|
||||
if (toolResult) {
|
||||
msg.toolResult = { content: toolResult.content, isError: toolResult.isError };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages: normalized,
|
||||
total,
|
||||
hasMore,
|
||||
offset,
|
||||
limit,
|
||||
tokenUsage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user