Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/mindos/src/agent/runtime/codex-app-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ function createFakeCodexTransport(): CodexAppServerTransport & { sent: unknown[]
if (record.method === 'thread/resume') {
queue.push({ id: record.id!, result: { thread: { id: record.params?.threadId } } });
}
if (record.method === 'model/list') {
queue.push({
id: record.id!,
result: {
data: [{
id: 'gpt-5.6-sol',
model: 'gpt-5.6-sol',
displayName: 'GPT-5.6 Sol',
description: 'Fast coding model',
hidden: false,
isDefault: true,
supportedReasoningEfforts: [
{ reasoningEffort: 'low', description: 'Fastest' },
{ reasoningEffort: 'ultra', description: 'Maximum reasoning with delegation' },
],
defaultReasoningEffort: 'low',
}],
nextCursor: null,
},
});
}
if (record.method === 'thread/list') {
queue.push({
id: record.id!,
Expand Down Expand Up @@ -276,6 +297,36 @@ describe('agent runtime adapters: Codex app-server', () => {
});
});

it('lists model-advertised reasoning efforts without reducing them to a fixed enum', async () => {
const transport = createFakeCodexTransport();
const client = createCodexAppServerClient(transport);

await client.initialize();
const result = await client.listModels({ includeHidden: true, limit: 100 });

expect(result).toEqual({
data: [{
id: 'gpt-5.6-sol',
model: 'gpt-5.6-sol',
displayName: 'GPT-5.6 Sol',
description: 'Fast coding model',
hidden: false,
isDefault: true,
supportedReasoningEfforts: [
{ reasoningEffort: 'low', description: 'Fastest' },
{ reasoningEffort: 'ultra', description: 'Maximum reasoning with delegation' },
],
defaultReasoningEffort: 'low',
}],
nextCursor: null,
});
expect(transport.sent).toContainEqual({
method: 'model/list',
id: 2,
params: { includeHidden: true, limit: 100 },
});
});

it('lists and reads Codex threads without starting a turn', async () => {
const transport = createFakeCodexTransport();
const client = createCodexAppServerClient(transport);
Expand Down
94 changes: 94 additions & 0 deletions packages/mindos/src/agent/runtime/codex-app-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,33 @@ export type CodexThreadForkResult = Record<string, unknown> & {
thread: CodexThread;
};

export type CodexReasoningEffortOption = {
reasoningEffort: string;
description: string;
};

export type CodexModel = {
id: string;
model: string;
displayName: string;
description: string;
hidden: boolean;
isDefault: boolean;
supportedReasoningEfforts: CodexReasoningEffortOption[];
defaultReasoningEffort: string;
};

export type CodexModelListInput = {
cursor?: string | null;
limit?: number | null;
includeHidden?: boolean | null;
};

export type CodexModelListResult = {
data: CodexModel[];
nextCursor: string | null;
};

export type CodexAppServerRequestOptions = {
signal?: AbortSignal;
};
Expand All @@ -143,6 +170,7 @@ export type CodexAppServerClient = {
initialize(options?: CodexAppServerRequestOptions): Promise<void>;
startThread(input?: { model?: string; cwd?: string }, options?: CodexAppServerRequestOptions): Promise<{ threadId: string }>;
resumeThread(input: { threadId: string }, options?: CodexAppServerRequestOptions): Promise<{ threadId: string }>;
listModels(input?: CodexModelListInput): Promise<CodexModelListResult>;
listThreads(input?: CodexThreadListInput): Promise<CodexThreadListResult>;
readThread(input: { threadId: string; includeTurns?: boolean }): Promise<CodexThreadReadResult>;
forkThread(input: CodexThreadForkInput): Promise<CodexThreadForkResult>;
Expand Down Expand Up @@ -391,6 +419,10 @@ export function createCodexAppServerClient(
const result = await request('thread/resume', { threadId: input.threadId }, options.signal);
return { threadId: getThreadId(result, 'thread/resume') ?? input.threadId };
},
async listModels(input = {}) {
const result = await request('model/list', pruneUndefined(input as Record<string, unknown>));
return getModelListResult(result, 'model/list');
},
async listThreads(input = {}) {
const result = await request('thread/list', pruneUndefined(input as Record<string, unknown>));
return getThreadListResult(result, 'thread/list');
Expand Down Expand Up @@ -742,6 +774,68 @@ function getThread(result: unknown, method: string): CodexThread {
return { ...thread, id } as CodexThread;
}

function getModelListResult(result: unknown, method: string): CodexModelListResult {
const record = asRecord(result);
if (!record || !Array.isArray(record.data)) {
throw new Error(`Codex app-server ${method} did not return a model list`);
}
return {
data: record.data.map((item, index) => getModel(item, method, index)),
nextCursor: typeof record.nextCursor === 'string' ? record.nextCursor : null,
};
}

function getModel(value: unknown, method: string, index: number): CodexModel {
const model = asRecord(value);
if (!model || Array.isArray(value)) {
throw new Error(`Codex app-server ${method} returned an invalid model at index ${index}`);
}
const supportedReasoningEfforts = model.supportedReasoningEfforts;
if (!Array.isArray(supportedReasoningEfforts)) {
throw new Error(`Codex app-server ${method} returned a model without supported reasoning efforts`);
}
return {
id: requiredModelString(model, 'id', method),
model: requiredModelString(model, 'model', method),
displayName: requiredModelString(model, 'displayName', method),
description: requiredModelString(model, 'description', method, true),
hidden: requiredModelBoolean(model, 'hidden', method),
isDefault: requiredModelBoolean(model, 'isDefault', method),
supportedReasoningEfforts: supportedReasoningEfforts.map((option) => {
const effort = asRecord(option);
if (!effort || Array.isArray(option)) {
throw new Error(`Codex app-server ${method} returned an invalid reasoning effort option`);
}
return {
reasoningEffort: requiredModelString(effort, 'reasoningEffort', method),
description: requiredModelString(effort, 'description', method, true),
};
}),
defaultReasoningEffort: requiredModelString(model, 'defaultReasoningEffort', method),
};
}

function requiredModelString(
record: Record<string, unknown>,
key: string,
method: string,
allowEmpty = false,
): string {
const value = record[key];
if (typeof value !== 'string' || (!allowEmpty && !value.trim())) {
throw new Error(`Codex app-server ${method} returned a model with an invalid ${key}`);
}
return value;
}

function requiredModelBoolean(record: Record<string, unknown>, key: string, method: string): boolean {
const value = record[key];
if (typeof value !== 'boolean') {
throw new Error(`Codex app-server ${method} returned a model with an invalid ${key}`);
}
return value;
}

function getThreadListResult(result: unknown, method: string): CodexThreadListResult {
const record = asRecord(result);
if (!record || !Array.isArray(record.data)) {
Expand Down
11 changes: 9 additions & 2 deletions packages/mindos/src/agent/runtime/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export type MindosNativeAgentTurnOptions = {
selectedSkills?: MindosSelectedSkill[];
permissionMode?: MindosPermissionMode;
modelOverride?: string;
reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh';
reasoningEffort?: string;
timeoutMs?: number;
runtimeEnv?: NodeJS.ProcessEnv;
signal?: AbortSignal;
Expand Down Expand Up @@ -412,14 +412,17 @@ async function runClaudeTurnWithClient(
materialized.attachments,
{ includeImages: true },
);
const reasoningEffort = isClaudeReasoningEffort(options.reasoningEffort)
? options.reasoningEffort
: undefined;
const turnEvents = resolvedClient.client.startTurn({
prompt,
cwd: options.cwd,
attachments: materialized.attachments,
selectedSkills: options.selectedSkills,
...(sessionId ? { sessionId } : {}),
...(options.modelOverride ? { model: options.modelOverride } : {}),
...(options.reasoningEffort ? { effort: options.reasoningEffort } : {}),
...(reasoningEffort ? { effort: reasoningEffort } : {}),
permissionMode: claudeCliPermissionModeForMindosMode(options.permissionMode),
...(permissionPrompt ? { permissionPrompt } : {}),
signal: options.signal,
Expand All @@ -445,6 +448,10 @@ async function runClaudeTurnWithClient(
}
}

function isClaudeReasoningEffort(value: string | undefined): value is 'low' | 'medium' | 'high' | 'xhigh' {
return value === 'low' || value === 'medium' || value === 'high' || value === 'xhigh';
}

function shouldFallbackFromClaudeSdkTurnError(error: Error, signal?: AbortSignal): boolean {
if (signal?.aborted) return false;
return isClaudeCodeSdkNativeBinaryError(error);
Expand Down
2 changes: 1 addition & 1 deletion packages/mindos/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export type MindosAgentTurnRequest = {
runtimeBinding?: MindosRuntimeSessionBinding | null;
selectedAcpAgent?: { id: string; name: string } | null;
runtimeOptions?: {
reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh';
reasoningEffort?: string;
modelOverride?: string;
};
providerOverride?: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/mindos/src/server.runtime-web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,7 @@ describe('MindOS server contract: runtime, agent turn stream, static web', () =>
},
permissionMode: 'read',
runtimeOptions: {
reasoningEffort: 'high',
reasoningEffort: 'ultra',
modelOverride: 'gpt-test',
},
}, {
Expand All @@ -1250,7 +1250,7 @@ describe('MindOS server contract: runtime, agent turn stream, static web', () =>
expect(events).toEqual([
{ type: 'status', message: 'context=session-from-path;message=hello from turn;skill=research' },
{ type: 'status', message: 'runtime=codex:codex;cwd=/repo/app;space=Research' },
{ type: 'status', message: 'permission=read;effort=high;model=gpt-test' },
{ type: 'status', message: 'permission=read;effort=ultra;model=gpt-test' },
{ type: 'done' },
]);
});
Expand Down
40 changes: 39 additions & 1 deletion packages/mindos/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ describe('MindOS server contract: core, files, HTTP', () => {
path: '/api/agent-runtimes/readiness',
auth: 'required',
});
expect(contract.routes).toContainEqual({
id: 'agent-runtimes.codex.models',
method: 'GET',
path: '/api/agent-runtimes/codex/models',
auth: 'required',
});
expect(contract.routes).toContainEqual({
id: 'agent-runtimes.codex.threads',
method: 'GET',
Expand Down Expand Up @@ -1766,6 +1772,7 @@ hidden: true
headers: { 'sec-fetch-site': 'same-origin' },
})).status).toBe(200);
const protectedCodexRoutes: Array<{ path: string; init?: RequestInit }> = [
{ path: '/api/agent-runtimes/codex/models' },
{ path: '/api/agent-runtimes/codex/threads' },
{ path: '/api/agent-runtimes/codex/threads/thr-existing' },
{ path: '/api/agent-runtimes/codex/threads/thr-existing/fork', init: { method: 'POST', body: '{}' } },
Expand Down Expand Up @@ -1811,7 +1818,7 @@ hidden: true
}
});

it('dispatches Codex thread manager routes through the Product HTTP server without starting turns', async () => {
it('dispatches Codex model and thread manager routes through the Product HTTP server without starting turns', async () => {
const root = mkdtempSync(join(tmpdir(), 'mindos-http-codex-threads-'));
const calls: string[] = [];
const app = createMindosHttpServer({
Expand All @@ -1825,6 +1832,25 @@ hidden: true
initialize: async () => {
calls.push('initialize');
},
listModels: async () => {
calls.push('model/list');
return {
data: [{
id: 'gpt-5.6-sol',
model: 'gpt-5.6-sol',
displayName: 'GPT-5.6 Sol',
description: 'Fast coding model',
hidden: false,
isDefault: true,
supportedReasoningEfforts: [
{ reasoningEffort: 'low', description: 'Fastest' },
{ reasoningEffort: 'ultra', description: 'Maximum reasoning with delegation' },
],
defaultReasoningEffort: 'low',
}],
nextCursor: null,
};
},
listThreads: async () => {
calls.push('thread/list');
return {
Expand Down Expand Up @@ -1875,6 +1901,7 @@ hidden: true
const auth = { authorization: 'Bearer secret-token' };

try {
const models = await fetch(`${base}/api/agent-runtimes/codex/models`, { headers: auth });
const list = await fetch(`${base}/api/agent-runtimes/codex/threads?limit=10`, { headers: auth });
const read = await fetch(`${base}/api/agent-runtimes/codex/threads/thr-existing?includeTurns=1`, { headers: auth });
const fork = await fetch(`${base}/api/agent-runtimes/codex/threads/thr-existing/fork`, {
Expand All @@ -1891,6 +1918,14 @@ hidden: true
headers: auth,
});

expect(models.status).toBe(200);
expect(await models.json()).toEqual({
data: [expect.objectContaining({
id: 'gpt-5.6-sol',
defaultReasoningEffort: 'low',
})],
nextCursor: null,
});
expect(list.status).toBe(200);
expect(await list.json()).toEqual({
data: [expect.objectContaining({ id: 'thr-existing' })],
Expand All @@ -1908,6 +1943,9 @@ hidden: true
expect(archive.status).toBe(200);
expect(unarchive.status).toBe(200);
expect(calls).toEqual([
'initialize',
'model/list',
'close',
'initialize',
'thread/list',
'close',
Expand Down
1 change: 1 addition & 0 deletions packages/mindos/src/server/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const MINDOS_SERVER_ROUTES: MindosServerRouteContract[] = [
{ id: 'agent-runtimes.extensions', method: 'GET', path: '/api/agent-runtimes/extensions', auth: 'required' },
{ id: 'agent-runtimes.extensions.preflight', method: 'POST', path: '/api/agent-runtimes/extensions/preflight', auth: 'required' },
{ id: 'agent-runtimes.extensions.install', method: 'POST', path: '/api/agent-runtimes/extensions/install', auth: 'required' },
{ id: 'agent-runtimes.codex.models', method: 'GET', path: '/api/agent-runtimes/codex/models', auth: 'required' },
{ id: 'agent-runtimes.codex.threads', method: 'GET', path: '/api/agent-runtimes/codex/threads', auth: 'required' },
{ id: 'agent-runtimes.codex.thread', method: 'GET', path: '/api/agent-runtimes/codex/threads/[threadId]', auth: 'required' },
{ id: 'agent-runtimes.codex.thread.fork', method: 'POST', path: '/api/agent-runtimes/codex/threads/[threadId]/fork', auth: 'required' },
Expand Down
Loading