Files
hermes-ui/tests/server/coding-agents-launch.test.ts
T
toor 1e9ea89d86
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
2026-10-16 10:34:35 +08:00

1221 lines
49 KiB
TypeScript

import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { dirname, join } from 'path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { claudeProxyMessages, claudeProxyModels, registerClaudeCodeProxyTarget } from '../../packages/server/src/services/agent-runner/proxies/claude-code-proxy'
import { codexProxyModels, codexProxyResponses, registerCodexProxyTarget } from '../../packages/server/src/services/agent-runner/proxies/codex-proxy'
import { prepareCodingAgentLaunch } from '../../packages/server/src/services/coding-agents'
const homes: string[] = []
function mockProcessUid(uid: number) {
vi.stubGlobal('process', Object.assign(process, {
getuid: vi.fn(() => uid),
geteuid: vi.fn(() => uid),
}))
}
function makeHome() {
const home = mkdtempSync(join(tmpdir(), 'hermes-coding-agent-launch-'))
homes.push(home)
process.env.HERMES_WEB_UI_HOME = home
process.env.HERMES_CODING_AGENT_GLOBAL_HOME = join(home, 'global-home')
return home
}
beforeEach(() => {
mockProcessUid(1000)
})
afterEach(() => {
delete process.env.HERMES_WEB_UI_HOME
delete process.env.HERMES_CODING_AGENT_GLOBAL_HOME
delete process.env.HERMES_AGENT_NODE
vi.restoreAllMocks()
vi.unstubAllGlobals()
for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true })
})
function makeProxyContext(routeKey: string, token: string, body: any): any {
return {
params: { key: routeKey },
request: { body },
responseHeaders: {} as Record<string, string>,
get(name: string) {
if (name.toLowerCase() === 'authorization') return `Bearer ${token}`
return ''
},
set(name: string, value: string) {
this.responseHeaders[name] = value
},
}
}
describe('coding agent launch preparation', () => {
it('launches Claude Code with the global config when requested', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('claude-code', {
mode: 'global',
profile: 'default',
})
expect(result).toMatchObject({
agentId: 'claude-code',
mode: 'global',
profile: 'default',
provider: 'global',
model: '',
rootDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),
workspaceDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),
command: 'claude',
args: [
'--append-system-prompt-file',
join(home, 'global-home', '.claude', 'hermes-rules.md'),
'--dangerously-skip-permissions',
],
env: {},
shellCommand: `cd ${join(home, 'coding-agent', 'workspace', 'default', 'global')} && claude --append-system-prompt-file ${join(home, 'global-home', '.claude', 'hermes-rules.md')} --dangerously-skip-permissions`,
files: [{
key: 'prompt',
path: '~/.claude/hermes-rules.md',
absolutePath: join(home, 'global-home', '.claude', 'hermes-rules.md'),
}],
})
const prompt = readFileSync(join(home, 'global-home', '.claude', 'hermes-rules.md'), 'utf-8')
expect(prompt).toContain('<!-- BEGIN HERMES WEB UI PROMPT -->')
expect(prompt).toContain('# 输出格式规范')
})
it('uses Claude Code auto permission mode instead of dangerous bypass when running as root', async () => {
mockProcessUid(0)
const home = makeHome()
const result = await prepareCodingAgentLaunch('claude-code', {
mode: 'global',
profile: 'default',
})
expect(result).toMatchObject({
agentId: 'claude-code',
mode: 'global',
rootDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),
command: 'claude',
args: [
'--append-system-prompt-file',
join(home, 'global-home', '.claude', 'hermes-rules.md'),
'--permission-mode',
'auto',
],
shellCommand: `cd ${join(home, 'coding-agent', 'workspace', 'default', 'global')} && claude --append-system-prompt-file ${join(home, 'global-home', '.claude', 'hermes-rules.md')} --permission-mode auto`,
})
})
it('launches Codex with the global config when requested', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('codex', {
mode: 'global',
profile: 'default',
})
expect(result).toMatchObject({
agentId: 'codex',
mode: 'global',
profile: 'default',
provider: 'global',
model: '',
rootDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),
workspaceDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),
command: 'codex',
args: [],
env: {},
shellCommand: `cd ${join(home, 'coding-agent', 'workspace', 'default', 'global')} && codex`,
files: [],
})
})
it('preserves existing global Claude Code prompt files while updating the Hermes block', async () => {
const home = makeHome()
const claudePromptPath = join(home, 'global-home', '.claude', 'hermes-rules.md')
mkdirSync(dirname(claudePromptPath), { recursive: true })
writeFileSync(claudePromptPath, 'Existing Claude notes\n')
await prepareCodingAgentLaunch('claude-code', { mode: 'global', profile: 'default' })
await prepareCodingAgentLaunch('claude-code', { mode: 'global', profile: 'default' })
const claudePrompt = readFileSync(claudePromptPath, 'utf-8')
expect(claudePrompt).toContain('Existing Claude notes')
expect(claudePrompt.match(/BEGIN HERMES WEB UI PROMPT/g)).toHaveLength(1)
})
it('uses a selected workspace directory when launching a coding agent', async () => {
const home = makeHome()
const workspace = join(home, 'selected workspace')
const result = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'openrouter',
model: 'openai/gpt-oss-20b:free',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
workspace,
})
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'codex'))
expect(result.workspaceDir).toBe(workspace)
expect(result.shellCommand).toContain(workspace)
})
it('launches Claude Code with scoped settings instead of a CLI --model override', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('claude-code', {
profile: 'default',
provider: 'openrouter',
model: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
})
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'claude-code'))
expect(result.workspaceDir).toBe(join(home, 'coding-agent', 'workspace', 'default', 'openrouter'))
expect(result.args).toEqual([
'--settings',
join(result.rootDir, 'settings.json'),
'--mcp-config',
join(result.rootDir, 'mcp.json'),
'--append-system-prompt-file',
join(result.rootDir, 'hermes-rules.md'),
'--dangerously-skip-permissions',
])
expect(result.shellCommand).toContain(`cd ${join(home, 'coding-agent', 'workspace', 'default', 'openrouter')} &&`)
expect(result.shellCommand).toContain(join(result.rootDir, 'launch.sh'))
expect(result.shellCommand).not.toContain('ANTHROPIC_API_KEY')
expect(result.shellCommand).not.toContain('hwui_')
expect(result.shellCommand).not.toContain('--model')
const launcher = readFileSync(join(result.rootDir, 'launch.sh'), 'utf-8')
expect(launcher).toContain('exec claude --settings')
expect(launcher).toContain('--dangerously-skip-permissions')
expect(launcher).not.toContain('--model')
const settings = JSON.parse(readFileSync(join(result.rootDir, 'settings.json'), 'utf-8'))
expect(settings.model).toBe('cognitivecomputations/dolphin-mistral-24b-venice-edition:free')
expect(settings.env.ANTHROPIC_API_KEY).toMatch(/^hwui_/)
expect(settings.env).not.toHaveProperty('ANTHROPIC_AUTH_TOKEN')
expect(settings.env.ANTHROPIC_BASE_URL).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/api\/claude-code-proxy\/.+$/)
expect(settings.env).toMatchObject({
ANTHROPIC_MODEL: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
ANTHROPIC_CUSTOM_MODEL_OPTION: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'Dolphin Mistral 24b Venice Edition:Free',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: 'Dolphin Mistral 24b Venice Edition:Free',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: 'Dolphin Mistral 24b Venice Edition:Free',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: 'Dolphin Mistral 24b Venice Edition:Free',
})
expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).not.toBe('claude-sonnet-4-6')
const mcp = JSON.parse(readFileSync(join(result.rootDir, 'mcp.json'), 'utf-8'))
expect(mcp.mcpServers['hermes-studio-api']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: home,
HERMES_WEBUI_STATE_DIR: home,
HERMES_WEB_UI_PROFILE: 'default',
HERMES_MCP_SERVER_NAME: 'hermes-studio-api',
HERMES_MCP_TOOLSET: 'api',
HERMES_WEB_UI_MANAGED_MCP: '1',
},
})
expect(mcp.mcpServers['hermes-studio-devices']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices'],
env: {
HERMES_MCP_SERVER_NAME: 'hermes-studio-devices',
HERMES_MCP_TOOLSET: 'devices',
},
})
expect(mcp.mcpServers['hermes-studio-use']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use'],
env: {
HERMES_MCP_SERVER_NAME: 'hermes-studio-use',
HERMES_MCP_TOOLSET: 'use',
},
})
const prompt = readFileSync(join(result.rootDir, 'hermes-rules.md'), 'utf-8')
expect(prompt).toContain('# 输出格式规范')
expect(prompt).toContain('当你的回复中包含图片、视频或文件引用时')
})
it('uses the desktop runtime node for scoped Hermes Studio MCP configs when available', async () => {
const home = makeHome()
process.env.HERMES_AGENT_NODE = '/runtime/node'
const result = await prepareCodingAgentLaunch('claude-code', {
profile: 'default',
provider: 'openrouter',
model: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
})
const mcp = JSON.parse(readFileSync(join(result.rootDir, 'mcp.json'), 'utf-8'))
expect(mcp.mcpServers['hermes-studio-api']).toMatchObject({
command: '/runtime/node',
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_HOME: home,
HERMES_MCP_SERVER_NAME: 'hermes-studio-api',
HERMES_MCP_TOOLSET: 'api',
},
})
expect(mcp.mcpServers['hermes-studio-devices'].command).toBe('/runtime/node')
expect(mcp.mcpServers['hermes-studio-use'].command).toBe('/runtime/node')
})
it('cleans legacy Hermes MCP entries from scoped Claude and Codex configs', async () => {
const home = makeHome()
const claudeRoot = join(home, 'coding-agent', 'model', 'default', 'openrouter', 'claude-code')
const claudeMcpPath = join(claudeRoot, 'mcp.json')
mkdirSync(dirname(claudeMcpPath), { recursive: true })
writeFileSync(claudeMcpPath, `${JSON.stringify({
mcpServers: {
'hermes-studio': {
command: 'hermes-web-ui-mcp',
env: { HERMES_WEB_UI_MANAGED_MCP: '1' },
},
'hermes-web-ui-mcp': {
command: 'hermes-web-ui-mcp',
env: { HERMES_WEB_UI_MANAGED_MCP: '1' },
},
custom: {
command: 'custom-mcp',
},
},
}, null, 2)}\n`)
const claude = await prepareCodingAgentLaunch('claude-code', {
profile: 'default',
provider: 'openrouter',
model: 'anthropic/claude-sonnet-4.6',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
})
const claudeMcp = JSON.parse(readFileSync(join(claude.rootDir, 'mcp.json'), 'utf-8'))
expect(claudeMcp.mcpServers['hermes-studio']).toBeUndefined()
expect(claudeMcp.mcpServers['hermes-web-ui-mcp']).toBeUndefined()
expect(claudeMcp.mcpServers.custom).toEqual({ command: 'custom-mcp' })
expect(claudeMcp.mcpServers['hermes-studio-api']).toBeDefined()
expect(claudeMcp.mcpServers['hermes-studio-devices']).toBeDefined()
expect(claudeMcp.mcpServers['hermes-studio-use']).toBeDefined()
const codexRoot = join(home, 'coding-agent', 'model', 'default', 'openrouter', 'codex')
const codexConfigPath = join(codexRoot, 'config.toml')
mkdirSync(dirname(codexConfigPath), { recursive: true })
writeFileSync(codexConfigPath, [
'[mcp_servers.hermes-studio]',
'command = "hermes-web-ui-mcp"',
'[mcp_servers.hermes-web-ui-mcp]',
'command = "hermes-web-ui-mcp"',
'',
].join('\n'))
const codex = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'openrouter',
model: 'openai/gpt-oss-20b:free',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
})
const codexConfig = readFileSync(join(codex.rootDir, 'config.toml'), 'utf-8')
expect(codexConfig).not.toContain('[mcp_servers.hermes-studio]')
expect(codexConfig).not.toContain('[mcp_servers.hermes-web-ui-mcp]')
expect(codexConfig).toContain('[mcp_servers.hermes-studio-api]')
expect(codexConfig).toContain('[mcp_servers.hermes-studio-devices]')
expect(codexConfig).toContain('[mcp_servers.hermes-studio-use]')
})
it('inherits external MCP configs for scoped Claude and Codex launches', async () => {
const home = makeHome()
const claudeGlobalMcpPath = join(home, 'global-home', '.claude', 'mcp.json')
const claudeGlobalSettingsPath = join(home, 'global-home', '.claude', 'settings.json')
const codexGlobalConfigPath = join(home, 'global-home', '.codex', 'config.toml')
const codexScopedConfigPath = join(home, 'coding-agent', 'model', 'default', 'openrouter', 'codex', 'config.toml')
mkdirSync(dirname(claudeGlobalMcpPath), { recursive: true })
mkdirSync(dirname(codexGlobalConfigPath), { recursive: true })
mkdirSync(dirname(codexScopedConfigPath), { recursive: true })
writeFileSync(claudeGlobalMcpPath, `${JSON.stringify({
mcpServers: {
'nowledge-mem': {
type: 'streamableHttp',
url: 'https://nowledge-mem.example/remote-api/mcp/',
headers: { APP: 'claude code', Authorization: 'Bearer test' },
},
'hermes-studio-api': { command: 'stale-managed' },
},
}, null, 2)}
`)
writeFileSync(claudeGlobalSettingsPath, `${JSON.stringify({
enabledMcpjsonServers: ['nowledge-mem'],
plugins: { 'nowledge-mem@nowledge-community': true },
}, null, 2)}
`)
writeFileSync(codexGlobalConfigPath, [
'[mcp_servers.nowledge-mem]',
'type = "streamableHttp"',
'url = "https://nowledge-mem.example/remote-api/mcp/"',
'',
'[mcp_servers.nowledge-mem.http_headers]',
'APP = "codex"',
'Authorization = "Bearer test"',
'',
'[mcp_servers.hermes-studio-api]',
'command = "stale-managed"',
'',
'[model_providers.unrelated]',
'name = "should-not-be-copied"',
'',
].join('\n'))
writeFileSync(codexScopedConfigPath, [
'[mcp_servers.nowledge-mem]',
'type = "streamableHttp"',
'url = "https://nowledge-mem.scoped-latest.example/remote-api/mcp/"',
'',
'[mcp_servers.nowledge-mem.http_headers]',
'APP = "codex-scoped"',
'Authorization = "Bearer scoped"',
'',
'[mcp_servers.nowledge-mem]',
'type = "streamableHttp"',
'url = "https://nowledge-mem.scoped-latest.example/remote-api/mcp/"',
'',
'[mcp_servers.nowledge-mem.http_headers]',
'APP = "codex-scoped-latest"',
'Authorization = "Bearer scoped-latest"',
'',
].join('\n'))
const claude = await prepareCodingAgentLaunch('claude-code', {
profile: 'default',
provider: 'openrouter',
model: 'anthropic/claude-sonnet-4.6',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'test-api-key',
})
const claudeSettings = JSON.parse(readFileSync(join(claude.rootDir, 'settings.json'), 'utf-8'))
const claudeMcp = JSON.parse(readFileSync(join(claude.rootDir, 'mcp.json'), 'utf-8'))
expect(claudeSettings.enabledMcpjsonServers).toEqual(['nowledge-mem'])
expect(claudeSettings.plugins).toMatchObject({ 'nowledge-mem@nowledge-community': true })
expect(claudeMcp.mcpServers['nowledge-mem']).toMatchObject({
type: 'http',
url: 'https://nowledge-mem.example/remote-api/mcp/',
})
expect(claudeMcp.mcpServers['hermes-studio-api'].command).toBe(process.execPath)
const codex = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'openrouter',
model: 'openai/gpt-oss-20b:free',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'test-api-key',
})
const codexConfig = readFileSync(join(codex.rootDir, 'config.toml'), 'utf-8')
expect(codexConfig.match(/^\[mcp_servers\.nowledge-mem\]$/gm)).toHaveLength(1)
expect(codexConfig.match(/^\[mcp_servers\.nowledge-mem\.http_headers\]$/gm)).toHaveLength(1)
expect(codexConfig).toContain('url = "https://nowledge-mem.scoped-latest.example/remote-api/mcp/"')
expect(codexConfig).toContain('APP = "codex-scoped-latest"')
expect(codexConfig).not.toContain('APP = "codex"')
expect(codexConfig).not.toContain('APP = "codex-scoped"')
expect(codexConfig).not.toContain('command = "stale-managed"')
expect(codexConfig).not.toContain('[model_providers.unrelated]')
expect(codexConfig).toContain('[mcp_servers.hermes-studio-api]')
expect(codexConfig).toContain('[mcp_servers.hermes-studio-devices]')
expect(codexConfig).toContain('[mcp_servers.hermes-studio-use]')
})
it('isolates Claude Code settings for hidden chat runs only', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('claude-code', {
profile: 'default',
provider: 'openrouter',
model: 'anthropic/claude-sonnet-4.6',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
isolateSettings: true,
})
expect(result.args).toEqual([
'--settings',
join(result.rootDir, 'settings.json'),
'--setting-sources',
'local',
'--mcp-config',
join(result.rootDir, 'mcp.json'),
'--append-system-prompt-file',
join(result.rootDir, 'hermes-rules.md'),
'--dangerously-skip-permissions',
])
expect(result.shellCommand).not.toContain('--setting-sources local')
const launcher = readFileSync(join(result.rootDir, 'launch.sh'), 'utf-8')
expect(launcher).toContain('--setting-sources local')
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'claude-code'))
})
it('uses Claude Code auto permission mode for scoped root launches', async () => {
mockProcessUid(0)
const home = makeHome()
const result = await prepareCodingAgentLaunch('claude-code', {
profile: 'default',
provider: 'openrouter',
model: 'anthropic/claude-sonnet-4.6',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
isolateSettings: true,
})
expect(result.args).toEqual([
'--settings',
join(result.rootDir, 'settings.json'),
'--setting-sources',
'local',
'--mcp-config',
join(result.rootDir, 'mcp.json'),
'--append-system-prompt-file',
join(result.rootDir, 'hermes-rules.md'),
'--permission-mode',
'auto',
])
const launcher = readFileSync(join(result.rootDir, 'launch.sh'), 'utf-8')
expect(launcher).toContain('--permission-mode auto')
expect(launcher).not.toContain('--dangerously-skip-permissions')
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'claude-code'))
})
it('keeps Claude Code protocol overrides behind the local proxy', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('claude-code', {
profile: 'default',
provider: 'openrouter',
model: 'anthropic/claude-sonnet-4.6',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
apiMode: 'anthropic_messages',
})
const settings = JSON.parse(readFileSync(join(result.rootDir, 'settings.json'), 'utf-8'))
expect(settings.env.ANTHROPIC_API_KEY).toMatch(/^hwui_/)
expect(settings.env).not.toHaveProperty('ANTHROPIC_AUTH_TOKEN')
expect(settings.env.ANTHROPIC_BASE_URL).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/api\/claude-code-proxy\/.+$/)
})
it('keeps Codex model selection on the CLI while isolating CODEX_HOME', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'openrouter',
model: 'openai/gpt-oss-20b:free',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-test',
})
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'codex'))
expect(result.workspaceDir).toBe(join(home, 'coding-agent', 'workspace', 'default', 'openrouter'))
expect(result.args).toEqual(['--model', 'openai/gpt-oss-20b:free'])
expect(result.env).toEqual({ CODEX_HOME: result.rootDir })
const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')
expect(config).toContain('requires_openai_auth = false')
expect(config).toContain(`model_catalog_json = "${join(result.rootDir, 'codex-model-catalog.json')}"`)
expect(config).toContain('model_reasoning_summary = "auto"')
expect(config).toContain('developer_instructions = """')
expect(config).toContain('Hermes Studio MCP usage')
expect(config).toContain('# 输出格式规范')
expect(config).toContain('[mcp_servers.hermes-studio-api]')
expect(config).toContain('[mcp_servers.hermes-studio-devices]')
expect(config).toContain('[mcp_servers.hermes-studio-use]')
expect(config).toContain(`command = "${process.execPath}"`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-studio-mcp.mjs')}", "api"]`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-studio-mcp.mjs')}", "devices"]`)
expect(config).toContain(`args = ["${join(process.cwd(), 'bin/hermes-studio-mcp.mjs')}", "use"]`)
expect(config).toContain(`env = { HERMES_WEB_UI_URL = "http://127.0.0.1:8648", HERMES_WEB_UI_HOME = "${home}"`)
expect(config).toContain('HERMES_WEBUI_STATE_DIR = "')
expect(config).toContain('HERMES_WEB_UI_PROFILE = "default"')
expect(config).toContain('HERMES_MCP_SERVER_NAME = "hermes-studio-api"')
expect(config).toContain('HERMES_MCP_SERVER_NAME = "hermes-studio-devices"')
expect(config).toContain('HERMES_MCP_SERVER_NAME = "hermes-studio-use"')
expect(config).toContain('HERMES_MCP_TOOLSET = "api"')
expect(config).toContain('HERMES_MCP_TOOLSET = "devices"')
expect(config).toContain('HERMES_MCP_TOOLSET = "use"')
expect(config).toContain('HERMES_WEB_UI_MANAGED_MCP = "1"')
expect(result.files.some(file => file.key === 'agents')).toBe(false)
const catalog = JSON.parse(readFileSync(join(result.rootDir, 'codex-model-catalog.json'), 'utf-8'))
expect(catalog.models.some((entry: any) => entry.slug === 'openai/gpt-oss-20b:free')).toBe(true)
expect(catalog.models[0]).toHaveProperty('base_instructions')
expect(catalog.models[0]).toHaveProperty('model_messages')
expect(catalog.models[0]).toHaveProperty('default_reasoning_summary', 'auto')
})
it('points Codex Chat Completions providers at the local Responses proxy', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'deepseek',
model: 'deepseek-v4-pro',
baseUrl: 'https://api.deepseek.com',
apiKey: 'sk-upstream',
apiMode: 'chat_completions',
})
const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')
expect(config).toContain(`base_url = "http://127.0.0.1:8648/api/codex-proxy/`)
expect(config).toContain('wire_api = "responses"')
expect(config).toContain('requires_openai_auth = false')
expect(config).toMatch(/experimental_bearer_token = "hwui_[^"]+"/)
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'deepseek', 'codex'))
const catalog = JSON.parse(readFileSync(join(result.rootDir, 'codex-model-catalog.json'), 'utf-8'))
const deepseekModel = catalog.models.find((entry: any) => entry.slug === 'deepseek-v4-pro')
expect(deepseekModel).toMatchObject({
display_name: 'Deepseek V4 Pro',
})
expect(deepseekModel.context_window).toBeGreaterThan(0)
expect(deepseekModel.max_context_window).toBe(deepseekModel.context_window)
expect(deepseekModel.model_messages.instructions_template).toContain('{{ base_instructions }}')
})
it('normalizes Codex app-server provider mode to Responses for scoped Codex runs', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'ai-pixel.online',
model: 'gpt-5.5',
baseUrl: 'https://ai-pixel.online/v1',
apiKey: 'sk-upstream',
apiMode: 'codex_app_server' as any,
})
const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')
expect(config).toContain(`base_url = "http://127.0.0.1:`)
expect(config).toContain('/api/codex-proxy/')
expect(config).toContain('wire_api = "responses"')
expect(config).toContain('requires_openai_auth = false')
expect(config).toMatch(/experimental_bearer_token = "hwui_[^"]+"/)
expect(config).not.toContain('base_url = "https://ai-pixel.online/v1"')
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'ai-pixel.online', 'codex'))
})
it('defaults Codex providers without an api mode to Chat Completions', async () => {
makeHome()
const launch = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'xiaomi',
model: 'mimo-v2.5-pro',
baseUrl: 'https://api.xiaomimimo.com/v1',
apiKey: 'sk-upstream',
})
const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')
const routeKey = config.match(/\/api\/codex-proxy\/([^/]+)\/v1/)?.[1] || ''
const token = config.match(/experimental_bearer_token = "([^"]+)"/)?.[1] || ''
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
id: 'chatcmpl_test',
choices: [{
finish_reason: 'stop',
message: { role: 'assistant', content: 'ok' },
}],
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(routeKey, token, {
max_output_tokens: 16,
input: [{ role: 'user', content: [{ type: 'input_text', text: 'hello' }] }],
})
await codexProxyResponses(ctx)
expect(fetchMock).toHaveBeenCalledWith('https://api.xiaomimimo.com/v1/chat/completions', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Bearer sk-upstream' }),
}))
})
it('points Codex Responses providers at the local Responses proxy for stream capture', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'openai-api',
model: 'gpt-5.5',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-upstream',
apiMode: 'codex_responses',
sessionId: 'chat-session-1',
agentSessionId: 'agent-session-1',
})
const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')
expect(config).toContain(`base_url = "http://127.0.0.1:8648/api/codex-proxy/`)
expect(config).toMatch(/experimental_bearer_token = "hwui_[^"]+"/)
expect(config).not.toContain('base_url = "https://api.openai.com/v1"')
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openai-api', 'codex'))
})
it('points Codex Anthropic Messages providers at the local Responses proxy', async () => {
const home = makeHome()
const result = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'anthropic-compatible',
model: 'claude-sonnet-4-6',
baseUrl: 'https://api.example.com',
apiKey: 'sk-upstream',
apiMode: 'anthropic_messages',
})
const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')
expect(config).toContain(`base_url = "http://127.0.0.1:8648/api/codex-proxy/`)
expect(config).toContain('wire_api = "responses"')
expect(config).toContain('requires_openai_auth = false')
expect(config).toMatch(/experimental_bearer_token = "hwui_[^"]+"/)
expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'anthropic-compatible', 'codex'))
})
it('adapts Codex Responses requests to OpenAI Chat Completions', async () => {
makeHome()
const launch = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'deepseek',
model: 'deepseek-v4-pro',
baseUrl: 'https://api.deepseek.com',
apiKey: 'sk-upstream',
apiMode: 'chat_completions',
})
const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')
const routeKey = config.match(/\/api\/codex-proxy\/([^/]+)\/v1/)?.[1] || ''
const token = config.match(/experimental_bearer_token = "([^"]+)"/)?.[1] || ''
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
id: 'chatcmpl_test',
choices: [{
finish_reason: 'stop',
message: { role: 'assistant', content: 'ok' },
}],
usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 },
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(routeKey, token, {
max_output_tokens: 16,
input: [
{ role: 'user', content: [{ type: 'input_text', text: 'hello' }] },
{ role: 'developer', content: [{ type: 'input_text', text: 'be terse' }] },
],
})
await codexProxyResponses(ctx)
expect(fetchMock).toHaveBeenCalledWith('https://api.deepseek.com/v1/chat/completions', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Bearer sk-upstream' }),
}))
const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)
expect(requestBody).toMatchObject({
model: 'deepseek-v4-pro',
max_tokens: 16,
messages: [
{ role: 'user', content: 'hello' },
{ role: 'system', content: 'be terse' },
],
})
expect(ctx.body.output[0].content[0].text).toBe('ok')
expect(ctx.body.usage).toMatchObject({ input_tokens: 3, output_tokens: 1, total_tokens: 4 })
})
it('adapts Codex Responses requests to Anthropic Messages', async () => {
makeHome()
const launch = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'anthropic-compatible',
model: 'claude-sonnet-4-6',
baseUrl: 'https://api.example.com',
apiKey: 'sk-upstream',
apiMode: 'anthropic_messages',
})
const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')
const routeKey = config.match(/\/api\/codex-proxy\/([^/]+)\/v1/)?.[1] || ''
const token = config.match(/experimental_bearer_token = "([^"]+)"/)?.[1] || ''
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
id: 'msg_test',
type: 'message',
role: 'assistant',
model: 'claude-sonnet-4-6',
content: [
{ type: 'text', text: 'ok' },
{ type: 'tool_use', id: 'toolu_1', name: 'search', input: { query: 'repo' } },
],
stop_reason: 'tool_use',
usage: { input_tokens: 5, output_tokens: 2 },
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(routeKey, token, {
instructions: 'be terse',
max_output_tokens: 64,
input: [
{ role: 'user', content: [{ type: 'input_text', text: 'hello' }] },
{ type: 'function_call_output', call_id: 'call_0', output: 'done' },
],
tools: [{
type: 'function',
name: 'search',
description: 'Search files',
parameters: { type: 'object', properties: { query: { type: 'string' } } },
}],
})
await codexProxyResponses(ctx)
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/v1/messages', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer sk-upstream',
'x-api-key': 'sk-upstream',
'anthropic-version': '2023-06-01',
}),
}))
const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)
expect(requestBody).toMatchObject({
model: 'claude-sonnet-4-6',
system: 'be terse',
max_tokens: 64,
messages: [
{ role: 'user', content: [{ type: 'text', text: 'hello' }] },
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_0', content: 'done' }] },
],
tools: [{
name: 'search',
description: 'Search files',
input_schema: { type: 'object', properties: { query: { type: 'string' } } },
}],
})
expect(ctx.body.output[0].content[0].text).toBe('ok')
expect(ctx.body.output[1]).toMatchObject({
type: 'function_call',
call_id: 'toolu_1',
name: 'search',
arguments: '{"query":"repo"}',
})
expect(ctx.body.usage).toMatchObject({ input_tokens: 5, output_tokens: 2, total_tokens: 7 })
})
it('streams Codex proxy text as complete Responses message events', async () => {
makeHome()
const launch = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'deepseek',
model: 'deepseek-v4-pro',
baseUrl: 'https://api.deepseek.com',
apiKey: 'sk-upstream',
apiMode: 'chat_completions',
})
const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')
const routeKey = config.match(/\/api\/codex-proxy\/([^/]+)\/v1/)?.[1] || ''
const token = config.match(/experimental_bearer_token = "([^"]+)"/)?.[1] || ''
const encoder = new TextEncoder()
const fetchMock = vi.fn(async () => new Response(new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('data: {"choices":[{"delta":{"content":"p"}}]}\n\n'))
controller.enqueue(encoder.encode('data: {"choices":[{"delta":{"content":"ong"}}]}\n\n'))
controller.enqueue(encoder.encode('data: {"choices":[],"usage":{"prompt_tokens":12,"completion_tokens":3,"total_tokens":15}}\n\n'))
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
controller.close()
},
}), { status: 200, headers: { 'Content-Type': 'text/event-stream' } }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(routeKey, token, {
stream: true,
input: [{ role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],
})
await codexProxyResponses(ctx)
const chunks: string[] = []
for await (const chunk of ctx.body) chunks.push(String(chunk))
const sse = chunks.join('')
expect(sse).toContain('event: response.output_item.added')
expect(sse).toContain('event: response.content_part.added')
expect(sse).toContain('"delta":"p"')
expect(sse).toContain('"delta":"ong"')
expect(sse).toContain('event: response.output_text.done')
expect(sse).toContain('"text":"pong"')
expect(sse).toContain('event: response.output_item.done')
expect(sse).toContain('"output":[{"type":"message"')
expect(sse).not.toContain('"usage"')
})
it('streams Codex proxy Anthropic text as Responses message events', async () => {
makeHome()
const launch = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'anthropic-compatible',
model: 'claude-sonnet-4-6',
baseUrl: 'https://api.example.com',
apiKey: 'sk-upstream',
apiMode: 'anthropic_messages',
})
const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')
const routeKey = config.match(/\/api\/codex-proxy\/([^/]+)\/v1/)?.[1] || ''
const token = config.match(/experimental_bearer_token = "([^"]+)"/)?.[1] || ''
const encoder = new TextEncoder()
const fetchMock = vi.fn(async () => new Response(new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('event: message_start\ndata: {"type":"message_start","message":{"id":"msg_test","usage":{"input_tokens":3,"output_tokens":0}}}\n\n'))
controller.enqueue(encoder.encode('event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n'))
controller.enqueue(encoder.encode('event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"he"}}\n\n'))
controller.enqueue(encoder.encode('event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"llo"}}\n\n'))
controller.enqueue(encoder.encode('event: message_delta\ndata: {"type":"message_delta","usage":{"output_tokens":2}}\n\n'))
controller.enqueue(encoder.encode('event: message_stop\ndata: {"type":"message_stop"}\n\n'))
controller.close()
},
}), { status: 200, headers: { 'Content-Type': 'text/event-stream' } }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(routeKey, token, {
stream: true,
input: [{ role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],
})
await codexProxyResponses(ctx)
const chunks: string[] = []
for await (const chunk of ctx.body) chunks.push(String(chunk))
const sse = chunks.join('')
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/v1/messages', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ 'anthropic-version': '2023-06-01' }),
}))
expect(sse).toContain('event: response.output_item.added')
expect(sse).toContain('"delta":"he"')
expect(sse).toContain('"delta":"llo"')
expect(sse).toContain('event: response.output_text.done')
expect(sse).toContain('"text":"hello"')
expect(sse).toContain('event: response.completed')
expect(sse).not.toContain('"usage"')
})
it('preserves native Responses usage for Codex Responses providers', async () => {
const target = registerCodexProxyTarget({
profile: 'default',
provider: 'openai-api',
model: 'gpt-5.5',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-upstream',
apiMode: 'codex_responses',
})
const encoder = new TextEncoder()
const fetchMock = vi.fn(async () => new Response(new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('event: response.created\ndata: {"type":"response.created","response":{"id":"resp_native","status":"in_progress"}}\n\n'))
controller.enqueue(encoder.encode('event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"ok"}\n\n'))
controller.enqueue(encoder.encode('event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_native","status":"completed","usage":{"input_tokens":11,"output_tokens":2,"total_tokens":13}}}\n\n'))
controller.close()
},
}), { status: 200, headers: { 'Content-Type': 'text/event-stream' } }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(target.routeKey, target.token, {
stream: true,
input: [{ role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],
})
await codexProxyResponses(ctx)
const chunks: string[] = []
for await (const chunk of ctx.body) chunks.push(String(chunk))
const sse = chunks.join('')
expect(fetchMock).toHaveBeenCalledWith('https://api.openai.com/v1/responses', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Bearer sk-upstream' }),
}))
expect(sse).toContain('"usage":{"input_tokens":11,"output_tokens":2,"total_tokens":13}')
})
it('exposes Codex proxy models with route-token authentication', async () => {
makeHome()
const launch = await prepareCodingAgentLaunch('codex', {
profile: 'default',
provider: 'deepseek',
model: 'deepseek-v4-pro',
baseUrl: 'https://api.deepseek.com',
apiKey: 'sk-upstream',
apiMode: 'chat_completions',
})
const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')
const routeKey = config.match(/\/api\/codex-proxy\/([^/]+)\/v1/)?.[1] || ''
const token = config.match(/experimental_bearer_token = "([^"]+)"/)?.[1] || ''
const ctx = makeProxyContext(routeKey, token, {})
await codexProxyModels(ctx)
expect(ctx.body).toMatchObject({
object: 'list',
data: [{ id: 'deepseek-v4-pro', object: 'model', owned_by: 'deepseek' }],
})
})
it('adapts Claude Code streaming requests to the Responses API for codex_responses providers', async () => {
const target = registerClaudeCodeProxyTarget({
provider: 'fun-codex',
model: 'gpt-5.5',
baseUrl: 'https://api.apikey.fun/v1',
apiKey: 'sk-upstream',
apiMode: 'codex_responses',
})
const encoder = new TextEncoder()
const fetchMock = vi.fn(async () => new Response(new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('data: {"type":"response.output_text.delta","delta":"hi"}\n\n'))
controller.enqueue(encoder.encode('data: {"type":"response.completed","response":{"status":"completed","usage":{"output_tokens":1}}}\n\n'))
controller.close()
},
}), { status: 200 }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(target.routeKey, target.token, {
stream: true,
max_tokens: 32,
messages: [{ role: 'user', content: 'hello' }],
})
await claudeProxyMessages(ctx)
expect(fetchMock).toHaveBeenCalledWith('https://api.apikey.fun/v1/responses', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Bearer sk-upstream' }),
}))
const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)
expect(requestBody).toMatchObject({
model: 'gpt-5.5',
stream: true,
store: false,
max_output_tokens: 32,
input: [{ role: 'user', content: 'hello' }],
})
const chunks: string[] = []
for await (const chunk of ctx.body) chunks.push(String(chunk))
const sse = chunks.join('')
expect(ctx.responseHeaders['Content-Type']).toContain('text/event-stream')
expect(sse).toContain('event: message_start')
expect(sse).toContain('"type":"text_delta","text":"hi"')
expect(sse).toContain('event: message_stop')
})
it('round-trips reasoning_content for DeepSeek-style OpenAI Chat tool calls', async () => {
const target = registerClaudeCodeProxyTarget({
provider: 'deepseek',
model: 'deepseek-reasoner',
baseUrl: 'https://api.deepseek.com/v1',
apiKey: 'sk-upstream',
apiMode: 'chat_completions',
})
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
id: 'chatcmpl_test',
choices: [{
finish_reason: 'tool_calls',
message: {
role: 'assistant',
reasoning_content: 'Need to inspect the repository first.',
content: null,
tool_calls: [{
id: 'call_2',
type: 'function',
function: { name: 'search', arguments: '{"query":"proxy"}' },
}],
},
}],
usage: { prompt_tokens: 12, completion_tokens: 8 },
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(target.routeKey, target.token, {
max_tokens: 32,
messages: [
{ role: 'user', content: 'check it' },
{
role: 'assistant',
content: [
{ type: 'thinking', thinking: 'Need the current repo files.' },
{ type: 'tool_use', id: 'call_1', name: 'search', input: { query: 'reasoning_content' } },
],
},
{
role: 'user',
content: [
{ type: 'tool_result', tool_use_id: 'call_1', content: 'found one file' },
],
},
],
})
await claudeProxyMessages(ctx)
const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)
expect(requestBody.messages[1]).toMatchObject({
role: 'assistant',
reasoning_content: 'Need the current repo files.',
tool_calls: [{
id: 'call_1',
type: 'function',
function: { name: 'search', arguments: '{"query":"reasoning_content"}' },
}],
})
expect(ctx.body.content[0]).toEqual({
type: 'thinking',
thinking: 'Need to inspect the repository first.',
})
expect(ctx.body.content[1]).toMatchObject({
type: 'tool_use',
id: 'call_2',
name: 'search',
input: { query: 'proxy' },
})
})
it('passes Anthropic Messages providers through the local proxy without exposing upstream credentials', async () => {
const target = registerClaudeCodeProxyTarget({
provider: 'fun-claude',
model: 'claude-sonnet-4-6',
baseUrl: 'https://api.apikey.fun',
apiKey: 'sk-upstream',
apiMode: 'anthropic_messages',
})
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
id: 'msg_test',
type: 'message',
role: 'assistant',
model: 'claude-sonnet-4-6',
content: [{ type: 'text', text: 'hi' }],
stop_reason: 'end_turn',
usage: { input_tokens: 1, output_tokens: 1 },
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
vi.stubGlobal('fetch', fetchMock)
const ctx = makeProxyContext(target.routeKey, target.token, {
model: 'ignored-client-model',
max_tokens: 32,
messages: [{ role: 'user', content: 'hello' }],
})
await claudeProxyMessages(ctx)
expect(fetchMock).toHaveBeenCalledWith('https://api.apikey.fun/v1/messages', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer sk-upstream',
'x-api-key': 'sk-upstream',
}),
}))
const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)
expect(requestBody.model).toBe('claude-sonnet-4-6')
expect(ctx.body.content[0].text).toBe('hi')
})
it('keeps Claude proxy routes separate for the same model with different protocols', () => {
const chat = registerClaudeCodeProxyTarget({
provider: 'same-provider',
model: 'same-model',
baseUrl: 'https://api.example.com/v1',
apiKey: 'sk-chat',
apiMode: 'chat_completions',
})
const anthropic = registerClaudeCodeProxyTarget({
provider: 'same-provider',
model: 'same-model',
baseUrl: 'https://api.example.com/v1',
apiKey: 'sk-anthropic',
apiMode: 'anthropic_messages',
})
expect(chat.routeKey).not.toBe(anthropic.routeKey)
expect(chat.token).not.toBe(anthropic.token)
})
it('keeps proxy routes separate for different hidden agent sessions', () => {
const first = registerClaudeCodeProxyTarget({
provider: 'same-provider',
model: 'same-model',
baseUrl: 'https://api.example.com/v1',
apiKey: 'sk-one',
apiMode: 'chat_completions',
agentSessionId: 'agent-one',
chatSessionId: 'chat-one',
})
const second = registerClaudeCodeProxyTarget({
provider: 'same-provider',
model: 'same-model',
baseUrl: 'https://api.example.com/v1',
apiKey: 'sk-two',
apiMode: 'chat_completions',
agentSessionId: 'agent-two',
chatSessionId: 'chat-two',
})
expect(first.routeKey).not.toBe(second.routeKey)
expect(first.token).not.toBe(second.token)
})
it('keeps Codex proxy routes separate for the same model with different upstream URLs', () => {
const first = registerCodexProxyTarget({
profile: 'default',
provider: 'same-provider',
model: 'same-model',
baseUrl: 'https://api-one.example.com/v1',
apiKey: 'sk-one',
apiMode: 'chat_completions',
})
const second = registerCodexProxyTarget({
profile: 'default',
provider: 'same-provider',
model: 'same-model',
baseUrl: 'https://api-two.example.com/v1',
apiKey: 'sk-two',
apiMode: 'chat_completions',
})
expect(first.routeKey).not.toBe(second.routeKey)
expect(first.token).not.toBe(second.token)
})
it('exposes Claude-visible alias models from the local proxy models endpoint', async () => {
const target = registerClaudeCodeProxyTarget({
provider: 'openrouter',
model: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
baseUrl: 'https://openrouter.ai/api/v1',
apiKey: 'sk-upstream',
apiMode: 'codex_responses',
})
const ctx = makeProxyContext(target.routeKey, target.token, {})
await claudeProxyModels(ctx)
const ids = ctx.body.data.map((model: any) => model.id)
expect(ids).toContain('claude-haiku-4-5')
expect(ids).toContain('claude-sonnet-4-6')
expect(ids).toContain('claude-opus-4-7')
expect(ids).toContain('cognitivecomputations/dolphin-mistral-24b-venice-edition:free')
})
})