Files
claude-code-haha/src/server/__tests__/providers.test.ts
T
程序员阿江(Relakkes) ccc48df0e0 feat(api): align Codex request routing and tool payloads
Preserve optional function arguments with explicit non-strict schemas.
Send stable session and thread identities, retain routing state through
one user turn and dispose it on completion or cancellation.

Compress Codex OAuth request bodies with zstd when supported, retaining
readable request audit data and a local compatibility opt-out without
replaying submitted requests.

Validation: 352 focused offline API/provider tests passed. The real query
loop regression covers tool continuation, next-turn reset and cleanup.
Selected repository verification passed with mocked transports.
2026-09-10 20:58:55 +08:00

3350 lines
127 KiB
TypeScript

/**
* Unit tests for ProviderService and Providers REST API
*/
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'
import { ProviderService } from '../services/providerService.js'
import { handleProvidersApi } from '../api/providers.js'
import { handleProxyRequest } from '../proxy/handler.js'
import {
clearTraceCaptureStateForTests,
drainTraceCaptureForTests,
setTraceAppendBeforeWriteHookForTests,
traceCaptureService,
} from '../services/traceCaptureService.js'
import type { CreateProviderInput } from '../types/provider.js'
import { buildComputerUseTools } from '../../vendor/computer-use-mcp/tools.js'
// ─── Test helpers ─────────────────────────────────────────────────────────────
let tmpDir: string
let originalConfigDir: string | undefined
let originalHome: string | undefined
async function setup() {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'provider-test-'))
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
originalHome = process.env.HOME
process.env.CLAUDE_CONFIG_DIR = tmpDir
process.env.HOME = tmpDir
clearTraceCaptureStateForTests()
}
async function teardown() {
await drainTraceCaptureForTests()
clearTraceCaptureStateForTests()
if (originalConfigDir !== undefined) {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
} else {
delete process.env.CLAUDE_CONFIG_DIR
}
if (originalHome !== undefined) {
process.env.HOME = originalHome
} else {
delete process.env.HOME
}
// The background trace projection may still hold a handle briefly (first
// index builds are slower); retry the removal instead of failing the test
// on Windows.
for (let attempt = 0; attempt < 60; attempt++) {
try {
await fs.rm(tmpDir, { recursive: true, force: true })
return
} catch {
await new Promise(resolve => setTimeout(resolve, 25))
}
}
await fs.rm(tmpDir, { recursive: true, force: true })
}
/** Create a mock Request */
function makeRequest(
method: string,
urlStr: string,
body?: Record<string, unknown>,
): { req: Request; url: URL; segments: string[] } {
const url = new URL(urlStr, 'http://localhost:3456')
const init: RequestInit = { method }
if (body) {
init.headers = { 'Content-Type': 'application/json' }
init.body = JSON.stringify(body)
}
const req = new Request(url.toString(), init)
const segments = url.pathname.split('/').filter(Boolean)
return { req, url, segments }
}
/** A sample provider input for reuse across tests */
function sampleInput(overrides?: Partial<CreateProviderInput>): CreateProviderInput {
return {
presetId: 'custom',
name: 'Test Provider',
baseUrl: 'https://api.example.com',
apiKey: 'sk-test-key-123',
apiFormat: 'anthropic',
models: {
main: 'model-main',
haiku: 'model-haiku',
sonnet: 'model-sonnet',
opus: 'model-opus',
},
...overrides,
}
}
/** Read the settings.json written to the temp config dir */
async function readSettings(): Promise<Record<string, unknown>> {
const raw = await fs.readFile(path.join(tmpDir, 'cc-haha', 'settings.json'), 'utf-8')
return JSON.parse(raw) as Record<string, unknown>
}
/** Read the providers.json written to the temp config dir */
async function readProvidersConfig(): Promise<Record<string, unknown>> {
const raw = await fs.readFile(path.join(tmpDir, 'cc-haha', 'providers.json'), 'utf-8')
return JSON.parse(raw) as Record<string, unknown>
}
async function waitForCompletedProxyTrace(sessionId: string) {
for (let attempt = 0; attempt < 100; attempt += 1) {
const trace = await traceCaptureService.getSessionTrace(sessionId)
if (
trace.calls.some((call) => call.response) &&
trace.events.some((event) => event.phase === 'upstream_fetch_completed')
) {
return trace
}
await new Promise((resolve) => setTimeout(resolve, 5))
}
return traceCaptureService.getSessionTrace(sessionId)
}
function blockNextTraceAppend() {
let releaseWrite: () => void = () => {}
const blockedWrite = new Promise<void>((resolve) => {
releaseWrite = resolve
})
let signalBlocked: () => void = () => {}
const writeBlocked = new Promise<void>((resolve) => {
signalBlocked = resolve
})
setTraceAppendBeforeWriteHookForTests(async () => {
setTraceAppendBeforeWriteHookForTests(null)
signalBlocked()
await blockedWrite
})
return { releaseWrite, writeBlocked }
}
async function settlesBeforeBlockedTraceWrite<T>(promise: Promise<T>): Promise<T | null> {
return Promise.race([
promise,
// The trace hook is already blocked, so this timeout is only a failure
// bound. Keep it generous enough that a busy CI worker cannot masquerade
// as response/trace coupling.
new Promise<null>((resolve) => setTimeout(() => resolve(null), 1_000)),
])
}
async function captureOpenAIChatRequest(options: {
contentSource?: 'user' | 'tool'
baseUrl: string
model: string
content: Array<Record<string, unknown>>
}): Promise<Record<string, unknown>> {
const originalFetch = globalThis.fetch
const calls: Array<{ body: Record<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
return new Response(JSON.stringify({
id: 'chatcmpl-computer-use',
object: 'chat.completion',
created: 0,
model: options.model,
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
apiFormat: 'openai_chat',
baseUrl: options.baseUrl,
models: {
main: options.model,
haiku: options.model,
sonnet: options.model,
opus: options.model,
},
}))
await svc.activateProvider(provider.id)
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: options.model,
max_tokens: 64,
messages: [{
role: 'user',
content: options.contentSource === 'user' ? options.content : [{
type: 'tool_result',
tool_use_id: 'computer_1',
content: options.content,
}],
}],
}),
})
const res = await handleProxyRequest(req, new URL(req.url))
expect(res.status).toBe(200)
expect(calls).toHaveLength(1)
return calls[0].body
} finally {
globalThis.fetch = originalFetch
}
}
// =============================================================================
// ProviderService
// =============================================================================
describe('ProviderService', () => {
beforeEach(setup)
afterEach(teardown)
// ─── listProviders ───────────────────────────────────────────────────────
describe('listProviders', () => {
test('should return empty array when no providers exist', async () => {
const svc = new ProviderService()
const result = await svc.listProviders()
expect(result).toEqual({
providers: [],
activeId: null,
providerOrder: ['claude-official', 'openai-official', 'grok-official'],
})
})
test('should recover from a malformed providers index after an upgrade', async () => {
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(path.join(tmpDir, 'cc-haha', 'providers.json'), '{not json', 'utf-8')
const svc = new ProviderService()
const result = await svc.listProviders()
const files = await fs.readdir(path.join(tmpDir, 'cc-haha'))
expect(result).toEqual({
providers: [],
activeId: null,
providerOrder: ['claude-official', 'openai-official', 'grok-official'],
})
expect(files.some((name) => name.startsWith('providers.json.invalid-'))).toBe(true)
})
test('should normalize a legacy activeProviderId field', async () => {
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
const provider = {
id: 'legacy-provider',
...sampleInput({ name: 'Legacy Provider' }),
}
await fs.writeFile(
path.join(tmpDir, 'cc-haha', 'providers.json'),
JSON.stringify({ activeProviderId: provider.id, providers: [provider] }),
'utf-8',
)
const svc = new ProviderService()
const result = await svc.listProviders()
expect(result.activeId).toBe(provider.id)
expect(result.providers).toHaveLength(1)
expect(result.providers[0].name).toBe('Legacy Provider')
})
test('should return all added providers', async () => {
const svc = new ProviderService()
await svc.addProvider(sampleInput({ name: 'Provider A' }))
await svc.addProvider(sampleInput({ name: 'Provider B' }))
const { providers, activeId } = await svc.listProviders()
expect(providers).toHaveLength(2)
expect(providers[0].name).toBe('Provider A')
expect(providers[1].name).toBe('Provider B')
expect(activeId).toBeNull()
})
})
// ─── addProvider ─────────────────────────────────────────────────────────
describe('addProvider', () => {
test('should add a provider and return it with generated fields', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
expect(provider.id).toBeDefined()
expect(provider.name).toBe('Test Provider')
expect(provider.baseUrl).toBe('https://api.example.com')
expect(provider.apiKey).toBe('sk-test-key-123')
expect(provider.models.main).toBe('model-main')
})
test('should normalize empty model mappings to the main model when adding a provider', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
models: {
main: 'gpt-5.5',
haiku: '',
sonnet: ' ',
opus: '',
},
}))
expect(provider.models).toEqual({
main: 'gpt-5.5',
haiku: 'gpt-5.5',
sonnet: 'gpt-5.5',
opus: 'gpt-5.5',
})
const config = await readProvidersConfig()
expect((config.providers as Array<{ models: unknown }>)[0]?.models).toEqual(provider.models)
})
test('new providers should not be auto-activated', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
expect(provider.id).toBeDefined()
const { activeId } = await svc.listProviders()
expect(activeId).toBeNull()
})
test('adding a provider should not sync settings until activated', async () => {
const svc = new ProviderService()
await svc.addProvider(sampleInput())
await expect(fs.readFile(path.join(tmpDir, 'cc-haha', 'settings.json'), 'utf-8')).rejects.toThrow()
})
test('custom providers keep thinking compatibility without narrowing CLI effort', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
models: {
main: 'deepseek-ai/DeepSeek-V4-Pro',
haiku: 'deepseek-ai/DeepSeek-V4-Pro',
sonnet: 'deepseek-ai/DeepSeek-V4-Pro',
opus: 'deepseek-ai/DeepSeek-V4-Pro',
},
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('deepseek-ai/DeepSeek-V4-Pro')
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
})
test('Xiaomi MiMo custom Anthropic providers keep the compatibility effort fallback', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
name: 'Xiaomi MiMo Custom',
baseUrl: 'https://token-plan-sgp.xiaomimimo.com/anthropic',
models: {
main: 'mimo-v2.5-pro[1m]',
haiku: 'mimo-v2.5-pro[1m]',
sonnet: 'mimo-v2.5-pro[1m]',
opus: 'mimo-v2.5-pro[1m]',
},
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
})
test('custom providers can mark main and role models as 1M-capable', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
models: {
main: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5',
sonnet: 'claude-sonnet-4-6',
opus: 'claude-opus-4-7',
},
model1mSupport: {
main: true,
haiku: false,
sonnet: true,
opus: true,
},
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6[1m]')
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5')
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6[1m]')
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-7[1m]')
const runtimeEnv = await svc.getProviderRuntimeEnv(provider.id)
expect(runtimeEnv.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6[1m]')
expect(runtimeEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5')
expect(runtimeEnv.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6[1m]')
expect(runtimeEnv.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-7[1m]')
})
test('DeepSeek preset follows the global thinking toggle instead of forcing disabled thinking', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
presetId: 'deepseek',
name: 'DeepSeek',
baseUrl: 'https://api.deepseek.com/anthropic',
models: {
main: 'deepseek-v4-pro',
haiku: 'deepseek-v4-flash',
sonnet: 'deepseek-v4-pro',
opus: 'deepseek-v4-pro',
},
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.CC_HAHA_SEND_DISABLED_THINKING).toBeUndefined()
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,xhigh_effort,max_effort',
)
})
test('adding additional providers should keep activeId unchanged', async () => {
const svc = new ProviderService()
await svc.addProvider(sampleInput({ name: 'First' }))
const second = await svc.addProvider(sampleInput({ name: 'Second' }))
expect(second.id).toBeDefined()
const { activeId } = await svc.listProviders()
expect(activeId).toBeNull()
})
test('should preserve optional notes field', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ notes: 'dev environment' }))
expect(provider.notes).toBe('dev environment')
})
test('should preserve optional auto compact window', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ autoCompactWindow: 64000 }))
expect(provider.autoCompactWindow).toBe(64000)
})
test('should preserve optional model context windows', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
modelContextWindows: {
'model-main': 300000,
'model-haiku': 128000,
},
}))
expect(provider.modelContextWindows).toEqual({
'model-main': 300000,
'model-haiku': 128000,
})
})
test('should persist an optional image provider without placing it in the skill', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
imageGeneration: {
model: 'image-model',
baseUrl: 'https://images.example.test/v1',
apiKey: 'image-secret',
},
}))
expect(provider.imageGeneration).toEqual({
model: 'image-model',
baseUrl: 'https://images.example.test/v1',
apiKey: 'image-secret',
})
const config = await readProvidersConfig()
expect((config.providers as Array<{ imageGeneration?: unknown }>)[0]?.imageGeneration)
.toEqual(provider.imageGeneration)
})
})
// ─── getProvider ─────────────────────────────────────────────────────────
describe('getProvider', () => {
test('should return the provider by id', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput())
const fetched = await svc.getProvider(added.id)
expect(fetched.id).toBe(added.id)
expect(fetched.name).toBe(added.name)
})
describe('ChatGPT Official provider metadata', () => {
test('normalizes the built-in ChatGPT provider as an active provider id', async () => {
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(
path.join(tmpDir, 'cc-haha', 'providers.json'),
JSON.stringify({ activeId: 'openai-official', providers: [] }),
'utf-8',
)
const svc = new ProviderService()
const result = await svc.listProviders()
expect(result.activeId).toBe('openai-official')
expect(result.providers).toEqual([])
})
test('returns built-in ChatGPT provider metadata without persisting secrets', async () => {
const svc = new ProviderService()
const provider = await svc.getProvider('openai-official')
expect(provider).toMatchObject({
id: 'openai-official',
presetId: 'openai-official',
name: 'ChatGPT Official',
apiKey: '',
apiFormat: 'openai_responses',
runtimeKind: 'openai_oauth',
models: {
main: 'gpt-5.6-sol',
haiku: 'gpt-5.6-luna',
sonnet: 'gpt-5.6-terra',
opus: 'gpt-5.6-sol',
},
})
})
test('activating ChatGPT Official writes OpenAI OAuth runtime env without Anthropic auth or proxy env', async () => {
const svc = new ProviderService()
await svc.activateProvider('openai-official')
const config = await readProvidersConfig()
const settings = await readSettings()
expect(config.activeId).toBe('openai-official')
const env = settings.env as Record<string, string>
expect(env.CC_HAHA_OPENAI_OAUTH_PROVIDER).toBe('1')
expect(env.OPENAI_CODEX_OAUTH_FILE).toBe(
path.join(tmpDir, 'cc-haha', 'openai-oauth.json'),
)
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.6-sol')
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.6-luna')
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.6-terra')
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.6-sol')
expect(typeof env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS).toBe('string')
expect(JSON.parse(env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)).toEqual({
'gpt-5.6-sol': 353_400,
'gpt-5.6-terra': 353_400,
'gpt-5.6-luna': 353_400,
'gpt-5.3-codex': 258_400,
'gpt-5.4': 950_000,
'gpt-5.5': 258_400,
'gpt-5.4-mini': 258_400,
'gpt-6-astra': 997_500,
})
expect(env.ANTHROPIC_BASE_URL).toBeUndefined()
expect(env.ANTHROPIC_API_KEY).toBeUndefined()
expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
})
test('activating ChatGPT Official clears stale managed provider env', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
apiFormat: 'openai_responses',
baseUrl: 'https://api.example.com/openai',
models: {
main: 'provider-main',
haiku: 'provider-haiku',
sonnet: 'provider-sonnet',
opus: 'provider-opus',
},
}))
await svc.activateProvider(provider.id)
expect(((await readSettings()).env as Record<string, string>).ANTHROPIC_BASE_URL).toContain('/proxy')
await svc.activateProvider('openai-official')
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.CC_HAHA_OPENAI_OAUTH_PROVIDER).toBe('1')
expect(env.OPENAI_CODEX_OAUTH_FILE).toBe(
path.join(tmpDir, 'cc-haha', 'openai-oauth.json'),
)
expect(env.ANTHROPIC_BASE_URL).toBeUndefined()
expect(env.ANTHROPIC_API_KEY).toBeUndefined()
expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
})
test('auth status reports ChatGPT Official from the desktop OpenAI token file', async () => {
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(
path.join(tmpDir, 'cc-haha', 'openai-oauth.json'),
JSON.stringify({
accessToken: 'openai-access',
refreshToken: 'openai-refresh',
expiresAt: Date.now() + 60 * 60_000,
email: 'user@example.com',
accountId: 'acct_123',
}),
'utf-8',
)
const svc = new ProviderService()
await svc.activateProvider('openai-official')
await expect(svc.checkAuthStatus()).resolves.toMatchObject({
hasAuth: true,
source: 'openai-oauth',
activeProvider: 'ChatGPT Official',
})
})
test('auth status reports Claude Official from the desktop Claude token file', async () => {
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(
path.join(tmpDir, 'cc-haha', 'oauth.json'),
JSON.stringify({
accessToken: 'claude-access',
refreshToken: 'claude-refresh',
expiresAt: Date.now() + 60 * 60_000,
scopes: [],
subscriptionType: 'pro',
}),
'utf-8',
)
const svc = new ProviderService()
await expect(svc.checkAuthStatus()).resolves.toMatchObject({
hasAuth: true,
source: 'claude-oauth',
activeProvider: 'Claude Official',
})
})
test('auth status reports ChatGPT Official as unauthenticated when the OpenAI token file is missing', async () => {
const svc = new ProviderService()
await svc.activateProvider('openai-official')
await expect(svc.checkAuthStatus()).resolves.toMatchObject({
hasAuth: false,
source: 'none',
activeProvider: 'ChatGPT Official',
})
})
test('activating another provider clears ChatGPT Official runtime markers', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
await svc.activateProvider('openai-official')
await svc.activateProvider(provider.id)
const env = (await readSettings()).env as Record<string, string>
expect(env.CC_HAHA_OPENAI_OAUTH_PROVIDER).toBeUndefined()
expect(env.OPENAI_CODEX_OAUTH_FILE).toBeUndefined()
expect(env.ANTHROPIC_BASE_URL).toBe('https://api.example.com')
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-test-key-123')
})
})
describe('Grok Official provider metadata', () => {
test('normalizes the built-in Grok provider and appends it to legacy provider order', async () => {
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(
path.join(tmpDir, 'cc-haha', 'providers.json'),
JSON.stringify({
activeId: 'grok-official',
providers: [],
providerOrder: ['claude-official', 'openai-official'],
}),
'utf-8',
)
const svc = new ProviderService()
const result = await svc.listProviders()
expect(result.activeId).toBe('grok-official')
expect(result.providers).toEqual([])
expect(result.providerOrder).toEqual([
'claude-official',
'openai-official',
'grok-official',
])
})
test('returns and activates built-in Grok metadata while clearing OpenAI OAuth runtime env', async () => {
const svc = new ProviderService()
const provider = await svc.getProvider('grok-official')
expect(provider).toMatchObject({
id: 'grok-official',
presetId: 'grok-official',
name: 'Grok Official',
apiKey: '',
apiFormat: 'openai_chat',
runtimeKind: 'grok_oauth',
models: {
main: 'grok-4.6',
haiku: 'grok-4.6',
sonnet: 'grok-4.6',
opus: 'grok-4.6',
},
})
await svc.activateProvider('openai-official')
await svc.activateProvider('grok-official')
const config = await readProvidersConfig()
const env = (await readSettings()).env as Record<string, string>
expect(config.activeId).toBe('grok-official')
expect(env.CC_HAHA_GROK_OAUTH_PROVIDER).toBe('1')
expect(env.GROK_OAUTH_FILE).toBe(
path.join(tmpDir, 'cc-haha', 'grok-oauth.json'),
)
expect(env.ANTHROPIC_MODEL).toBe('grok-4.6')
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('grok-4.6')
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('grok-4.6')
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('grok-4.6')
expect(env.CC_HAHA_OPENAI_OAUTH_PROVIDER).toBeUndefined()
expect(env.OPENAI_CODEX_OAUTH_FILE).toBeUndefined()
})
test('auth status reports Grok Official from the isolated Grok token file', async () => {
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(
path.join(tmpDir, 'cc-haha', 'grok-oauth.json'),
JSON.stringify({
accessToken: 'grok-access',
refreshToken: 'grok-refresh',
expiresAt: Date.now() + 60 * 60_000,
email: 'grok@example.com',
clientId: 'grok-client',
}),
'utf-8',
)
const svc = new ProviderService()
await svc.activateProvider('grok-official')
await expect(svc.checkAuthStatus()).resolves.toMatchObject({
hasAuth: true,
source: 'grok-oauth',
activeProvider: 'Grok Official',
})
})
test('auth status reports Grok Official as unauthenticated without an isolated token file', async () => {
const svc = new ProviderService()
await svc.activateProvider('grok-official')
await expect(svc.checkAuthStatus()).resolves.toMatchObject({
hasAuth: false,
source: 'none',
activeProvider: 'Grok Official',
})
})
})
test('should throw 404 for non-existent id', async () => {
const svc = new ProviderService()
try {
await svc.getProvider('non-existent-id')
expect(true).toBe(false) // should not reach here
} catch (err: unknown) {
const apiErr = err as { statusCode: number }
expect(apiErr.statusCode).toBe(404)
}
})
})
// ─── updateProvider ──────────────────────────────────────────────────────
describe('updateProvider', () => {
test('should update provider fields', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput())
const updated = await svc.updateProvider(added.id, {
name: 'Updated Name',
baseUrl: 'https://new-api.example.com',
})
expect(updated.name).toBe('Updated Name')
expect(updated.baseUrl).toBe('https://new-api.example.com')
// unchanged fields preserved
expect(updated.apiKey).toBe('sk-test-key-123')
})
test('should throw 404 for non-existent provider', async () => {
const svc = new ProviderService()
try {
await svc.updateProvider('non-existent-id', { name: 'X' })
expect(true).toBe(false)
} catch (err: unknown) {
const apiErr = err as { statusCode: number }
expect(apiErr.statusCode).toBe(404)
}
})
test('updating active provider should re-sync settings.json', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput())
await svc.activateProvider(added.id)
await svc.updateProvider(added.id, {
baseUrl: 'https://new-api.example.com',
apiKey: 'sk-new-key',
})
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ANTHROPIC_BASE_URL).toBe('https://new-api.example.com')
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-new-key')
expect(env.ANTHROPIC_API_KEY).toBe('')
expect(env.ANTHROPIC_MODEL).toBe('model-main')
})
test('editing an existing provider persists supportsNestedToolResultMedia and reroutes it through the proxy', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput())
await svc.activateProvider(added.id)
// Default: nested media preserved, direct connection.
let settings = await readSettings()
let env = settings.env as Record<string, string>
expect(env.ANTHROPIC_BASE_URL).toBe('https://api.example.com')
const updated = await svc.updateProvider(added.id, { supportsNestedToolResultMedia: false })
expect(updated.supportsNestedToolResultMedia).toBe(false)
settings = await readSettings()
env = settings.env as Record<string, string>
expect(env.ANTHROPIC_BASE_URL).toContain('127.0.0.1')
expect(env.ANTHROPIC_API_KEY).toBe('proxy-managed')
// Editing back to nested media restores the direct connection.
const reverted = await svc.updateProvider(added.id, { supportsNestedToolResultMedia: true })
expect(reverted.supportsNestedToolResultMedia).toBe(true)
settings = await readSettings()
env = settings.env as Record<string, string>
expect(env.ANTHROPIC_BASE_URL).toBe('https://api.example.com')
})
test('updating active provider should override and clear auto compact window', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput({ autoCompactWindow: 64000 }))
await svc.activateProvider(added.id)
let settings = await readSettings()
let env = settings.env as Record<string, string>
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('64000')
await svc.updateProvider(added.id, { autoCompactWindow: 32000 })
settings = await readSettings()
env = settings.env as Record<string, string>
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('32000')
await svc.updateProvider(added.id, { autoCompactWindow: null })
settings = await readSettings()
env = settings.env as Record<string, string>
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBeUndefined()
})
test('should normalize empty model mappings before syncing settings', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
models: {
main: 'gpt-5.5',
haiku: '',
sonnet: '',
opus: '',
},
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ANTHROPIC_MODEL).toBe('gpt-5.5')
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.5')
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.5')
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.5')
})
test('updating active provider should override and clear model context windows', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput({
modelContextWindows: { 'model-main': 300000 },
}))
await svc.activateProvider(added.id)
let settings = await readSettings()
let env = settings.env as Record<string, string>
expect(JSON.parse(env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)).toEqual({
'model-main': 300000,
})
await svc.updateProvider(added.id, {
modelContextWindows: { 'model-main': 500000 },
})
settings = await readSettings()
env = settings.env as Record<string, string>
expect(JSON.parse(env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)).toEqual({
'model-main': 500000,
})
await svc.updateProvider(added.id, { modelContextWindows: null })
settings = await readSettings()
env = settings.env as Record<string, string>
expect(env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS).toBeUndefined()
})
test('updating an active provider drives image routing in both directions', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput())
await svc.activateProvider(added.id)
await svc.updateProvider(added.id, {
imageGeneration: {
model: 'image-model',
baseUrl: 'https://images.example.test/v1',
apiKey: 'image-secret',
},
})
let settings = await readSettings()
let env = settings.env as Record<string, string>
expect(env).toMatchObject({
CC_HAHA_IMAGE_PROVIDER_KIND: 'openai_images',
CC_HAHA_IMAGE_PROVIDER_ID: added.id,
CC_HAHA_IMAGE_BASE_URL: 'https://images.example.test/v1',
CC_HAHA_IMAGE_API_KEY: 'image-secret',
CC_HAHA_IMAGE_MODEL: 'image-model',
})
const updated = await svc.updateProvider(added.id, { imageGeneration: null })
expect(updated.imageGeneration).toBeUndefined()
settings = await readSettings()
env = settings.env as Record<string, string>
expect(env.CC_HAHA_IMAGE_PROVIDER_KIND).toBeUndefined()
expect(env.CC_HAHA_IMAGE_API_KEY).toBeUndefined()
})
})
// ─── deleteProvider ──────────────────────────────────────────────────────
describe('deleteProvider', () => {
test('should delete an inactive provider', async () => {
const svc = new ProviderService()
await svc.addProvider(sampleInput({ name: 'First' }))
const second = await svc.addProvider(sampleInput({ name: 'Second' }))
// Second is inactive, so deletion should succeed
await svc.deleteProvider(second.id)
const { providers } = await svc.listProviders()
expect(providers).toHaveLength(1)
expect(providers[0].name).toBe('First')
})
test('should throw 409 when deleting an active provider', async () => {
const svc = new ProviderService()
const active = await svc.addProvider(sampleInput())
await svc.activateProvider(active.id)
try {
await svc.deleteProvider(active.id)
expect(true).toBe(false)
} catch (err: unknown) {
const apiErr = err as { statusCode: number }
expect(apiErr.statusCode).toBe(409)
}
})
test('should throw 404 when deleting non-existent provider', async () => {
const svc = new ProviderService()
try {
await svc.deleteProvider('non-existent-id')
expect(true).toBe(false)
} catch (err: unknown) {
const apiErr = err as { statusCode: number }
expect(apiErr.statusCode).toBe(404)
}
})
})
// ─── reorderProviders ────────────────────────────────────────────────────
describe('reorderProviders', () => {
test('should reorder providers to match the given id order and persist it', async () => {
const svc = new ProviderService()
const a = await svc.addProvider(sampleInput({ name: 'A' }))
const b = await svc.addProvider(sampleInput({ name: 'B' }))
const c = await svc.addProvider(sampleInput({ name: 'C' }))
const result = await svc.reorderProviders([c.id, a.id, b.id])
expect(result.providers.map((p) => p.name)).toEqual(['C', 'A', 'B'])
// Persisted order survives a fresh read
const { providers } = await svc.listProviders()
expect(providers.map((p) => p.name)).toEqual(['C', 'A', 'B'])
const config = await readProvidersConfig()
expect((config.providers as Array<{ name: string }>).map((p) => p.name)).toEqual(['C', 'A', 'B'])
})
test('should persist display order including built-in official providers', async () => {
const svc = new ProviderService()
const a = await svc.addProvider(sampleInput({ name: 'A' }))
const b = await svc.addProvider(sampleInput({ name: 'B' }))
const result = await svc.reorderProviders([
'openai-official',
b.id,
'claude-official',
a.id,
'grok-official',
])
expect(result.providerOrder).toEqual([
'openai-official',
b.id,
'claude-official',
a.id,
'grok-official',
])
expect(result.providers.map((p) => p.id)).toEqual([b.id, a.id])
const listed = await svc.listProviders()
expect(listed.providerOrder).toEqual([
'openai-official',
b.id,
'claude-official',
a.id,
'grok-official',
])
const config = await readProvidersConfig()
expect(config.providerOrder).toEqual([
'openai-official',
b.id,
'claude-official',
a.id,
'grok-official',
])
})
test('should not change activeId when reordering', async () => {
const svc = new ProviderService()
const a = await svc.addProvider(sampleInput({ name: 'A' }))
const b = await svc.addProvider(sampleInput({ name: 'B' }))
await svc.activateProvider(a.id)
await svc.reorderProviders([b.id, a.id])
const { activeId } = await svc.listProviders()
expect(activeId).toBe(a.id)
})
test('should throw 400 when orderedIds is missing a provider', async () => {
const svc = new ProviderService()
const a = await svc.addProvider(sampleInput({ name: 'A' }))
await svc.addProvider(sampleInput({ name: 'B' }))
try {
await svc.reorderProviders([a.id])
expect(true).toBe(false)
} catch (err: unknown) {
const apiErr = err as { statusCode: number }
expect(apiErr.statusCode).toBe(400)
}
})
test('should throw 400 when orderedIds contains an unknown id', async () => {
const svc = new ProviderService()
const a = await svc.addProvider(sampleInput({ name: 'A' }))
const b = await svc.addProvider(sampleInput({ name: 'B' }))
try {
await svc.reorderProviders([a.id, b.id, 'ghost-id'])
expect(true).toBe(false)
} catch (err: unknown) {
const apiErr = err as { statusCode: number }
expect(apiErr.statusCode).toBe(400)
}
})
test('should throw 400 when orderedIds contains duplicates', async () => {
const svc = new ProviderService()
const a = await svc.addProvider(sampleInput({ name: 'A' }))
await svc.addProvider(sampleInput({ name: 'B' }))
try {
await svc.reorderProviders([a.id, a.id])
expect(true).toBe(false)
} catch (err: unknown) {
const apiErr = err as { statusCode: number }
expect(apiErr.statusCode).toBe(400)
}
})
})
// ─── activateProvider ────────────────────────────────────────────────────
describe('activateProvider', () => {
test('should activate a provider with a valid model', async () => {
const svc = new ProviderService()
const first = await svc.addProvider(sampleInput({ name: 'First' }))
const second = await svc.addProvider(
sampleInput({
name: 'Second',
baseUrl: 'https://second-api.example.com',
apiKey: 'sk-second-key',
}),
)
await svc.activateProvider(second.id)
// Second should now be active
const { activeId, providers } = await svc.listProviders()
expect(activeId).toBe(second.id)
expect(providers.find((p) => p.id === first.id)).toBeDefined()
expect(providers.find((p) => p.id === second.id)).toBeDefined()
})
test('should write correct settings.json on activation', async () => {
const svc = new ProviderService()
await svc.addProvider(sampleInput({ name: 'First' }))
const second = await svc.addProvider(
sampleInput({
name: 'Second',
baseUrl: 'https://second-api.example.com',
apiKey: 'sk-second-key',
}),
)
await svc.activateProvider(second.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ANTHROPIC_BASE_URL).toBe('https://second-api.example.com')
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-second-key')
expect(env.ANTHROPIC_API_KEY).toBe('')
expect(second.toolSearchEnabled).toBe(false)
expect(env.ENABLE_TOOL_SEARCH).toBe('false')
expect(env.ANTHROPIC_MODEL).toBe('model-main')
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('model-haiku')
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('model-sonnet')
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('model-opus')
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('0')
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBeUndefined()
})
test('should persist explicitly enabled tool search for native Anthropic providers', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
toolSearchEnabled: true,
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ENABLE_TOOL_SEARCH).toBe('true')
const runtimeEnv = await svc.getProviderRuntimeEnv(provider.id)
expect(runtimeEnv.ENABLE_TOOL_SEARCH).toBe('true')
})
test('should persist disabled experimental betas on activation and runtime env', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
disableExperimentalBetas: true,
}))
expect(provider.disableExperimentalBetas).toBe(true)
const config = await readProvidersConfig()
expect((config.providers as Array<Record<string, unknown>>)[0]?.disableExperimentalBetas).toBe(true)
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS).toBe('1')
const runtimeEnv = await svc.getProviderRuntimeEnv(provider.id)
expect(runtimeEnv.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS).toBe('1')
const updated = await svc.updateProvider(provider.id, { disableExperimentalBetas: false })
expect(updated.disableExperimentalBetas).toBeUndefined()
const clearedSettings = await readSettings()
const clearedEnv = clearedSettings.env as Record<string, string>
expect(clearedEnv.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS).toBeUndefined()
const clearedRuntimeEnv = await svc.getProviderRuntimeEnv(provider.id)
expect(clearedRuntimeEnv.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS).toBeUndefined()
})
test('should preserve attribution header for Claude-prefixed provider models', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
models: {
main: 'Claude Sonnet 4.6',
haiku: 'Claude Haiku 4.5',
sonnet: 'Claude Sonnet 4.6',
opus: 'Claude Opus 4.7',
},
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('1')
const runtimeEnv = await svc.getProviderRuntimeEnv(provider.id)
expect(runtimeEnv.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('1')
})
test('should honor provider auth env strategies on activation and runtime env', async () => {
const svc = new ProviderService()
const apiKeyProvider = await svc.addProvider(sampleInput({
apiKey: 'sk-api-key',
authStrategy: 'api_key',
}))
await svc.activateProvider(apiKeyProvider.id)
let env = (await readSettings()).env as Record<string, string>
expect(env.ANTHROPIC_API_KEY).toBe('sk-api-key')
expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
const bearerProvider = await svc.addProvider(sampleInput({
apiKey: 'sk-bearer',
authStrategy: 'auth_token_empty_api_key',
}))
await svc.activateProvider(bearerProvider.id)
env = (await readSettings()).env as Record<string, string>
expect(env.ANTHROPIC_API_KEY).toBe('')
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-bearer')
const dualProvider = await svc.addProvider(sampleInput({
apiKey: 'sk-dual',
authStrategy: 'dual_same_token',
}))
const runtimeEnv = await svc.getProviderRuntimeEnv(dualProvider.id)
expect(runtimeEnv.ANTHROPIC_API_KEY).toBe('sk-dual')
expect(runtimeEnv.ANTHROPIC_AUTH_TOKEN).toBe('sk-dual')
const dummyProvider = await svc.addProvider(sampleInput({
apiKey: '',
authStrategy: 'dual_dummy',
}))
const dummyRuntimeEnv = await svc.getProviderRuntimeEnv(dummyProvider.id)
expect(dummyRuntimeEnv.ANTHROPIC_API_KEY).toBe('dummy')
expect(dummyRuntimeEnv.ANTHROPIC_AUTH_TOKEN).toBe('dummy')
})
test('proxy providers keep transient desktop auth out of persisted settings', async () => {
const originalLocalAccessToken = process.env.CC_HAHA_LOCAL_ACCESS_TOKEN
process.env.CC_HAHA_LOCAL_ACCESS_TOKEN = 'desktop-local-secret'
try {
const svc = new ProviderService()
for (const apiFormat of ['openai_chat', 'openai_responses'] as const) {
const provider = await svc.addProvider(sampleInput({
apiFormat,
authStrategy: 'auth_token',
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ANTHROPIC_API_KEY).toBe('proxy-managed')
expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
expect(env.ENABLE_TOOL_SEARCH).toBeUndefined()
expect(JSON.stringify(settings)).not.toContain('desktop-local-secret')
}
} finally {
if (originalLocalAccessToken === undefined) {
delete process.env.CC_HAHA_LOCAL_ACCESS_TOKEN
} else {
process.env.CC_HAHA_LOCAL_ACCESS_TOKEN = originalLocalAccessToken
}
}
})
test.each([
['xuanshuapi', 'https://www.xuanshuapi.com', 'claude-sonnet-5'],
['fennoai', 'https://api.fenno.ai', 'claude-sonnet-5'],
['qiniuai', 'https://api.qnaigc.com', 'deepseek/deepseek-v4-pro'],
])('keeps legacy %s providers editable and usable after retirement', async (presetId, baseUrl, model) => {
// This is an old on-disk record: auth/context fields were not always persisted.
const legacyProvider = {
id: `saved-${presetId}`,
...sampleInput({ presetId, baseUrl, models: { main: model, haiku: model, sonnet: model, opus: model } }),
}
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(path.join(tmpDir, 'cc-haha', 'providers.json'), JSON.stringify({
providers: [legacyProvider],
activeId: legacyProvider.id,
}))
const svc = new ProviderService()
expect((await svc.listProviders()).providers).toEqual([expect.objectContaining(legacyProvider)])
await svc.updateProvider(legacyProvider.id, { name: 'Renamed saved provider' })
await svc.activateProvider(legacyProvider.id)
const restarted = new ProviderService()
expect(await restarted.getProvider(legacyProvider.id)).toMatchObject({
...legacyProvider,
name: 'Renamed saved provider',
})
expect((await restarted.listProviders()).activeId).toBe(legacyProvider.id)
const runtimeEnv = await restarted.getProviderRuntimeEnv(legacyProvider.id)
const settingsEnv = (await readSettings()).env as Record<string, string>
for (const env of [runtimeEnv, settingsEnv]) {
expect(env).toMatchObject({
ANTHROPIC_BASE_URL: baseUrl,
ANTHROPIC_AUTH_TOKEN: legacyProvider.apiKey,
ANTHROPIC_API_KEY: '',
ANTHROPIC_MODEL: model,
})
expect(JSON.parse(env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)[model]).toBe(1000000)
if (presetId === 'xuanshuapi') {
expect(env.CLAUDE_CODE_SUBAGENT_MODEL).toBe('claude-sonnet-5')
}
}
})
test('should include preset default env on activation and runtime env', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
presetId: 'shengsuanyun',
baseUrl: 'https://router.shengsuanyun.com/api',
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.API_TIMEOUT_MS).toBe('3000000')
expect(env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC).toBe('1')
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBeUndefined()
expect(JSON.parse(env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)).toEqual({
'anthropic/claude-sonnet-4.6': 1000000,
'anthropic/claude-haiku-4.5:thinking': 200000,
'anthropic/claude-opus-4.7': 1000000,
})
const runtimeEnv = await svc.getProviderRuntimeEnv(provider.id)
expect(runtimeEnv.API_TIMEOUT_MS).toBe('3000000')
expect(runtimeEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC).toBe('1')
expect(runtimeEnv.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBeUndefined()
expect(JSON.parse(runtimeEnv.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)).toEqual({
'anthropic/claude-sonnet-4.6': 1000000,
'anthropic/claude-haiku-4.5:thinking': 200000,
'anthropic/claude-opus-4.7': 1000000,
})
await svc.activateOfficial()
const clearedSettings = await readSettings()
const clearedEnv = (clearedSettings.env as Record<string, string> | undefined) ?? {}
expect(clearedEnv.API_TIMEOUT_MS).toBeUndefined()
expect(clearedEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC).toBeUndefined()
expect(clearedEnv.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBeUndefined()
expect(clearedEnv.CLAUDE_CODE_ATTRIBUTION_HEADER).toBeUndefined()
expect(clearedEnv.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS).toBeUndefined()
})
test('auth status treats preset default auth as active provider auth', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
presetId: 'lmstudio',
apiKey: '',
authStrategy: 'auth_token_empty_api_key',
models: {
main: 'lmstudio-model',
haiku: 'lmstudio-model',
sonnet: 'lmstudio-model',
opus: 'lmstudio-model',
},
}))
await svc.activateProvider(provider.id)
const status = await svc.checkAuthStatus()
expect(status).toEqual({
hasAuth: true,
source: 'cc-haha-provider',
activeProvider: provider.name,
})
})
test('auth status treats dummy proxy auth as active provider auth', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
apiKey: '',
apiFormat: 'openai_chat',
}))
await svc.activateProvider(provider.id)
const status = await svc.checkAuthStatus()
expect(status).toEqual({
hasAuth: true,
source: 'cc-haha-provider',
activeProvider: provider.name,
})
})
test('provider auto compact window should override preset default env on activation and runtime env', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
presetId: 'custom',
autoCompactWindow: 32000,
}))
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('32000')
const runtimeEnv = await svc.getProviderRuntimeEnv(provider.id)
expect(runtimeEnv.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('32000')
})
test('should preserve existing settings.json fields on activation', async () => {
// Pre-seed settings with an extra field
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(
path.join(tmpDir, 'cc-haha', 'settings.json'),
JSON.stringify({ theme: 'dark', env: { CUSTOM_VAR: 'keep-me' } }),
)
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
// Re-activate to verify merge behavior
await svc.activateProvider(provider.id)
const settings = await readSettings()
expect(settings.theme).toBe('dark')
const env = settings.env as Record<string, string>
expect(env.CUSTOM_VAR).toBe('keep-me')
expect(env.ANTHROPIC_BASE_URL).toBe('https://api.example.com')
})
test('should recover malformed managed settings before activation sync', async () => {
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
await fs.writeFile(path.join(tmpDir, 'cc-haha', 'settings.json'), '{not json', 'utf-8')
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
await svc.activateProvider(provider.id)
const settings = await readSettings()
const env = settings.env as Record<string, string>
const files = await fs.readdir(path.join(tmpDir, 'cc-haha'))
expect(env.ANTHROPIC_BASE_URL).toBe('https://api.example.com')
expect(files.some((name) => name.startsWith('settings.json.invalid-'))).toBe(true)
})
test('should throw 404 for non-existent provider id', async () => {
const svc = new ProviderService()
try {
await svc.activateProvider('non-existent-id')
expect(true).toBe(false)
} catch (err: unknown) {
const apiErr = err as { statusCode: number }
expect(apiErr.statusCode).toBe(404)
}
})
test('activeId should be persisted in providers.json', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
await svc.activateProvider(provider.id)
const config = await readProvidersConfig()
expect(config.activeId).toBe(provider.id)
})
})
// ─── getProviderForProxy ─────────────────────────────────────────────────
describe('getProviderForProxy', () => {
test('should return null when no provider is active', async () => {
const svc = new ProviderService()
const active = await svc.getProviderForProxy()
expect(active).toBeNull()
})
test('should return null for explicit ChatGPT Official proxy lookup', async () => {
const svc = new ProviderService()
const active = await svc.getProviderForProxy('openai-official')
expect(active).toBeNull()
})
test('should return null for explicit Grok Official proxy lookup', async () => {
const svc = new ProviderService()
const active = await svc.getProviderForProxy('grok-official')
expect(active).toBeNull()
})
test('should return the active provider proxy config', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
await svc.activateProvider(provider.id)
const active = await svc.getProviderForProxy()
expect(active).not.toBeNull()
expect(active!.baseUrl).toBe(provider.baseUrl)
expect(active!.apiKey).toBe(provider.apiKey)
expect(active!.apiFormat).toBe('anthropic')
})
test('should resolve preset default auth for a no-key proxy provider', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
presetId: 'lmstudio',
apiKey: '',
apiFormat: 'anthropic',
supportsNestedToolResultMedia: false,
}))
const config = await svc.getProviderForProxy(provider.id)
expect(config?.apiKey).toBe('lmstudio')
expect(config?.authStrategy).toBe('auth_token_empty_api_key')
})
test('should return null when ChatGPT Official is the active provider', async () => {
const svc = new ProviderService()
await svc.activateProvider('openai-official')
const active = await svc.getProviderForProxy()
expect(active).toBeNull()
})
test('should return null when Grok Official is the active provider', async () => {
const svc = new ProviderService()
await svc.activateProvider('grok-official')
const active = await svc.getProviderForProxy()
expect(active).toBeNull()
})
})
describe('handleProxyRequest', () => {
test('preserves optional Computer Use parameters in the final Responses proxy request', async () => {
const originalFetch = globalThis.fetch
const computerTools = buildComputerUseTools().filter(tool =>
['get_app_state', 'click'].includes(tool.name),
)
const originalSchemas = structuredClone(computerTools.map(tool => tool.inputSchema))
const calls: Array<{ url: string; body: Record<string, unknown> }> = []
globalThis.fetch = mock(async (input: string | URL | Request, init?: RequestInit) => {
calls.push({ url: String(input), body: JSON.parse(String(init?.body)) })
return Response.json({
id: 'resp_computer_schema',
object: 'response',
created_at: 0,
model: 'gpt-6-astra',
status: 'completed',
output: [],
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_responses' }))
await svc.activateProvider(provider.id)
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-6-astra',
max_tokens: 64,
messages: [{ role: 'user', content: 'Inspect Blender' }],
tools: computerTools.map(tool => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
})),
}),
})
const response = await handleProxyRequest(req, new URL(req.url))
expect(response.status).toBe(200)
await response.text()
expect(calls).toHaveLength(1)
expect(calls[0].url).toBe('https://api.example.com/v1/responses')
const outboundTools = calls[0].body.tools as Array<{
name: string
strict?: boolean
parameters: Record<string, unknown>
}>
expect(outboundTools).toHaveLength(computerTools.length)
for (const [index, tool] of outboundTools.entries()) {
expect(tool.name).toBe(computerTools[index].name)
expect(tool.strict).toBe(false)
expect(tool.parameters).toEqual(originalSchemas[index])
expect(tool.parameters.required).toEqual(['app'])
}
expect(computerTools.map(tool => tool.inputSchema)).toEqual(originalSchemas)
} finally {
globalThis.fetch = originalFetch
}
})
test('records a session trace for proxied OpenAI Chat calls', async () => {
const originalFetch = globalThis.fetch
const upstreamHeaders: Headers[] = []
globalThis.fetch = mock(async (_input: string | URL | Request, init?: RequestInit) => {
upstreamHeaders.push(new Headers(init?.headers))
return new Response(JSON.stringify({
id: 'chatcmpl-trace',
object: 'chat.completion',
created: 0,
model: 'gpt-4',
choices: [{ index: 0, message: { role: 'assistant', content: 'trace ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 11, completion_tokens: 3, total_tokens: 14 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json', 'x-request-id': 'req-trace' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_chat', name: 'Trace Provider' }))
await svc.activateProvider(provider.id)
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer desktop-local-secret',
'X-Claude-Code-Session-Id': 'session-proxy-trace',
},
body: JSON.stringify({
model: 'gpt-4',
max_tokens: 64,
messages: [{ role: 'user', content: 'capture this call' }],
}),
})
const res = await handleProxyRequest(req, new URL(req.url))
const trace = await waitForCompletedProxyTrace('session-proxy-trace')
expect(res.status).toBe(200)
expect(trace.summary.apiCalls).toBe(1)
expect(trace.calls[0]).toMatchObject({
source: 'proxy',
provider: {
id: provider.id,
name: 'Trace Provider',
format: 'openai_chat',
},
model: 'gpt-4',
})
expect(trace.calls[0].request.body.preview).toContain('capture this call')
expect(trace.calls[0].response.body.preview).toContain('chatcmpl-trace')
expect(upstreamHeaders[0].get('Authorization')).toBe('Bearer sk-test-key-123')
expect(upstreamHeaders[0].get('Authorization')).not.toContain('desktop-local-secret')
} finally {
globalThis.fetch = originalFetch
}
})
test('records DeepSeek Computer Use request semantics independently of the truncated raw body', async () => {
const originalFetch = globalThis.fetch
globalThis.fetch = mock(async () => new Response(JSON.stringify({
id: 'chatcmpl-trace-deepseek-computer-use',
object: 'chat.completion',
created: 0,
model: 'deepseek-v4-flash-vision-exp',
choices: [{ index: 0, message: { role: 'assistant', content: 'validated' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 119_000, completion_tokens: 12, total_tokens: 119_012 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})) as typeof fetch
const screenshotData = 'AQID'.repeat(80_000)
const injectedContext = '<system-reminder>\n# Project rules\nUse the repository instructions.\n</system-reminder>'
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
apiFormat: 'openai_chat',
baseUrl: 'https://opencode.ai/zen',
name: 'DeepSeek V4 Flash',
models: {
main: 'deepseek-v4-flash-vision-exp',
haiku: 'deepseek-v4-flash-vision-exp',
sonnet: 'deepseek-v4-flash-vision-exp',
opus: 'deepseek-v4-flash-vision-exp',
},
}))
await svc.activateProvider(provider.id)
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Claude-Code-Session-Id': 'session-deepseek-computer-use-trace',
},
body: JSON.stringify({
model: 'deepseek-v4-flash-vision-exp',
max_tokens: 64,
system: [{ type: 'text', text: 'You are Claude Code.' }],
messages: [
{
role: 'user',
content: [
{ type: 'text', text: injectedContext },
{ type: 'text', text: '创建并校验 large-200.json 文件' },
],
},
{
role: 'assistant',
content: [{
type: 'tool_use',
id: 'computer_1',
name: 'computer',
input: { action: 'screenshot' },
}],
},
{
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: 'computer_1',
content: [
{ type: 'text', text: 'Computer Use state' },
{
type: 'image',
source: { type: 'base64', media_type: 'image/jpeg', data: screenshotData },
},
],
}],
},
],
}),
})
const response = await handleProxyRequest(req, new URL(req.url))
const trace = await waitForCompletedProxyTrace('session-deepseek-computer-use-trace')
const call = trace.calls[0]
expect(response.status).toBe(200)
expect(call.request.body.truncated).toBe(true)
expect(() => JSON.parse(call.request.body.preview)).toThrow()
expect(call.request.semantic).toMatchObject({
version: 1,
request: {
model: 'deepseek-v4-flash-vision-exp',
system: [{ type: 'text', text: 'You are Claude Code.' }],
messages: [
{
role: 'user',
content: [
{ type: 'text', text: injectedContext },
{ type: 'text', text: '创建并校验 large-200.json 文件' },
],
},
{
role: 'assistant',
content: [{
type: 'tool_use',
id: 'computer_1',
name: 'computer',
input: { action: 'screenshot' },
}],
},
{
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: 'computer_1',
content: [
{ type: 'text', text: 'Computer Use state' },
{
type: 'image',
source: {
type: 'base64',
media_type: 'image/jpeg',
bytes: 240_000,
sha256: expect.stringMatching(/^[0-9a-f]{64}$/),
},
},
],
}],
},
],
},
})
const semanticMessages = call.request.semantic?.request.messages as Array<{
content: Array<Record<string, unknown>>
}>
expect(semanticMessages[2]?.content[0]).toMatchObject({
type: 'tool_result',
tool_use_id: 'computer_1',
})
expect(JSON.stringify(call.request.semantic)).not.toContain(screenshotData.slice(0, 64))
} finally {
globalThis.fetch = originalFetch
}
})
test('returns a non-streaming proxy response before trace persistence finishes', async () => {
const originalFetch = globalThis.fetch
globalThis.fetch = mock(async () => new Response(JSON.stringify({
id: 'chatcmpl-trace-background',
object: 'chat.completion',
created: 0,
model: 'gpt-4',
choices: [{ index: 0, message: { role: 'assistant', content: 'background trace ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})) as typeof fetch
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_chat' }))
await svc.activateProvider(provider.id)
const { releaseWrite, writeBlocked } = blockNextTraceAppend()
let released = false
let responsePromise: Promise<Response> | undefined
try {
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Claude-Code-Session-Id': 'session-non-stream-background-trace',
},
body: JSON.stringify({
model: 'gpt-4',
max_tokens: 64,
messages: [{ role: 'user', content: 'return before trace persistence' }],
}),
})
responsePromise = handleProxyRequest(req, new URL(req.url))
await writeBlocked
const response = await settlesBeforeBlockedTraceWrite(responsePromise)
expect(response).not.toBeNull()
expect(response?.status).toBe(200)
await expect(response?.json()).resolves.toMatchObject({
content: [{ text: 'background trace ok' }],
})
releaseWrite()
released = true
const trace = await waitForCompletedProxyTrace('session-non-stream-background-trace')
expect(trace.calls[0]?.response?.body.preview).toContain('chatcmpl-trace-background')
expect(trace.events.at(-1)?.phase).toBe('upstream_fetch_completed')
} finally {
if (!released) releaseWrite()
await responsePromise?.catch(() => undefined)
globalThis.fetch = originalFetch
}
})
test('delivers streaming EOF before trace persistence finishes', async () => {
const originalFetch = globalThis.fetch
const encoder = new TextEncoder()
globalThis.fetch = mock(async () => new Response(new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(encoder.encode([
'data: {"id":"chatcmpl-stream-trace","object":"chat.completion.chunk","model":"gpt-4","choices":[{"index":0,"delta":{"role":"assistant","content":"streamed"},"finish_reason":null}]}',
'',
'data: {"id":"chatcmpl-stream-trace","object":"chat.completion.chunk","model":"gpt-4","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2,"total_tokens":7}}',
'',
'data: [DONE]',
'',
].join('\n')))
controller.close()
},
}), {
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
})) as typeof fetch
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_chat' }))
await svc.activateProvider(provider.id)
const { releaseWrite, writeBlocked } = blockNextTraceAppend()
let released = false
let bodyPromise: Promise<string> | undefined
try {
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Claude-Code-Session-Id': 'session-stream-background-trace',
},
body: JSON.stringify({
model: 'gpt-4',
max_tokens: 64,
stream: true,
messages: [{ role: 'user', content: 'finish before trace persistence' }],
}),
})
const response = await handleProxyRequest(req, new URL(req.url))
bodyPromise = response.text()
await writeBlocked
const body = await settlesBeforeBlockedTraceWrite(bodyPromise)
expect(body).not.toBeNull()
expect(body).toContain('message_stop')
releaseWrite()
released = true
const trace = await waitForCompletedProxyTrace('session-stream-background-trace')
expect(trace.calls[0]?.response?.body.preview).toContain('message_stop')
expect(trace.events.at(-1)?.phase).toBe('upstream_fetch_completed')
} finally {
if (!released) releaseWrite()
await bodyPromise?.catch(() => undefined)
globalThis.fetch = originalFetch
}
})
test('strips leading billing attribution instead of injecting it for OpenAI-compatible upstreams', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ body: Record<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
return new Response(JSON.stringify({
id: 'chatcmpl-1',
object: 'chat.completion',
created: 0,
model: 'gpt-4',
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_chat' }))
await svc.activateProvider(provider.id)
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-4',
max_tokens: 64,
system: [
{ type: 'text', text: 'x-anthropic-billing-header: cc_version=2.1.220.693; cc_entrypoint=cli; cch=00000;' },
{ type: 'text', text: 'You are a helpful assistant.' },
],
messages: [{ role: 'user', content: 'hello from proxy' }],
}),
})
const res = await handleProxyRequest(req, new URL(req.url))
expect(res.status).toBe(200)
// The rotating billing header would change the prompt prefix on every
// request and defeat upstream prefix caching — it must not be forwarded.
const messages = calls[0].body.messages as Array<Record<string, string>>
expect(messages[0].role).toBe('system')
expect(messages[0].content).toBe('You are a helpful assistant.')
expect(JSON.stringify(calls[0].body)).not.toContain('x-anthropic-billing-header')
} finally {
globalThis.fetch = originalFetch
}
})
test('round-trips DeepSeek reasoning by model on generic OpenAI Chat hosts', async () => {
const originalFetch = globalThis.fetch
const calls: Array<Record<string, unknown>> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
const body = JSON.parse(String(init?.body)) as Record<string, unknown>
calls.push(body)
const messages = body.messages as Array<Record<string, unknown>>
const assistantMessage = messages.find(message => message.role === 'assistant')
if (body.model === 'deepseek-v4-flash' && assistantMessage?.reasoning_content === undefined) {
return new Response(JSON.stringify({
error: {
message: 'The reasoning_content in the thinking mode must be passed back to the API',
},
}), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
return new Response(JSON.stringify({
id: 'chatcmpl-reasoning-round-trip',
object: 'chat.completion',
created: 0,
model: body.model,
choices: [{ index: 0, message: { role: 'assistant', content: 'done' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
apiFormat: 'openai_chat',
baseUrl: 'https://api.b.ai',
}))
await svc.activateProvider(provider.id)
const makeFollowUpRequest = (model: string) => new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
max_tokens: 64,
messages: [
{
role: 'assistant',
content: [
{ type: 'thinking', thinking: 'Need to inspect the repository first.' },
{ type: 'tool_use', id: 'call_1', name: 'read_file', input: { path: 'README.md' } },
],
},
{
role: 'user',
content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'Repository contents' }],
},
],
}),
})
const deepSeekRequest = makeFollowUpRequest('deepseek-v4-flash')
const deepSeekResponse = await handleProxyRequest(deepSeekRequest, new URL(deepSeekRequest.url))
expect(deepSeekResponse.status).toBe(200)
const genericRequest = makeFollowUpRequest('generic-chat-model')
const genericResponse = await handleProxyRequest(genericRequest, new URL(genericRequest.url))
expect(genericResponse.status).toBe(200)
const deepSeekMessages = calls[0].messages as Array<Record<string, unknown>>
const deepSeekAssistant = deepSeekMessages.find(message => message.role === 'assistant')
expect(deepSeekAssistant?.reasoning_content).toBe('Need to inspect the repository first.')
const genericMessages = calls[1].messages as Array<Record<string, unknown>>
const genericAssistant = genericMessages.find(message => message.role === 'assistant')
expect(genericAssistant?.reasoning_content).toBeUndefined()
} finally {
globalThis.fetch = originalFetch
}
})
test('forwards a stable prompt_cache_key from client session metadata for OpenAI Responses upstreams', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ body: Record<string, unknown>; headers: Headers }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({
body: JSON.parse(String(init?.body)) as Record<string, unknown>,
headers: new Headers(init?.headers),
})
return new Response(JSON.stringify({
id: 'resp-1',
object: 'response',
created_at: 0,
model: 'gpt-5.4',
status: 'completed',
output: [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'ok' }] }],
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_responses' }))
await svc.activateProvider(provider.id)
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer desktop-local-secret',
},
body: JSON.stringify({
model: 'gpt-5.4',
max_tokens: 64,
metadata: { user_id: 'user_3f7a_account_9b2c_session_sess-42aa' },
messages: [{ role: 'user', content: 'hello from proxy' }],
}),
})
const res = await handleProxyRequest(req, new URL(req.url))
expect(res.status).toBe(200)
expect(calls[0].body.prompt_cache_key).toBe('sess-42aa')
expect(calls[0].headers.get('Authorization')).toBe('Bearer sk-test-key-123')
expect(calls[0].headers.get('Authorization')).not.toContain('desktop-local-secret')
} finally {
globalThis.fetch = originalFetch
}
})
test.each([
'https://opencode.ai/zen',
'https://api.deepseek.com',
'https://gateway.example.test',
])('preserves Computer Use tool images for explicit vision models at %s', async (baseUrl) => {
const body = await captureOpenAIChatRequest({
baseUrl,
model: 'deepseek-v4-flash-vision-exp',
content: [
{ type: 'text', text: 'Computer Use state' },
{ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: '/9j/AA==' } },
{ type: 'text', text: 'After screenshot' },
],
})
const messages = body.messages as Array<Record<string, unknown>>
expect(messages).toEqual([
{
role: 'tool',
tool_call_id: 'computer_1',
content: 'Computer Use stateAfter screenshot',
},
{
role: 'user',
content: [
{ type: 'text', text: '[Media content for tool call computer_1]' },
{ type: 'image_url', image_url: { url: 'data:image/jpeg;base64,/9j/AA==' } },
],
},
])
})
test.each([
{
name: 'opencode non-vision model',
baseUrl: 'https://opencode.ai/zen',
model: 'deepseek-v4-flash',
},
{
name: 'classic DeepSeek text model',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-v4-flash',
},
])('uses text-only Computer Use content for $name', async ({ baseUrl, model }) => {
const body = await captureOpenAIChatRequest({
baseUrl,
model,
content: [{
type: 'image',
source: { type: 'base64', media_type: 'image/jpeg', data: 'private-screenshot-data' },
}],
})
const messages = body.messages as Array<Record<string, unknown>>
expect(messages[0]).toEqual({
role: 'tool',
tool_call_id: 'computer_1',
content: '\n[Image omitted: this OpenAI-compatible chat endpoint only supports text content.]\n',
})
expect(JSON.stringify(body)).not.toContain('private-screenshot-data')
expect(JSON.stringify(body)).not.toContain('image_url')
})
test.each([
{ baseUrl: 'https://api.deepseek.com', model: 'deepseek-v4-flash-vision-exp' },
{ baseUrl: 'https://gateway.example.test', model: 'deepseek-v4-flash-vision-exp' },
{ baseUrl: 'https://opencode.ai/zen', model: 'deepseek-v4-flash-vision-exp' },
])('forwards chat attachments for $model at $baseUrl (#1304)', async ({ baseUrl, model }) => {
const body = await captureOpenAIChatRequest({
baseUrl,
model,
contentSource: 'user',
content: [
{ type: 'text', text: 'Describe this picture.' },
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'picture-data' } },
],
})
expect(body.messages).toEqual([{
role: 'user',
content: [
{ type: 'text', text: 'Describe this picture.' },
{ type: 'image_url', image_url: { url: 'data:image/png;base64,picture-data' } },
],
}])
expect(JSON.stringify(body)).not.toContain('Image omitted:')
})
test('keeps generic OpenAI Chat providers vision-capable by default', async () => {
const body = await captureOpenAIChatRequest({
baseUrl: 'https://chat.example.test',
model: 'custom-text-named-model',
content: [{
type: 'image',
source: { type: 'base64', media_type: 'image/png', data: 'generic-image-data' },
}],
})
const messages = body.messages as Array<Record<string, unknown>>
expect(messages).toEqual([
{
role: 'tool',
tool_call_id: 'computer_1',
content: 'Media result attached after this tool result.',
},
{
role: 'user',
content: [
{ type: 'text', text: '[Media content for tool call computer_1]' },
{ type: 'image_url', image_url: { url: 'data:image/png;base64,generic-image-data' } },
],
},
])
})
test('normalizes context-window suffixes before forwarding OpenAI Chat proxy requests', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ body: Record<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
return new Response(JSON.stringify({
id: 'chatcmpl-1',
object: 'chat.completion',
created: 0,
model: 'mimo-v2.5-pro',
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({ apiFormat: 'openai_chat' }))
await svc.activateProvider(provider.id)
const req = new Request('http://localhost:3456/proxy/v1/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'mimo-v2.5-pro[1m]',
max_tokens: 64,
messages: [{ role: 'user', content: 'hello from proxy' }],
}),
})
const res = await handleProxyRequest(req, new URL(req.url))
expect(res.status).toBe(200)
expect(calls[0].body.model).toBe('mimo-v2.5-pro')
} finally {
globalThis.fetch = originalFetch
}
})
})
describe('testProvider', () => {
test('should use preset default auth for saved no-key Anthropic-compatible providers', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ headers: Record<string, string> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ headers: init?.headers as Record<string, string> })
return new Response(JSON.stringify({
type: 'message',
model: 'lmstudio-model',
content: [],
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
presetId: 'lmstudio',
apiKey: '',
authStrategy: 'auth_token_empty_api_key',
models: {
main: 'lmstudio-model',
haiku: 'lmstudio-model',
sonnet: 'lmstudio-model',
opus: 'lmstudio-model',
},
}))
const result = await svc.testProvider(provider.id)
expect(result.connectivity.success).toBe(true)
expect(calls[0].headers.Authorization).toBe('Bearer lmstudio')
expect(calls[0].headers['x-api-key']).toBeUndefined()
} finally {
globalThis.fetch = originalFetch
}
})
test('rejects destination and auth overrides before testing with a saved key', async () => {
const originalFetch = globalThis.fetch
const calls: string[] = []
globalThis.fetch = mock(async (url: string | URL | Request) => {
calls.push(String(url))
return new Response('{}', { status: 200 })
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
const { req, url, segments } = makeRequest(
'POST',
`/api/providers/${provider.id}/test`,
{
baseUrl: 'https://override.example.com',
apiFormat: 'openai_chat',
authStrategy: 'auth_token',
},
)
const response = await handleProvidersApi(req, url, segments)
expect(response.status).toBe(400)
expect(calls).toEqual([])
} finally {
globalThis.fetch = originalFetch
}
})
test('accepts a model-only override without changing a saved provider destination', async () => {
const originalFetch = globalThis.fetch
const calls: string[] = []
globalThis.fetch = mock(async (url: string | URL | Request) => {
calls.push(String(url))
return new Response(JSON.stringify({
type: 'message',
model: 'alternate-model',
content: [],
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput({
baseUrl: 'https://saved.example.com',
}))
const { req, url, segments } = makeRequest(
'POST',
`/api/providers/${provider.id}/test`,
{ modelId: 'alternate-model' },
)
const response = await handleProvidersApi(req, url, segments)
expect(response.status).toBe(200)
expect(calls[0]).toContain('https://saved.example.com')
} finally {
globalThis.fetch = originalFetch
}
})
test('keeps explicit draft provider tests independent from saved credentials', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ url: string; authorization: string | null }> = []
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
calls.push({
url: String(url),
authorization: new Headers(init?.headers).get('authorization'),
})
return new Response(JSON.stringify({
type: 'message',
model: 'draft-model',
content: [],
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const { req, url, segments } = makeRequest(
'POST',
'/api/providers/test',
{
baseUrl: 'https://draft.example.com',
apiKey: 'draft-explicit-key',
modelId: 'draft-model',
apiFormat: 'anthropic',
authStrategy: 'auth_token',
},
)
const response = await handleProvidersApi(req, url, segments)
expect(response.status).toBe(200)
expect(calls).toEqual([{
url: 'https://draft.example.com/v1/messages',
authorization: 'Bearer draft-explicit-key',
}])
} finally {
globalThis.fetch = originalFetch
}
})
})
describe('testProviderConfig', () => {
for (const [basePath, messagePath] of [
['', '/v1/messages'],
['/', '/v1/messages'],
['/v1', '/v1/messages'],
['/v1///', '/v1/messages'],
['/anthropic', '/anthropic/v1/messages'],
['/anthropic/v1/', '/anthropic/v1/messages'],
['/v1/tenant', '/v1/tenant/v1/messages'],
]) {
test(`Anthropic base URL ${basePath || '(root)'} works in connectivity and proxy requests (#1279)`, async () => {
const requests: Array<{ path: string; apiKey: string | null; version: string | null }> = []
const server = Bun.serve({
hostname: '127.0.0.1',
port: 0,
async fetch(req) {
const requestPath = new URL(req.url).pathname
requests.push({
path: requestPath,
apiKey: req.headers.get('x-api-key'),
version: req.headers.get('anthropic-version'),
})
if (requestPath !== messagePath) {
return Response.json({ error: { message: 'Unknown endpoint' } }, { status: 404 })
}
const body = await req.json() as { stream?: boolean }
if (body.stream) {
return new Response('event: message_stop\ndata: {"type":"message_stop"}\n\n', {
headers: { 'Content-Type': 'text/event-stream' },
})
}
return Response.json({ type: 'message', model: 'model-main', content: [{ type: 'text', text: 'ok' }] })
},
})
try {
const svc = new ProviderService()
const baseUrl = `http://127.0.0.1:${server.port}${basePath}`
const provider = await svc.addProvider(sampleInput({
baseUrl,
authStrategy: 'api_key',
supportsNestedToolResultMedia: false,
}))
const result = await svc.testProvider(provider.id)
expect(requests[0]?.path).toBe(messagePath)
expect(result.connectivity.success).toBe(true)
expect(result.proxy?.success).toBe(true)
for (const stream of [false, true]) {
const req = new Request(`http://localhost/proxy/providers/${provider.id}/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
body: JSON.stringify({ model: 'model-main', max_tokens: 16, stream, messages: [{ role: 'user', content: 'hello' }] }),
})
const response = await handleProxyRequest(req, new URL(req.url))
expect(response.status).toBe(200)
if (stream) expect(await response.text()).toContain('event: message_stop')
else expect(await response.json()).toMatchObject({ type: 'message' })
}
expect(requests).toEqual(Array.from({ length: 4 }, () => ({
path: messagePath,
apiKey: 'sk-test-key-123',
version: '2023-06-01',
})))
expect((await svc.getProvider(provider.id))?.baseUrl).toBe(baseUrl)
} finally {
server.stop(true)
}
})
}
test('should use auth strategy headers for Anthropic-compatible tests', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ url: string; headers: Record<string, string> }> = []
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
calls.push({
url: String(url),
headers: init?.headers as Record<string, string>,
})
return new Response(JSON.stringify({
type: 'message',
model: 'model-main',
content: [],
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
await svc.testProviderConfig({
baseUrl: 'https://api.example.com/anthropic',
apiKey: 'sk-bearer',
modelId: 'model-main',
authStrategy: 'auth_token',
apiFormat: 'anthropic',
})
await svc.testProviderConfig({
baseUrl: 'https://api.example.com/anthropic',
apiKey: 'sk-api',
modelId: 'model-main',
authStrategy: 'api_key',
apiFormat: 'anthropic',
})
await svc.testProviderConfig({
baseUrl: 'https://api.example.com/anthropic',
apiKey: 'sk-dual',
modelId: 'model-main',
authStrategy: 'dual_same_token',
apiFormat: 'anthropic',
})
expect(calls[0].headers.Authorization).toBe('Bearer sk-bearer')
expect(calls[0].headers['x-api-key']).toBeUndefined()
expect(calls[1].headers['x-api-key']).toBe('sk-api')
expect(calls[1].headers.Authorization).toBeUndefined()
expect(calls[2].headers['x-api-key']).toBe('sk-dual')
expect(calls[2].headers.Authorization).toBe('Bearer sk-dual')
} finally {
globalThis.fetch = originalFetch
}
})
test('tests the proxy path for Anthropic providers that require media hoisting', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ body: Record<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
return new Response(JSON.stringify({
type: 'message',
model: 'model-main',
content: [{ type: 'text', text: 'ok' }],
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const result = await svc.testProviderConfig({
baseUrl: 'https://api.example.com/anthropic',
apiKey: 'sk-api',
modelId: 'model-main',
authStrategy: 'api_key',
apiFormat: 'anthropic',
supportsNestedToolResultMedia: false,
})
expect(result.connectivity.success).toBe(true)
expect(result.proxy?.success).toBe(true)
expect(calls).toHaveLength(2)
} finally {
globalThis.fetch = originalFetch
}
})
test('normalizes context-window suffixes for Anthropic-compatible connectivity tests', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ body: Record<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
return new Response(JSON.stringify({
type: 'message',
model: 'mimo-v2.5-pro',
content: [],
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const result = await svc.testProviderConfig({
baseUrl: 'https://api.xiaomimimo.com/anthropic',
apiKey: 'sk-api',
modelId: 'mimo-v2.5-pro[1m]',
authStrategy: 'auth_token',
apiFormat: 'anthropic',
})
expect(result.connectivity.success).toBe(true)
expect(result.connectivity.modelUsed).toBe('mimo-v2.5-pro')
expect(calls[0].body.model).toBe('mimo-v2.5-pro')
} finally {
globalThis.fetch = originalFetch
}
})
test('normalizes context-window suffixes for provider proxy pipeline tests', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ body: Record<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
return new Response(JSON.stringify({
id: 'chatcmpl-1',
object: 'chat.completion',
created: 0,
model: 'mimo-v2.5-pro',
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const result = await svc.testProviderConfig({
baseUrl: 'https://api.example.com',
apiKey: 'sk-api',
modelId: 'mimo-v2.5-pro[1m]',
authStrategy: 'api_key',
apiFormat: 'openai_chat',
})
expect(result.connectivity.success).toBe(true)
expect(result.proxy?.success).toBe(true)
expect(result.connectivity.modelUsed).toBe('mimo-v2.5-pro')
expect(result.proxy?.modelUsed).toBe('mimo-v2.5-pro')
expect(calls.map((call) => call.body.model)).toEqual(['mimo-v2.5-pro', 'mimo-v2.5-pro'])
} finally {
globalThis.fetch = originalFetch
}
})
test('requests non-stream OpenAI Chat responses during provider tests', async () => {
const originalFetch = globalThis.fetch
const calls: Array<{ body: Record<string, unknown> }> = []
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
calls.push({ body: JSON.parse(String(init?.body)) as Record<string, unknown> })
return new Response(JSON.stringify({
id: 'chatcmpl-1',
object: 'chat.completion',
created: 0,
model: 'deepseek-v4-flash',
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const result = await svc.testProviderConfig({
baseUrl: 'https://api.example.com',
apiKey: 'sk-api',
modelId: 'deepseek-v4-flash',
authStrategy: 'api_key',
apiFormat: 'openai_chat',
})
expect(result.connectivity.success).toBe(true)
expect(result.proxy?.success).toBe(true)
expect(calls.map((call) => call.body.stream)).toEqual([false, false])
} finally {
globalThis.fetch = originalFetch
}
})
test('bypasses manual proxy options when testing loopback provider endpoints', async () => {
await fs.writeFile(
path.join(tmpDir, 'settings.json'),
JSON.stringify({
network: {
proxy: { mode: 'manual', url: 'http://127.0.0.1:1181' },
},
}),
'utf-8',
)
const originalFetch = globalThis.fetch
const calls: Array<{ url: string; proxy?: string }> = []
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
calls.push({
url: String(url),
proxy: (init as RequestInit & { proxy?: string } | undefined)?.proxy,
})
return new Response(JSON.stringify({
id: 'chatcmpl-1',
object: 'chat.completion',
created: 0,
model: 'local-model',
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const result = await svc.testProviderConfig({
baseUrl: 'http://127.0.0.1:11434',
apiKey: 'local-key',
modelId: 'local-model',
authStrategy: 'api_key',
apiFormat: 'openai_chat',
})
expect(result.connectivity.success).toBe(true)
expect(result.proxy?.success).toBe(true)
expect(calls.map((call) => call.url)).toEqual([
'http://127.0.0.1:11434/v1/chat/completions',
'http://127.0.0.1:11434/v1/chat/completions',
])
expect(calls.map((call) => call.proxy)).toEqual([undefined, undefined])
} finally {
globalThis.fetch = originalFetch
}
})
test('bypasses inherited system proxy when testing direct provider endpoints', async () => {
await fs.writeFile(
path.join(tmpDir, 'settings.json'),
JSON.stringify({
network: {
proxy: { mode: 'direct', url: '' },
},
}),
'utf-8',
)
const originalFetch = globalThis.fetch
const originalHttpProxy = process.env.HTTP_PROXY
const originalHttpsProxy = process.env.HTTPS_PROXY
const originalLowerHttpProxy = process.env.http_proxy
const originalLowerHttpsProxy = process.env.https_proxy
const calls: Array<{ url: string; proxy?: string }> = []
process.env.HTTP_PROXY = 'http://127.0.0.1:1181'
process.env.HTTPS_PROXY = 'http://127.0.0.1:1181'
delete process.env.http_proxy
delete process.env.https_proxy
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
calls.push({
url: String(url),
proxy: (init as RequestInit & { proxy?: string } | undefined)?.proxy,
})
return new Response(JSON.stringify({
id: 'chatcmpl-direct',
object: 'chat.completion',
created: 0,
model: 'remote-model',
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
try {
const svc = new ProviderService()
const result = await svc.testProviderConfig({
baseUrl: 'https://api.example.com',
apiKey: 'remote-key',
modelId: 'remote-model',
authStrategy: 'api_key',
apiFormat: 'openai_chat',
})
expect(result.connectivity.success).toBe(true)
expect(result.proxy?.success).toBe(true)
expect(calls.map((call) => call.url)).toEqual([
'https://api.example.com/v1/chat/completions',
'https://api.example.com/v1/chat/completions',
])
expect(calls.map((call) => call.proxy)).toEqual([undefined, undefined])
} finally {
globalThis.fetch = originalFetch
if (originalHttpProxy === undefined) delete process.env.HTTP_PROXY
else process.env.HTTP_PROXY = originalHttpProxy
if (originalHttpsProxy === undefined) delete process.env.HTTPS_PROXY
else process.env.HTTPS_PROXY = originalHttpsProxy
if (originalLowerHttpProxy === undefined) delete process.env.http_proxy
else process.env.http_proxy = originalLowerHttpProxy
if (originalLowerHttpsProxy === undefined) delete process.env.https_proxy
else process.env.https_proxy = originalLowerHttpsProxy
}
})
test('should use configured network timeout for provider tests', async () => {
await fs.writeFile(
path.join(tmpDir, 'settings.json'),
JSON.stringify({
network: {
aiRequestTimeoutMs: 180_000,
proxy: { mode: 'system', url: '' },
},
}),
'utf-8',
)
const originalFetch = globalThis.fetch
const originalTimeout = AbortSignal.timeout
const timeoutCalls: number[] = []
globalThis.fetch = mock(async (_url: string | URL | Request, _init?: RequestInit) => {
return new Response(JSON.stringify({
type: 'message',
model: 'model-main',
content: [],
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}) as typeof fetch
AbortSignal.timeout = ((ms: number) => {
timeoutCalls.push(ms)
return originalTimeout(ms)
}) as typeof AbortSignal.timeout
try {
const svc = new ProviderService()
await svc.testProviderConfig({
baseUrl: 'https://api.example.com/anthropic',
apiKey: 'sk-api',
modelId: 'model-main',
authStrategy: 'api_key',
apiFormat: 'anthropic',
})
expect(timeoutCalls).toEqual([180_000])
} finally {
AbortSignal.timeout = originalTimeout
globalThis.fetch = originalFetch
}
})
})
})
// =============================================================================
// Providers REST API
// =============================================================================
describe('Providers API', () => {
beforeEach(setup)
afterEach(teardown)
// ─── GET /api/providers ──────────────────────────────────────────────────
test('GET /api/providers should return empty list initially', async () => {
const { req, url, segments } = makeRequest('GET', '/api/providers')
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
const body = (await res.json()) as { providers: unknown[] }
expect(body.providers).toEqual([])
})
test('GET /api/providers should list added providers', async () => {
// Seed a provider via service
const svc = new ProviderService()
await svc.addProvider(sampleInput())
const { req, url, segments } = makeRequest('GET', '/api/providers')
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
const body = (await res.json()) as { providers: { name: string; apiKey: string }[] }
expect(body.providers).toHaveLength(1)
expect(body.providers[0].name).toBe('Test Provider')
expect(body.providers[0].apiKey).toBe('sk-test-key-123')
})
// ─── POST /api/providers ─────────────────────────────────────────────────
test('POST /api/providers should create a provider', async () => {
const { req, url, segments } = makeRequest('POST', '/api/providers', {
presetId: 'custom',
name: 'New Provider',
baseUrl: 'https://api.example.com',
apiKey: 'sk-test',
apiFormat: 'anthropic',
autoCompactWindow: 64000,
disableExperimentalBetas: true,
models: {
main: 'gpt-4',
haiku: 'gpt-4-haiku',
sonnet: 'gpt-4-sonnet',
opus: 'gpt-4-opus',
},
})
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(201)
const body = (await res.json()) as { provider: { name: string; models: { main: string }; autoCompactWindow: number; disableExperimentalBetas?: boolean } }
expect(body.provider.name).toBe('New Provider')
expect(body.provider.models.main).toBe('gpt-4')
expect(body.provider.autoCompactWindow).toBe(64000)
expect(body.provider.disableExperimentalBetas).toBe(true)
})
test('POST /api/providers should return 400 for invalid input', async () => {
const { req, url, segments } = makeRequest('POST', '/api/providers', {
name: '', // invalid: empty name
})
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(400)
})
// ─── PUT /api/providers/reorder ──────────────────────────────────────────
test('PUT /api/providers/reorder should reorder providers', async () => {
const svc = new ProviderService()
const a = await svc.addProvider(sampleInput({ name: 'A' }))
const b = await svc.addProvider(sampleInput({ name: 'B' }))
const { req, url, segments } = makeRequest('PUT', '/api/providers/reorder', {
orderedIds: [b.id, a.id],
})
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
const body = (await res.json()) as { providers: { name: string }[]; providerOrder: string[] }
expect(body.providers.map((p) => p.name)).toEqual(['B', 'A'])
expect(body.providerOrder).toEqual([
b.id,
a.id,
'claude-official',
'openai-official',
'grok-official',
])
})
test('PUT /api/providers/reorder should return 400 for a non-permutation', async () => {
const svc = new ProviderService()
const a = await svc.addProvider(sampleInput({ name: 'A' }))
await svc.addProvider(sampleInput({ name: 'B' }))
const { req, url, segments } = makeRequest('PUT', '/api/providers/reorder', {
orderedIds: [a.id], // missing B
})
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(400)
})
test('PUT /api/providers/reorder should return 400 for empty orderedIds', async () => {
const { req, url, segments } = makeRequest('PUT', '/api/providers/reorder', {
orderedIds: [],
})
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(400)
})
test('POST /api/providers/reorder should be method-not-allowed', async () => {
const { req, url, segments } = makeRequest('POST', '/api/providers/reorder', {
orderedIds: [],
})
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(405)
})
test('POST /api/providers should return 400 for invalid auto compact window', async () => {
const { req, url, segments } = makeRequest('POST', '/api/providers', {
presetId: 'custom',
name: 'New Provider',
baseUrl: 'https://api.example.com',
apiKey: 'sk-test',
apiFormat: 'anthropic',
autoCompactWindow: 8000,
models: {
main: 'gpt-4',
haiku: 'gpt-4-haiku',
sonnet: 'gpt-4-sonnet',
opus: 'gpt-4-opus',
},
})
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(400)
})
// ─── GET /api/providers/:id ──────────────────────────────────────────────
test('GET /api/providers/:id should return a provider', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput())
const { req, url, segments } = makeRequest('GET', `/api/providers/${added.id}`)
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
const body = (await res.json()) as { provider: { id: string; name: string } }
expect(body.provider.id).toBe(added.id)
})
test('GET /api/providers/:id should return 404 for unknown id', async () => {
const { req, url, segments } = makeRequest('GET', '/api/providers/unknown-id')
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(404)
})
// ─── PUT /api/providers/:id ──────────────────────────────────────────────
test('PUT /api/providers/:id should update a provider', async () => {
const svc = new ProviderService()
const added = await svc.addProvider(sampleInput())
const { req, url, segments } = makeRequest('PUT', `/api/providers/${added.id}`, {
name: 'Renamed Provider',
disableExperimentalBetas: true,
})
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
const body = (await res.json()) as { provider: { name: string; disableExperimentalBetas?: boolean } }
expect(body.provider.name).toBe('Renamed Provider')
expect(body.provider.disableExperimentalBetas).toBe(true)
})
// ─── DELETE /api/providers/:id ───────────────────────────────────────────
test('DELETE /api/providers/:id should delete an inactive provider', async () => {
const svc = new ProviderService()
await svc.addProvider(sampleInput({ name: 'First' }))
const second = await svc.addProvider(sampleInput({ name: 'Second' }))
const { req, url, segments } = makeRequest('DELETE', `/api/providers/${second.id}`)
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
const body = (await res.json()) as { ok: boolean }
expect(body.ok).toBe(true)
})
test('DELETE /api/providers/:id should return 409 for active provider', async () => {
const svc = new ProviderService()
const active = await svc.addProvider(sampleInput())
await svc.activateProvider(active.id)
const { req, url, segments } = makeRequest('DELETE', `/api/providers/${active.id}`)
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(409)
})
// ─── POST /api/providers/:id/activate ────────────────────────────────────
test('POST /api/providers/:id/activate should activate a provider', async () => {
const svc = new ProviderService()
await svc.addProvider(sampleInput({ name: 'First' }))
const second = await svc.addProvider(
sampleInput({
name: 'Second',
baseUrl: 'https://second.example.com',
apiKey: 'sk-second',
}),
)
const { req, url, segments } = makeRequest(
'POST',
`/api/providers/${second.id}/activate`,
)
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
const body = (await res.json()) as { ok: boolean }
expect(body.ok).toBe(true)
// Verify settings were synced
const settings = await readSettings()
const env = settings.env as Record<string, string>
expect(env.ANTHROPIC_BASE_URL).toBe('https://second.example.com')
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-second')
expect(env.ANTHROPIC_API_KEY).toBe('')
expect(env.ANTHROPIC_MODEL).toBe('model-main')
})
test('POST /api/providers/:id/activate should not require modelId', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
const { req, url, segments } = makeRequest(
'POST',
`/api/providers/${provider.id}/activate`,
{},
)
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
})
test('POST /api/providers/:id/activate should ignore modelId because session runtime selects the model', async () => {
const svc = new ProviderService()
const provider = await svc.addProvider(sampleInput())
const { req, url, segments } = makeRequest(
'POST',
`/api/providers/${provider.id}/activate`,
{ modelId: 'non-existent-model' },
)
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(200)
})
// ─── Method not allowed ──────────────────────────────────────────────────
test('should return 405 for unsupported methods', async () => {
const { req, url, segments } = makeRequest('PATCH', '/api/providers')
const res = await handleProvidersApi(req, url, segments)
expect(res.status).toBe(405)
})
})
describe('ApiSmart preset request contract (offline fixtures)', () => {
beforeEach(setup)
afterEach(teardown)
const apiKey = 'sk-apismart-offline-fixture'
async function createApiSmart() {
const { PROVIDER_PRESETS } = await import('../config/providerPresets.js')
const preset = PROVIDER_PRESETS.find(item => item.id === 'apismart')!
expect(preset).toBeDefined()
// Exercise the same API boundary as the desktop's save action.
const { req, url, segments } = makeRequest('POST', '/api/providers', {
presetId: preset.id, name: preset.name, baseUrl: preset.baseUrl,
apiFormat: preset.apiFormat, apiKey, models: preset.defaultModels,
})
const result = await handleProvidersApi(req, url, segments)
expect(result.status).toBe(201)
const saved = await result.json()
return { preset, provider: saved.provider }
}
async function proxy(providerId: string, body: Record<string, unknown>) {
const { req, url } = makeRequest('POST', `/proxy/providers/${providerId}/v1/messages`, body)
return handleProxyRequest(req, url)
}
test('preset save, model fetch, connectivity, and actual chat all use exactly one /v1', async () => {
const { preset, provider } = await createApiSmart()
const originalFetch = globalThis.fetch
const calls: Array<{ url: string; method: string | undefined; body: any }> = []
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input)
expect(new Headers(init?.headers).get('authorization')).toBe(`Bearer ${apiKey}`)
const body = init?.body ? JSON.parse(String(init.body)) : undefined
calls.push({ url, method: init?.method, body })
if (url === 'https://gw.apismart.ai/v1/models' && init?.method === 'GET') {
return Response.json({ object: 'list', data: [
{ id: 'deepseek-v4-pro-0813', object: 'model', owned_by: 'deepseek' },
{ id: 'deepseek-v4-flash-0731-tem', object: 'model', owned_by: 'deepseek' },
] })
}
if (url !== 'https://gw.apismart.ai/v1/chat/completions' || init?.method !== 'POST') {
return Response.json({ error: 'Wrong request endpoint' }, { status: 404 })
}
return Response.json({
id: 'chatcmpl-offline', object: 'chat.completion', model: body.model,
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 },
})
}) as typeof fetch
try {
const catalog = makeRequest('POST', '/api/providers/models', { baseUrl: preset.baseUrl, apiKey })
const models = await handleProvidersApi(catalog.req, catalog.url, catalog.segments)
expect(models.status).toBe(200)
expect(await models.json()).toMatchObject({ ok: true, models: [
{ id: 'deepseek-v4-flash-0731-tem' }, { id: 'deepseek-v4-pro-0813' },
] })
const check = await new ProviderService().testProviderConfig({
baseUrl: preset.baseUrl, apiKey, apiFormat: preset.apiFormat, modelId: preset.defaultModels.main,
})
expect(check.connectivity.success).toBe(true)
expect(check.proxy?.success).toBe(true)
for (const model of [preset.defaultModels.main, preset.defaultModels.haiku]) {
const response = await proxy(provider.id, {
model, max_tokens: 64, system: 'Be helpful.', messages: [{ role: 'user', content: 'hello' }],
})
expect(response.status).toBe(200)
expect(await response.json()).toMatchObject({
model, content: [{ type: 'text', text: 'ok' }], stop_reason: 'end_turn',
})
expect(calls.at(-1)?.body).toMatchObject({ model, stream: false, messages: [
{ role: 'system', content: 'Be helpful.' }, { role: 'user', content: 'hello' },
] })
}
expect(calls.map(call => call.url)).toEqual([
'https://gw.apismart.ai/v1/models',
...Array(4).fill('https://gw.apismart.ai/v1/chat/completions'),
])
} finally {
globalThis.fetch = originalFetch
}
})
test('streamed tool calls survive the proxy and their result is sent back with DeepSeek reasoning', async () => {
const { preset, provider } = await createApiSmart()
const originalFetch = globalThis.fetch
const calls: any[] = []
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
expect(String(input)).toBe('https://gw.apismart.ai/v1/chat/completions')
expect(init?.method).toBe('POST')
expect(new Headers(init?.headers).get('authorization')).toBe(`Bearer ${apiKey}`)
calls.push(JSON.parse(String(init?.body)))
const chunks = calls.length === 1 ? [
{ delta: { role: 'assistant', reasoning_content: 'Need the weather.' } },
{ delta: { tool_calls: [{ index: 0, id: 'call_weather', type: 'function', function: { name: 'weather', arguments: '{"city":' } }] } },
{ delta: { tool_calls: [{ index: 0, function: { arguments: '"Paris"}' } }] } },
{ delta: {}, finish_reason: 'tool_calls' },
] : [
{ delta: { role: 'assistant', content: 'It is sunny.' } },
{ delta: {}, finish_reason: 'stop' },
]
const data = chunks.map(choice => `data: ${JSON.stringify({
id: 'chatcmpl-offline-tool', object: 'chat.completion.chunk', model: preset.defaultModels.main,
choices: [{ index: 0, ...choice }],
})}\n\n`).join('') + 'data: [DONE]\n\n'
return new Response(data, { headers: { 'Content-Type': 'text/event-stream' } })
}) as typeof fetch
try {
const tool = { name: 'weather', description: 'Get weather', input_schema: {
type: 'object', properties: { city: { type: 'string' } }, required: ['city'],
} }
const first = await proxy(provider.id, { model: preset.defaultModels.main, stream: true, max_tokens: 64,
messages: [{ role: 'user', content: 'Weather in Paris?' }], tools: [tool], tool_choice: { type: 'auto' },
})
expect(first.status).toBe(200)
const events = (await first.text()).split('\n\n').flatMap(block => {
const data = block.split('\n').find(line => line.startsWith('data: '))?.slice(6)
return data ? [JSON.parse(data)] : []
})
expect(calls[0]).toMatchObject({ stream: true, stream_options: { include_usage: true },
tools: [{ type: 'function', function: { name: 'weather', parameters: tool.input_schema } }], tool_choice: 'auto',
})
expect(events).toContainEqual(expect.objectContaining({ type: 'content_block_start', content_block: {
type: 'tool_use', id: 'call_weather', name: 'weather', input: {},
} }))
const toolInput = events.filter(event => event.delta?.type === 'input_json_delta')
.map(event => event.delta.partial_json).join('')
expect(JSON.parse(toolInput)).toEqual({ city: 'Paris' })
const thinking = events.filter(event => event.delta?.type === 'thinking_delta')
.map(event => event.delta.thinking).join('')
expect(thinking).toBe('Need the weather.')
expect(events.find(event => event.type === 'message_delta')?.delta.stop_reason).toBe('tool_use')
expect(events.at(-1)?.type).toBe('message_stop')
const second = await proxy(provider.id, { model: preset.defaultModels.main, stream: true, max_tokens: 64,
messages: [
{ role: 'user', content: 'Weather in Paris?' },
{ role: 'assistant', content: [
{ type: 'thinking', thinking },
{ type: 'tool_use', id: 'call_weather', name: 'weather', input: JSON.parse(toolInput) },
] },
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_weather', content: 'sunny' }] },
], tools: [tool],
})
expect(second.status).toBe(200)
expect(await second.text()).toContain('It is sunny.')
expect(calls[1].messages).toEqual([
{ role: 'user', content: 'Weather in Paris?' },
{ role: 'assistant', content: null, reasoning_content: 'Need the weather.', tool_calls: [
{ id: 'call_weather', type: 'function', function: { name: 'weather', arguments: '{"city":"Paris"}' } },
] },
{ role: 'tool', tool_call_id: 'call_weather', content: 'sunny' },
])
} finally {
globalThis.fetch = originalFetch
}
})
})