Add Codex model selection

This commit is contained in:
weishu
2026-04-25 10:48:12 +08:00
parent b712ee67a5
commit 97be34e21c
30 changed files with 644 additions and 58 deletions
+25
View File
@@ -19,6 +19,31 @@ export interface InitializeResponse {
[key: string]: unknown;
}
export interface ModelListParams {
includeHidden?: boolean;
}
export interface ModelListItem {
id: string;
model?: string;
displayName?: string;
description?: string;
hidden?: boolean;
supportedReasoningEfforts?: Array<{
reasoningEffort?: string;
description?: string;
}>;
defaultReasoningEffort?: string | null;
isDefault?: boolean;
[key: string]: unknown;
}
export interface ModelListResponse {
data?: ModelListItem[];
nextCursor?: string | null;
[key: string]: unknown;
}
export interface ThreadStartParams {
model?: string;
modelProvider?: string;
+9
View File
@@ -4,6 +4,8 @@ import { killProcessByChildProcess } from '@/utils/process';
import type {
InitializeParams,
InitializeResponse,
ModelListParams,
ModelListResponse,
ThreadStartParams,
ThreadStartResponse,
ThreadResumeParams,
@@ -133,6 +135,13 @@ export class CodexAppServerClient {
return response as InitializeResponse;
}
async listModels(params?: ModelListParams): Promise<ModelListResponse> {
const response = await this.sendRequest('model/list', params ?? {}, {
timeoutMs: 30_000
});
return response as ModelListResponse;
}
async startThread(params: ThreadStartParams, options?: { signal?: AbortSignal }): Promise<ThreadStartResponse> {
const response = await this.sendRequest('thread/start', params, {
signal: options?.signal,
+37 -18
View File
@@ -71,21 +71,15 @@ export async function runCodex(opts: {
lifecycle.registerProcessHandlers();
registerKillSessionHandler(session.rpcHandlerManager, lifecycle.cleanupAndExit);
const syncSessionMode = () => {
const applyCurrentConfigToSession = (options?: { syncModel?: boolean }) => {
const sessionInstance = sessionWrapperRef.current;
if (!sessionInstance) {
return;
}
const sessionModel = sessionInstance.getModel();
if (sessionModel !== undefined) {
currentModel = sessionModel ?? undefined;
}
const sessionModelReasoningEffort = sessionInstance.getModelReasoningEffort();
if (sessionModelReasoningEffort !== undefined) {
currentModelReasoningEffort = (sessionModelReasoningEffort ?? undefined) as ReasoningEffort | undefined;
}
sessionInstance.setPermissionMode(currentPermissionMode);
sessionInstance.setModel(currentModel ?? null);
if (options?.syncModel !== false) {
sessionInstance.setModel(currentModel ?? null);
}
sessionInstance.setModelReasoningEffort(currentModelReasoningEffort ?? null);
sessionInstance.setCollaborationMode(currentCollaborationMode);
logger.debug(
@@ -167,16 +161,32 @@ export async function runCodex(opts: {
return value as ReasoningEffort;
};
const resolveModel = (value: unknown): string => {
if (typeof value !== 'string') {
throw new Error('Invalid model');
}
const trimmedValue = value.trim();
if (!trimmedValue) {
throw new Error('Invalid model');
}
return trimmedValue;
};
session.rpcHandlerManager.registerHandler('set-session-config', async (payload: unknown) => {
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid session config payload');
}
const config = payload as { permissionMode?: unknown; modelReasoningEffort?: unknown; collaborationMode?: unknown };
const config = payload as { permissionMode?: unknown; model?: unknown; modelReasoningEffort?: unknown; collaborationMode?: unknown };
if (config.permissionMode !== undefined) {
currentPermissionMode = resolvePermissionMode(config.permissionMode);
}
const shouldSyncModel = config.model !== undefined;
if (shouldSyncModel) {
currentModel = resolveModel(config.model);
}
if (config.modelReasoningEffort !== undefined) {
currentModelReasoningEffort = resolveModelReasoningEffort(config.modelReasoningEffort);
}
@@ -185,13 +195,22 @@ export async function runCodex(opts: {
currentCollaborationMode = resolveCollaborationMode(config.collaborationMode);
}
syncSessionMode();
applyCurrentConfigToSession({ syncModel: shouldSyncModel });
const applied: {
permissionMode: PermissionMode;
model?: string | null;
modelReasoningEffort: ReasoningEffort | null;
collaborationMode: EnhancedMode['collaborationMode'];
} = {
permissionMode: currentPermissionMode,
modelReasoningEffort: currentModelReasoningEffort ?? null,
collaborationMode: currentCollaborationMode
};
if (shouldSyncModel) {
applied.model = currentModel ?? null;
}
return {
applied: {
permissionMode: currentPermissionMode,
modelReasoningEffort: currentModelReasoningEffort ?? null,
collaborationMode: currentCollaborationMode
}
applied
};
});
@@ -213,7 +232,7 @@ export async function runCodex(opts: {
onModeChange: createModeChangeHandler(session),
onSessionReady: (instance) => {
sessionWrapperRef.current = instance;
syncSessionMode();
applyCurrentConfigToSession();
}
});
} catch (error) {
+90
View File
@@ -0,0 +1,90 @@
import { CodexAppServerClient } from '@/codex/codexAppServerClient';
import { getErrorMessage } from './rpcResponses';
export interface CodexModelSummary {
id: string;
displayName: string;
isDefault: boolean;
defaultReasoningEffort?: string | null;
supportedReasoningEfforts?: string[];
}
export interface ListCodexModelsRequest {
includeHidden?: boolean;
}
export interface ListCodexModelsResponse {
success: boolean;
models?: CodexModelSummary[];
error?: string;
}
function asNonEmptyString(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
}
function normalizeSupportedReasoningEfforts(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const efforts = value
.map((entry) => {
if (!entry || typeof entry !== 'object') {
return null;
}
const reasoningEffort = asNonEmptyString((entry as { reasoningEffort?: unknown }).reasoningEffort);
return reasoningEffort;
})
.filter((entry): entry is string => entry !== null);
return efforts.length > 0 ? efforts : undefined;
}
function normalizeModel(entry: unknown): CodexModelSummary | null {
if (!entry || typeof entry !== 'object') {
return null;
}
const record = entry as Record<string, unknown>;
const id = asNonEmptyString(record.id) ?? asNonEmptyString(record.model);
if (!id) {
return null;
}
return {
id,
displayName: asNonEmptyString(record.displayName) ?? id,
isDefault: record.isDefault === true,
defaultReasoningEffort: asNonEmptyString(record.defaultReasoningEffort),
supportedReasoningEfforts: normalizeSupportedReasoningEfforts(record.supportedReasoningEfforts)
};
}
export async function listCodexModels(includeHidden: boolean = false): Promise<CodexModelSummary[]> {
const client = new CodexAppServerClient();
try {
await client.connect();
await client.initialize({
clientInfo: {
name: 'hapi-codex-models',
version: '1.0.0'
},
capabilities: {
experimentalApi: true
}
});
const response = await client.listModels({ includeHidden });
const models = Array.isArray(response.data)
? response.data.map(normalizeModel).filter((model): model is CodexModelSummary => model !== null)
: [];
return models;
} catch (error) {
throw new Error(getErrorMessage(error, 'Failed to list Codex models'));
} finally {
await client.disconnect().catch(() => undefined);
}
}
@@ -0,0 +1,22 @@
import { logger } from '@/ui/logger';
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager';
import {
listCodexModels,
type ListCodexModelsRequest,
type ListCodexModelsResponse
} from '../codexModels';
import { getErrorMessage, rpcError } from '../rpcResponses';
export function registerCodexModelHandlers(rpcHandlerManager: RpcHandlerManager): void {
rpcHandlerManager.registerHandler<ListCodexModelsRequest, ListCodexModelsResponse>('listCodexModels', async (data) => {
logger.debug('List Codex models request');
try {
const models = await listCodexModels(data?.includeHidden === true);
return { success: true, models };
} catch (error) {
logger.debug('Failed to list Codex models:', error);
return rpcError(getErrorMessage(error, 'Failed to list Codex models'));
}
});
}
@@ -1,5 +1,6 @@
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
import { registerBashHandlers } from './handlers/bash'
import { registerCodexModelHandlers } from './handlers/codexModels'
import { registerDirectoryHandlers } from './handlers/directories'
import { registerDifftasticHandlers } from './handlers/difftastic'
import { registerFileHandlers } from './handlers/files'
@@ -11,6 +12,7 @@ import { registerUploadHandlers } from './handlers/uploads'
export function registerCommonHandlers(rpcHandlerManager: RpcHandlerManager, workingDirectory: string): void {
registerBashHandlers(rpcHandlerManager, workingDirectory)
registerCodexModelHandlers(rpcHandlerManager)
registerFileHandlers(rpcHandlerManager, workingDirectory)
registerDirectoryHandlers(rpcHandlerManager, workingDirectory)
registerRipgrepHandlers(rpcHandlerManager, workingDirectory)