mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-17 22:20:19 +00:00
fix(coding-agent): persist API mode per session
This commit is contained in:
+17
-1
@@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ export interface CodingAgentLaunchResult {
|
||||
profile: string
|
||||
provider: string
|
||||
model: string
|
||||
apiMode?: CodingAgentApiMode
|
||||
rootDir: string
|
||||
workspaceDir: string
|
||||
command: string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, string> = { 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 = ''
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export const SESSIONS_SCHEMA: Record<string, string> = {
|
||||
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',
|
||||
|
||||
@@ -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<string, unknown>): 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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string>/.test(normalized)) {
|
||||
return { ...schema, type: 'array', items: { type: 'string' } }
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -83,6 +83,7 @@ function makeParentSession(overrides: Record<string, any> = {}) {
|
||||
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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user