diff --git a/docs/openapi.json b/docs/openapi.json index e20a734b..75143302 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -9267,6 +9267,22 @@ }, "provider": { "type": "string" + }, + "apiMode": { + "type": "string", + "enum": [ + "chat_completions", + "codex_responses", + "anthropic_messages" + ] + }, + "api_mode": { + "type": "string", + "enum": [ + "chat_completions", + "codex_responses", + "anthropic_messages" + ] } }, "required": [ @@ -11690,4 +11706,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/client/src/api/coding-agents.ts b/packages/client/src/api/coding-agents.ts index 48ce0350..a37f0e2a 100644 --- a/packages/client/src/api/coding-agents.ts +++ b/packages/client/src/api/coding-agents.ts @@ -106,6 +106,7 @@ export interface CodingAgentLaunchResult { profile: string provider: string model: string + apiMode?: CodingAgentApiMode rootDir: string workspaceDir: string command: string diff --git a/packages/client/src/api/hermes/sessions.ts b/packages/client/src/api/hermes/sessions.ts index 54472bcc..41a7c9f1 100644 --- a/packages/client/src/api/hermes/sessions.ts +++ b/packages/client/src/api/hermes/sessions.ts @@ -11,6 +11,7 @@ export interface SessionSummary { agent_native_session_id?: string model: string provider?: string + api_mode?: ProviderApiMode title: string | null parent_session_id?: string | null fork_point_message_id?: string | null diff --git a/packages/client/src/components/hermes/chat/ChatPanel.vue b/packages/client/src/components/hermes/chat/ChatPanel.vue index 509d5014..af5a2304 100644 --- a/packages/client/src/components/hermes/chat/ChatPanel.vue +++ b/packages/client/src/components/hermes/chat/ChatPanel.vue @@ -1078,7 +1078,9 @@ async function selectSessionModel(model: string, provider: string) { if (meta?.disabled || !sessionModelSessionId.value) return; if (isSessionModelExternalCodingAgent.value) { pendingSessionModelSwitch.value = { model, provider }; - sessionModelApiMode.value = defaultSessionModelApiMode(provider); + sessionModelApiMode.value = sessionModelSession.value?.provider === provider && sessionModelSession.value.apiMode + ? normalizeCodingAgentApiMode(sessionModelSession.value.apiMode, defaultSessionModelApiMode(provider)) + : defaultSessionModelApiMode(provider); showSessionModelModeModal.value = true; return; } diff --git a/packages/client/src/stores/hermes/chat.ts b/packages/client/src/stores/hermes/chat.ts index 216a9b7b..782d78b5 100644 --- a/packages/client/src/stores/hermes/chat.ts +++ b/packages/client/src/stores/hermes/chat.ts @@ -562,6 +562,7 @@ function mapHermesSession(s: SessionSummary): Session { updatedAt: Math.round(activitySeconds * 1000), model: s.model, provider: s.provider || (s as any).billing_provider || '', + apiMode: s.api_mode, messageCount: s.message_count, messageTotal: s.message_count, loadedMessageCount: 0, @@ -615,6 +616,10 @@ function clearCodingAgentRuntimeCredentials(session?: Session | null) { session.apiKey = undefined } +function shouldPreserveRuntimeApiMode(session?: Session | null): boolean { + return isCodingAgentLikeSession(session) && session?.codingAgentMode !== 'global' +} + function isQuotaExceededError(error: unknown): boolean { if (!error || typeof error !== 'object') return false const e = error as { name?: string, code?: number } @@ -982,6 +987,7 @@ export const useChatStore = defineStore('chat', () => { ...mapped, messages: existing.messages, contextTokens: existing.contextTokens, + apiMode: mapped.apiMode || existing.apiMode, loadedMessageCount: existing.loadedMessageCount, hasMoreBefore: existing.hasMoreBefore, }) @@ -1001,11 +1007,13 @@ export const useChatStore = defineStore('chat', () => { const runtimeByIdBefore = new Map(sessions.value.map(s => [s.id, { messages: s.messages, contextTokens: s.contextTokens, + apiMode: s.apiMode, }])) for (const s of fresh) { const prev = runtimeByIdBefore.get(s.id) if (prev?.messages?.length) s.messages = prev.messages if (prev?.contextTokens != null) s.contextTokens = prev.contextTokens + if (!s.apiMode && prev?.apiMode) s.apiMode = prev.apiMode } sessions.value = fresh pruneCompletedUnreadSessions(new Set(sessions.value.map(s => s.id))) @@ -1073,6 +1081,7 @@ export const useChatStore = defineStore('chat', () => { existing.endedAt = fresh.endedAt existing.model = fresh.model existing.provider = fresh.provider + existing.apiMode = fresh.apiMode || existing.apiMode existing.messageCount = fresh.messageCount existing.inputTokens = fresh.inputTokens existing.outputTokens = fresh.outputTokens @@ -1468,18 +1477,22 @@ export const useChatStore = defineStore('chat', () => { const shouldClearRuntimeCredentials = previousProvider !== nextProvider && ( isCodingAgentLikeSession(target) || isCodingAgentLikeSession(activeTarget) ) - const ok = await setSessionModel(targetId, modelId, provider || '', apiMode) + const preservedApiMode = apiMode || (previousProvider === nextProvider + ? (shouldPreserveRuntimeApiMode(target) ? target?.apiMode : undefined) || + (shouldPreserveRuntimeApiMode(activeTarget) ? activeTarget?.apiMode : undefined) + : undefined) + const ok = await setSessionModel(targetId, modelId, provider || '', preservedApiMode) if (!ok) return false if (target) { target.model = modelId target.provider = provider || '' - if (apiMode) target.apiMode = apiMode + target.apiMode = preservedApiMode if (shouldClearRuntimeCredentials) clearCodingAgentRuntimeCredentials(target) } if (activeTarget) { activeTarget.model = modelId activeTarget.provider = provider || '' - if (apiMode) activeTarget.apiMode = apiMode + activeTarget.apiMode = preservedApiMode if (shouldClearRuntimeCredentials) clearCodingAgentRuntimeCredentials(activeTarget) } return true diff --git a/packages/server/src/controllers/hermes/sessions.ts b/packages/server/src/controllers/hermes/sessions.ts index 04924e94..78181f43 100644 --- a/packages/server/src/controllers/hermes/sessions.ts +++ b/packages/server/src/controllers/hermes/sessions.ts @@ -323,6 +323,7 @@ export async function listConversations(ctx: any) { agent_native_session_id: s.agent_native_session_id, model: s.model, provider: s.provider, + api_mode: s.api_mode, title: s.title, started_at: s.started_at, ended_at: s.ended_at, @@ -985,8 +986,17 @@ export async function setWorkspace(ctx: any) { ctx.body = { ok: true } } +type SessionProviderApiMode = 'chat_completions' | 'codex_responses' | 'anthropic_messages' + +function normalizeSessionApiMode(value: unknown): SessionProviderApiMode | undefined { + const mode = typeof value === 'string' ? value.trim() : '' + return mode === 'chat_completions' || mode === 'codex_responses' || mode === 'anthropic_messages' + ? mode + : undefined +} + export async function setModel(ctx: any) { - const { model, provider } = ctx.request.body as { model?: string; provider?: string } + const { model, provider, apiMode, api_mode } = ctx.request.body as { model?: string; provider?: string; apiMode?: SessionProviderApiMode; api_mode?: SessionProviderApiMode } if (!model || typeof model !== 'string') { ctx.status = 400 ctx.body = { error: 'model is required' } @@ -1004,19 +1014,22 @@ export async function setModel(ctx: any) { const profile = existing?.profile || requestedProfile(ctx) || 'default' const cleanModel = model.trim() const cleanProvider = (provider || '').trim() + const cleanApiMode = normalizeSessionApiMode(apiMode ?? api_mode) const codingAgentSession = isCodingAgentSession(existing) const workspace = !codingAgentSession ? await ensureHermesRunWorkspace(profile, existing?.workspace) : undefined if (!existing) { - createSession({ id, profile, title: '', model: cleanModel, provider: cleanProvider, workspace }) + createSession({ id, profile, title: '', model: cleanModel, provider: cleanProvider, api_mode: cleanApiMode || '', workspace }) } const updates: Record = { model: cleanModel, provider: cleanProvider } + if (cleanApiMode) updates.api_mode = cleanApiMode + else if (codingAgentSession && existing && existing.provider !== cleanProvider) updates.api_mode = '' if (!codingAgentSession && existing && !existing.workspace && workspace) updates.workspace = workspace if ( codingAgentSession && existing && - (existing.model !== cleanModel || existing.provider !== cleanProvider) + (existing.model !== cleanModel || existing.provider !== cleanProvider || (cleanApiMode && existing.api_mode !== cleanApiMode)) ) { updates.agent_native_session_id = '' } diff --git a/packages/server/src/db/hermes/schemas.ts b/packages/server/src/db/hermes/schemas.ts index 48ddf75c..6816a05b 100644 --- a/packages/server/src/db/hermes/schemas.ts +++ b/packages/server/src/db/hermes/schemas.ts @@ -39,6 +39,7 @@ export const SESSIONS_SCHEMA: Record = { user_id: 'TEXT', model: 'TEXT NOT NULL DEFAULT \'\'', provider: 'TEXT NOT NULL DEFAULT \'\'', + api_mode: 'TEXT NOT NULL DEFAULT \'\'', title: 'TEXT', parent_session_id: 'TEXT', fork_point_message_id: 'TEXT', diff --git a/packages/server/src/db/hermes/session-store.ts b/packages/server/src/db/hermes/session-store.ts index 404dce50..85419334 100644 --- a/packages/server/src/db/hermes/session-store.ts +++ b/packages/server/src/db/hermes/session-store.ts @@ -19,6 +19,7 @@ export interface HermesSessionRow { user_id: string | null model: string provider: string + api_mode: string title: string | null parent_session_id: string | null fork_point_message_id: string | null @@ -105,6 +106,7 @@ function mapSessionRow(row: Record): HermesSessionRow { user_id: row.user_id != null ? String(row.user_id) : null, model: String(row.model || ''), provider: String(row.provider || ''), + api_mode: String(row.api_mode || ''), title, parent_session_id: row.parent_session_id != null ? String(row.parent_session_id) : null, fork_point_message_id: row.fork_point_message_id != null ? String(row.fork_point_message_id) : null, @@ -165,6 +167,7 @@ export function createSession(data: { agent_native_session_id?: string model?: string provider?: string + api_mode?: string title?: string parent_session_id?: string | null workspace?: string @@ -177,7 +180,7 @@ export function createSession(data: { id: data.id, profile: data.profile || 'default', source, agent, agent_mode: data.agent_mode || '', agent_session_id: data.agent_session_id || '', agent_native_session_id: data.agent_native_session_id || '', - user_id: null, model: data.model || '', provider: data.provider || '', title: data.title || null, + user_id: null, model: data.model || '', provider: data.provider || '', api_mode: data.api_mode || '', title: data.title || null, parent_session_id: data.parent_session_id || null, fork_point_message_id: null, started_at: now, ended_at: null, end_reason: null, @@ -189,8 +192,8 @@ export function createSession(data: { } const db = getDb()! db.prepare( - `INSERT INTO ${SESSIONS_TABLE} (id, profile, source, agent, agent_mode, agent_session_id, agent_native_session_id, model, provider, title, parent_session_id, started_at, last_active, workspace) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO ${SESSIONS_TABLE} (id, profile, source, agent, agent_mode, agent_session_id, agent_native_session_id, model, provider, api_mode, title, parent_session_id, started_at, last_active, workspace) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ).run( data.id, data.profile || 'default', @@ -201,6 +204,7 @@ export function createSession(data: { data.agent_native_session_id || '', data.model || '', data.provider || '', + data.api_mode || '', data.title || null, data.parent_session_id || null, now, @@ -221,6 +225,7 @@ export function createBranchedSession(data: { agent_native_session_id?: string model?: string provider?: string + api_mode?: string title?: string workspace?: string | null ended_at: number @@ -257,8 +262,8 @@ export function createBranchedSession(data: { ).run(data.ended_at, 'branched', data.parent_session_id) db.prepare( - `INSERT INTO ${SESSIONS_TABLE} (id, profile, source, agent, agent_mode, agent_session_id, agent_native_session_id, model, provider, title, parent_session_id, started_at, last_active, workspace, message_count) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO ${SESSIONS_TABLE} (id, profile, source, agent, agent_mode, agent_session_id, agent_native_session_id, model, provider, api_mode, title, parent_session_id, started_at, last_active, workspace, message_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ).run( data.id, data.profile || 'default', @@ -269,6 +274,7 @@ export function createBranchedSession(data: { data.agent_native_session_id || '', data.model || '', data.provider || '', + data.api_mode || '', data.title || null, data.parent_session_id, data.ended_at, diff --git a/packages/server/src/services/agent-runner/coding-agent-run-manager.ts b/packages/server/src/services/agent-runner/coding-agent-run-manager.ts index 3fcc1ed9..61f95e6b 100644 --- a/packages/server/src/services/agent-runner/coding-agent-run-manager.ts +++ b/packages/server/src/services/agent-runner/coding-agent-run-manager.ts @@ -3,6 +3,7 @@ import { existsSync, accessSync, chmodSync, constants as fsConstants, readFileSy import { homedir } from 'os' import { spawn, type ChildProcess } from 'child_process' import { createSession, addMessage, getSession, updateSession, updateSessionStats } from '../../db/hermes/session-store' +import type { ApiMode } from './types' import { logger } from '../logger' import { applyResponseStreamEvent, flushResponseRunToDb } from '../hermes/run-chat/response-stream' import { calcAndUpdateUsage, updateMessageContextTokenUsage } from '../hermes/run-chat/usage' @@ -62,6 +63,7 @@ export interface CodingAgentRunLaunch { profile: string provider: string model: string + apiMode?: ApiMode sessionId: string agentNativeSessionId?: string nativeResume?: boolean @@ -417,6 +419,7 @@ export class CodingAgentRunManager { provider?: string model?: string reasoningEffort?: string + apiMode?: ApiMode }): boolean { const run = this.getBySession(sessionId) if (!run || run.exited) return false @@ -428,6 +431,8 @@ export class CodingAgentRunManager { const model = String(launch.model || '').trim() if (provider && run.launch.provider !== provider) return false if (model && run.launch.model !== model) return false + const apiMode = String(launch.apiMode || '').trim() + if (apiMode && String(run.launch.apiMode || '').trim() !== apiMode) return false } if (String(run.launch.reasoningEffort || '').trim() !== String(launch.reasoningEffort || '').trim()) return false if (!hasManagedHermesMcpConfig(run)) return false @@ -697,12 +702,13 @@ export class CodingAgentRunManager { createSession({ id: run.launch.sessionId, profile: run.launch.profile, - source, - agent: run.launch.agentId === 'codex' ? 'codex' : 'claude', - agent_session_id: run.id, - agent_native_session_id: run.launch.agentNativeSessionId, - model: run.launch.model, + source, + agent: run.launch.agentId === 'codex' ? 'codex' : 'claude', + agent_session_id: run.id, + agent_native_session_id: run.launch.agentNativeSessionId, + model: run.launch.model, provider: run.launch.provider, + api_mode: run.launch.apiMode || '', title: '', workspace: run.launch.workspaceDir, }) diff --git a/packages/server/src/services/coding-agents.ts b/packages/server/src/services/coding-agents.ts index ba89f754..1d74462a 100644 --- a/packages/server/src/services/coding-agents.ts +++ b/packages/server/src/services/coding-agents.ts @@ -124,6 +124,7 @@ export interface CodingAgentLaunchResult { profile: string provider: string model: string + apiMode?: ApiMode rootDir: string workspaceDir: string command: string @@ -500,12 +501,17 @@ async function resolveStoredProviderLaunchInput( if (input.mode === 'global') return input const profile = String(input.profile || existingSession?.profile || 'default').trim() || 'default' - const provider = String(input.provider || existingSession?.provider || '').trim() + const inputProvider = String(input.provider || '').trim() + const storedProvider = String(existingSession?.provider || '').trim() + const provider = String(inputProvider || storedProvider).trim() const model = String(input.model || existingSession?.model || '').trim() const workspace = input.workspace || existingSession?.workspace || undefined let baseUrl = String(input.baseUrl || '').trim() let apiKey = String(input.apiKey || '').trim() - let apiMode = input.apiMode + const storedApiMode = !inputProvider || inputProvider === storedProvider + ? normalizeStoredLaunchApiMode(existingSession?.api_mode) + : undefined + let apiMode = input.apiMode || storedApiMode let canonicalProvider = provider const ignoredStaleProviderRuntime = belongsToDifferentBuiltinProvider(provider, baseUrl) if (ignoredStaleProviderRuntime) { @@ -577,6 +583,15 @@ async function resolveStoredProviderLaunchInput( } } +function normalizeStoredLaunchApiMode(value: unknown): ApiMode | undefined { + if (!value) return undefined + try { + return normalizeLaunchApiMode(value, 'chat_completions') + } catch { + return undefined + } +} + function normalizeLaunchApiMode(value: unknown, fallback: ApiMode): ApiMode { if (!value) return fallback const mode = String(value).trim() as ApiMode @@ -1726,6 +1741,7 @@ export async function prepareCodingAgentLaunch(id: string, input: CodingAgentLau profile: scope.profile, provider: scope.provider, model, + apiMode, rootDir, workspaceDir, command: tool.command, @@ -1773,7 +1789,8 @@ export async function startCodingAgentRun( ? storedCodingAgentMode(existingSession) === requestedMode && (existingSession.agent === (id === 'codex' ? 'codex' : 'claude') || !existingSession.agent) && String(existingSession.provider || '').trim() === String(resolvedInput.provider || '').trim() && - String(existingSession.model || '').trim() === String(resolvedInput.model || '').trim() + String(existingSession.model || '').trim() === String(resolvedInput.model || '').trim() && + (!String(existingSession.api_mode || '').trim() || String(existingSession.api_mode || '').trim() === String(resolvedInput.apiMode || '').trim()) : false const existingNativeSessionId = canResumeNativeSession ? existingSession?.agent_native_session_id || '' : '' const agentNativeSessionId = resolvedInput.agentNativeSessionId || existingNativeSessionId || (id === 'claude-code' ? randomUUID() : '') @@ -1800,6 +1817,7 @@ export async function startCodingAgentRun( profile: launch.profile, provider: persistedProvider, model: launch.model, + apiMode: launch.apiMode, sessionId, agentNativeSessionId, nativeResume: Boolean(existingNativeSessionId), @@ -1820,6 +1838,7 @@ export async function startCodingAgentRun( agent_native_session_id: agentNativeSessionId, model: launch.model, provider: persistedProvider, + api_mode: launch.apiMode || '', workspace: launch.workspaceDir, }) return { diff --git a/packages/server/src/services/hermes/conversations.ts b/packages/server/src/services/hermes/conversations.ts index ce2b40c8..6882cb2b 100644 --- a/packages/server/src/services/hermes/conversations.ts +++ b/packages/server/src/services/hermes/conversations.ts @@ -42,6 +42,7 @@ export interface ConversationSummary { agent_native_session_id?: string model: string provider?: string + api_mode?: string title: string | null started_at: number ended_at: number | null diff --git a/packages/server/src/services/hermes/run-chat/handle-coding-agent-run.ts b/packages/server/src/services/hermes/run-chat/handle-coding-agent-run.ts index 816fea4b..c1dc05e0 100644 --- a/packages/server/src/services/hermes/run-chat/handle-coding-agent-run.ts +++ b/packages/server/src/services/hermes/run-chat/handle-coding-agent-run.ts @@ -64,11 +64,13 @@ export async function handleCodingAgentRun( const storedSession = getSession(sessionId) const launchProvider = data.provider || (mode === 'scoped' ? storedSession?.provider || undefined : undefined) const launchModel = data.model || (mode === 'scoped' ? storedSession?.model || undefined : undefined) + const launchApiMode = data.apiMode || data.api_mode || (mode === 'scoped' ? storedSession?.api_mode || undefined : undefined) if (runId && !codingAgentRunManager.isSessionLaunchCompatible(sessionId, { agentId, mode, provider: launchProvider, model: launchModel, + apiMode: launchApiMode, reasoningEffort: data.reasoning_effort, })) { codingAgentRunManager.stop(sessionId, { reportClosed: false }) @@ -84,7 +86,7 @@ export async function handleCodingAgentRun( workspace: data.workspace, baseUrl: data.baseUrl || data.base_url, apiKey: data.apiKey || data.api_key, - apiMode: data.apiMode || data.api_mode, + apiMode: launchApiMode, reasoningEffort: data.reasoning_effort, sessionSource: data.session_source, }, state) diff --git a/packages/server/src/services/hermes/run-chat/session-command.ts b/packages/server/src/services/hermes/run-chat/session-command.ts index ffa229db..795c7da0 100644 --- a/packages/server/src/services/hermes/run-chat/session-command.ts +++ b/packages/server/src/services/hermes/run-chat/session-command.ts @@ -1117,6 +1117,7 @@ function createBranchSession(parentSessionId: string, requestedTitle: string, ct agent_native_session_id: parent.agent_native_session_id || '', model: parent.model || ctx.model || '', provider: parent.provider || ctx.provider || '', + api_mode: parent.api_mode || '', title, parent_session_id: parentSessionId, workspace: parent.workspace || undefined, diff --git a/scripts/generate-openapi.mjs b/scripts/generate-openapi.mjs index 2c8a0e04..8e74358e 100644 --- a/scripts/generate-openapi.mjs +++ b/scripts/generate-openapi.mjs @@ -608,6 +608,9 @@ function schemaFromType(type) { const schema = {} if (/\bnull\b/.test(normalized)) schema.nullable = true + if (/SessionProviderApiMode|CodingAgentApiMode/.test(normalized)) { + return { ...schema, type: 'string', enum: ['chat_completions', 'codex_responses', 'anthropic_messages'] } + } if (/string\[\]|Array/.test(normalized)) { return { ...schema, type: 'array', items: { type: 'string' } } } diff --git a/tests/client/chat-store-reasoning-tool-boundary.test.ts b/tests/client/chat-store-reasoning-tool-boundary.test.ts index a2d33040..eabf3e58 100644 --- a/tests/client/chat-store-reasoning-tool-boundary.test.ts +++ b/tests/client/chat-store-reasoning-tool-boundary.test.ts @@ -546,6 +546,33 @@ describe('chat store reasoning/tool boundaries', () => { expect(session.apiMode).toBe('chat_completions') }) + + it('preserves a scoped coding-agent API mode when reselecting the same provider', async () => { + const store = useChatStore() + const session = makeSession() + session.source = 'coding_agent' + session.agent = 'codex' + session.codingAgentId = 'codex' + session.codingAgentMode = 'scoped' + session.provider = 'fun-codex' + session.model = 'gpt-5.4' + session.apiMode = 'chat_completions' + store.sessions = [session] + store.activeSessionId = 'session-1' + store.activeSession = session + + const ok = await store.switchSessionModel('gpt-5.5', 'fun-codex', 'session-1') + + expect(ok).toBe(true) + expect(sessionsApi.setSessionModel).toHaveBeenCalledWith( + 'session-1', + 'gpt-5.5', + 'fun-codex', + 'chat_completions', + ) + expect(session.apiMode).toBe('chat_completions') + }) + it('sends the selected workspace when starting a coding-agent run', async () => { const store = useChatStore() const session = makeSession() diff --git a/tests/server/coding-agent-resume-config.test.ts b/tests/server/coding-agent-resume-config.test.ts index ed20ac77..084070f5 100644 --- a/tests/server/coding-agent-resume-config.test.ts +++ b/tests/server/coding-agent-resume-config.test.ts @@ -278,6 +278,39 @@ describe('coding agent resumed session config', () => { })) }) + + it('prefers a stored coding-agent API mode when resuming without an explicit mode', async () => { + const home = makeHome() + getSessionMock.mockReturnValue({ + id: 'session-1', + profile: 'default', + source: 'coding_agent', + agent: 'codex', + agent_session_id: 'agent-session-1', + provider: 'fun-codex', + model: 'gpt-5.5', + api_mode: 'chat_completions', + }) + readConfigYamlForProfileMock.mockResolvedValue({}) + safeReadFileMock.mockResolvedValue('') + + const { startCodingAgentRun } = await import('../../packages/server/src/services/coding-agents') + await startCodingAgentRun('codex', { + sessionId: 'session-1', + baseUrl: 'https://api.apikey.fun/v1', + apiKey: 'sk-test', + }) + + const launch = startRunMock.mock.calls[0][0] + expect(launch.apiMode).toBe('chat_completions') + const config = readFileSync(join(home, 'coding-agent', 'model', 'default', 'fun-codex', 'codex', 'config.toml'), 'utf-8') + const routeKey = config.match(/\/api\/codex-proxy\/([^/]+)\/v1/)?.[1] || '' + expect(Buffer.from(routeKey, 'base64url').toString('utf8')).toContain('chat_completions') + expect(updateSessionMock).toHaveBeenCalledWith('session-1', expect.objectContaining({ + api_mode: 'chat_completions', + })) + }) + it('rejects OAuth/subscription providers for scoped coding-agent launches', async () => { makeHome() getSessionMock.mockReturnValue(null) diff --git a/tests/server/session-command-branch.test.ts b/tests/server/session-command-branch.test.ts index 5aa16b20..763df62a 100644 --- a/tests/server/session-command-branch.test.ts +++ b/tests/server/session-command-branch.test.ts @@ -83,6 +83,7 @@ function makeParentSession(overrides: Record = {}) { agent_native_session_id: '', model: 'openai/gpt-5.4', provider: 'openai-codex', + api_mode: 'chat_completions', title: 'Parent chat', preview: 'Parent prompt', workspace: '/repo', @@ -229,6 +230,7 @@ describe('branch session command', () => { agent: 'hermes', model: 'openai/gpt-5.4', provider: 'openai-codex', + api_mode: 'chat_completions', workspace: '/repo', ended_at: expect.any(Number), last_active: expect.any(Number), diff --git a/tests/server/session-store-branch.test.ts b/tests/server/session-store-branch.test.ts index 5cf92720..6c119de4 100644 --- a/tests/server/session-store-branch.test.ts +++ b/tests/server/session-store-branch.test.ts @@ -52,6 +52,7 @@ describe('createBranchedSession', () => { user_id: null, model: 'openai/gpt-5.4', provider: 'openai-codex', + api_mode: 'chat_completions', title: 'Forked child', parent_session_id: 'parent-session', fork_point_message_id: '42', @@ -86,6 +87,7 @@ describe('createBranchedSession', () => { agent: 'hermes', model: 'openai/gpt-5.4', provider: 'openai-codex', + api_mode: 'chat_completions', title: 'Forked child', workspace: '/repo', ended_at: 123, diff --git a/tests/server/sessions-controller.test.ts b/tests/server/sessions-controller.test.ts index 3459afb8..77222943 100644 --- a/tests/server/sessions-controller.test.ts +++ b/tests/server/sessions-controller.test.ts @@ -1317,7 +1317,7 @@ describe('session conversations controller', () => { expect(ctx.body).toEqual({ ok: true }) }) - it('stores a coding agent session model without stopping the runner or notifying the Hermes bridge', async () => { + it('stores a coding agent session model and API mode without stopping the runner or notifying the Hermes bridge', async () => { bridgeGetRuntimeStateMock.mockReturnValue({ ready: true, running: true, endpoint: 'ipc:///tmp/hermes-agent-bridge.sock' }) getSessionMock.mockReturnValue({ id: 'codex-session', @@ -1326,6 +1326,7 @@ describe('session conversations controller', () => { agent: 'codex', model: 'old-model', provider: 'openrouter', + api_mode: 'codex_responses', agent_native_session_id: 'old-native-thread', workspace: '/tmp/original-workspace', }) @@ -1333,7 +1334,7 @@ describe('session conversations controller', () => { const mod = await import('../../packages/server/src/controllers/hermes/sessions') const ctx: any = { params: { id: 'codex-session' }, - request: { body: { model: 'gpt-5.5', provider: 'openai-codex' } }, + request: { body: { model: 'gpt-5.5', provider: 'openai-codex', apiMode: 'chat_completions' } }, body: null, } await mod.setModel(ctx) @@ -1341,6 +1342,7 @@ describe('session conversations controller', () => { expect(localUpdateSessionMock).toHaveBeenCalledWith('codex-session', { model: 'gpt-5.5', provider: 'openai-codex', + api_mode: 'chat_completions', agent_native_session_id: '', }) expect(codingAgentRunManagerMock.stop).not.toHaveBeenCalled()