import { describe, test, expect, beforeEach, afterEach } from 'bun:test' import * as fs from 'fs/promises' import * as os from 'os' import * as path from 'path' import { handleProvidersApi } from '../api/providers.js' import { PROVIDER_PRESETS } from '../config/providerPresets.js' let tmpDir: string let originalConfigDir: string | undefined beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'provider-presets-test-')) originalConfigDir = process.env.CLAUDE_CONFIG_DIR process.env.CLAUDE_CONFIG_DIR = tmpDir }) afterEach(async () => { if (originalConfigDir !== undefined) { process.env.CLAUDE_CONFIG_DIR = originalConfigDir } else { delete process.env.CLAUDE_CONFIG_DIR } await fs.rm(tmpDir, { recursive: true, force: true }) }) function makeRequest( method: string, urlStr: string, body?: Record, ): { 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 } } describe('provider presets API', () => { test('GET /api/providers/presets returns the configured presets', async () => { const { req, url, segments } = makeRequest('GET', '/api/providers/presets') const response = await handleProvidersApi(req, url, segments) expect(response.status).toBe(200) expect(await response.json()).toEqual({ presets: PROVIDER_PRESETS }) }) test('configured presets include built-in official and custom entries', () => { expect(PROVIDER_PRESETS.some((preset) => preset.id === 'official')).toBe(true) expect(PROVIDER_PRESETS.some((preset) => preset.id === 'custom')).toBe(true) }) test('local Anthropic-compatible presets appear immediately before custom', () => { expect(PROVIDER_PRESETS.at(-3)?.id).toBe('lmstudio') expect(PROVIDER_PRESETS.at(-2)?.id).toBe('ollama') expect(PROVIDER_PRESETS.at(-1)?.id).toBe('custom') }) test('configured presets keep current default model ids aligned with official provider docs', () => { const lmstudio = PROVIDER_PRESETS.find((preset) => preset.id === 'lmstudio') const ollama = PROVIDER_PRESETS.find((preset) => preset.id === 'ollama') const deepseek = PROVIDER_PRESETS.find((preset) => preset.id === 'deepseek') const zhipu = PROVIDER_PRESETS.find((preset) => preset.id === 'zhipuglm') const kimi = PROVIDER_PRESETS.find((preset) => preset.id === 'kimi') const minimax = PROVIDER_PRESETS.find((preset) => preset.id === 'minimax') const jiekouai = PROVIDER_PRESETS.find((preset) => preset.id === 'jiekouai') const shengsuanyun = PROVIDER_PRESETS.find((preset) => preset.id === 'shengsuanyun') const teamorouter = PROVIDER_PRESETS.find((preset) => preset.id === 'teamorouter') expect(lmstudio?.baseUrl).toBe('http://localhost:1234') expect(lmstudio?.apiFormat).toBe('anthropic') expect(lmstudio?.authStrategy).toBe('auth_token_empty_api_key') expect(lmstudio?.defaultModels.main).toBe('qwen/qwen3.6-27b') expect(ollama?.baseUrl).toBe('http://localhost:11434') expect(ollama?.apiFormat).toBe('anthropic') expect(ollama?.authStrategy).toBe('auth_token_empty_api_key') expect(ollama?.defaultModels.main).toBe('qwen3.6:27b') expect(deepseek?.authStrategy).toBe('auth_token') expect(deepseek?.defaultModels.main).toBe('deepseek-v4-pro[1m]') expect(deepseek?.defaultModels.haiku).toBe('deepseek-v4-flash') expect(deepseek?.defaultModels.sonnet).toBe('deepseek-v4-pro[1m]') expect(deepseek?.defaultModels.opus).toBe('deepseek-v4-pro[1m]') expect(deepseek?.defaultEnv?.CC_HAHA_SEND_DISABLED_THINKING).toBeUndefined() expect(deepseek?.defaultEnv?.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe( 'thinking,effort,adaptive_thinking,max_effort', ) expect(zhipu?.authStrategy).toBe('auth_token') expect(zhipu?.defaultModels.main).toBe('glm-5.2[1m]') expect(zhipu?.defaultModels.haiku).toBe('glm-4.7') expect(zhipu?.defaultModels.sonnet).toBe('glm-5.2[1m]') expect(zhipu?.defaultModels.opus).toBe('glm-5.2[1m]') expect(zhipu?.defaultEnv?.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('1000000') expect(kimi?.baseUrl).toBe('https://api.moonshot.cn/anthropic') expect(kimi?.authStrategy).toBe('auth_token') expect(kimi?.defaultModels.main).toBe('kimi-k2.7-code') expect(kimi?.defaultEnv?.CC_HAHA_SEND_DISABLED_THINKING).toBeUndefined() expect(kimi?.defaultEnv?.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe('thinking') expect(minimax?.authStrategy).toBe('auth_token') expect(minimax?.defaultModels.main).toBe('MiniMax-M3[1m]') expect(minimax?.defaultEnv?.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBe('1000000') expect(minimax?.defaultEnv?.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe('thinking,adaptive_thinking') expect(minimax?.modelContextWindows?.['MiniMax-M3']).toBe(1000000) expect(jiekouai?.baseUrl).toBe('https://api.jiekou.ai/anthropic') expect(jiekouai?.authStrategy).toBe('auth_token') expect(jiekouai?.defaultModels.main).toBe('claude-sonnet-4-6') expect(jiekouai?.defaultModels.opus).toBe('claude-opus-4-7') expect(jiekouai?.defaultEnv?.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe('none') expect(jiekouai?.modelContextWindows?.['claude-sonnet-4-6']).toBe(1000000) expect(shengsuanyun?.baseUrl).toBe('https://router.shengsuanyun.com/api') expect(shengsuanyun?.authStrategy).toBe('auth_token') expect(shengsuanyun?.defaultModels.main).toBe('anthropic/claude-sonnet-4.6') expect(shengsuanyun?.defaultModels.haiku).toBe('anthropic/claude-haiku-4.5:thinking') expect(shengsuanyun?.modelContextWindows?.['anthropic/claude-sonnet-4.6']).toBe(1000000) expect(teamorouter?.baseUrl).toBe('https://api.teamorouter.com') expect(teamorouter?.apiFormat).toBe('anthropic') expect(teamorouter?.authStrategy).toBe('auth_token') expect(teamorouter?.defaultModels.main).toBe('claude-opus-4-8') expect(teamorouter?.defaultModels.haiku).toBe('claude-haiku-4-5') expect(teamorouter?.defaultModels.sonnet).toBe('claude-sonnet-5') expect(teamorouter?.defaultModels.opus).toBe('claude-opus-4-8') expect(teamorouter?.modelContextWindows?.['claude-opus-4-8']).toBe(1000000) }) test('configured presets can expose optional API key and promo metadata', () => { const lmstudio = PROVIDER_PRESETS.find((preset) => preset.id === 'lmstudio') const ollama = PROVIDER_PRESETS.find((preset) => preset.id === 'ollama') const deepseek = PROVIDER_PRESETS.find((preset) => preset.id === 'deepseek') const zhipu = PROVIDER_PRESETS.find((preset) => preset.id === 'zhipuglm') const kimi = PROVIDER_PRESETS.find((preset) => preset.id === 'kimi') const minimax = PROVIDER_PRESETS.find((preset) => preset.id === 'minimax') const jiekouai = PROVIDER_PRESETS.find((preset) => preset.id === 'jiekouai') const shengsuanyun = PROVIDER_PRESETS.find((preset) => preset.id === 'shengsuanyun') const teamorouter = PROVIDER_PRESETS.find((preset) => preset.id === 'teamorouter') const custom = PROVIDER_PRESETS.find((preset) => preset.id === 'custom') expect(lmstudio?.needsApiKey).toBe(false) expect(lmstudio?.promoText).toContain('http://localhost:1234') expect(lmstudio?.promoText).toContain('200K') expect(lmstudio?.defaultEnv).toEqual({ ANTHROPIC_AUTH_TOKEN: 'lmstudio', }) expect(ollama?.needsApiKey).toBe(false) expect(ollama?.promoText).toContain('http://localhost:11434') expect(ollama?.promoText).toContain('200K') expect(ollama?.defaultEnv).toEqual({ ANTHROPIC_AUTH_TOKEN: 'ollama', }) expect(deepseek?.apiKeyUrl).toBe('https://platform.deepseek.com/api_keys') expect(deepseek?.modelContextWindows?.['deepseek-v4-pro']).toBe(1000000) expect(deepseek?.modelContextWindows?.['deepseek-v4-flash']).toBe(1000000) expect(zhipu?.apiKeyUrl).toBe('https://www.bigmodel.cn/invite?icode=d41B2qi8Z5xNwTGLNPPF3OZLO2QH3C0EBTSr%2BArzMw4%3D') expect(zhipu?.promoText).toContain('cc-haha') expect(zhipu?.defaultEnv?.CC_HAHA_SEND_DISABLED_THINKING).toBeUndefined() expect(zhipu?.modelContextWindows?.['glm-5.2']).toBe(1000000) expect(zhipu?.modelContextWindows?.['glm-5.1']).toBe(200000) expect(zhipu?.modelContextWindows?.['glm-4.7']).toBe(200000) expect(zhipu?.modelContextWindows?.['glm-4.5-air']).toBe(128000) expect(kimi?.apiKeyUrl).toBe('https://platform.kimi.com/console/api-keys') expect(kimi?.modelContextWindows?.['kimi-k2.7-code']).toBe(262144) expect(kimi?.modelContextWindows?.['kimi-k2.7-code-highspeed']).toBe(262144) expect(kimi?.modelContextWindows?.['kimi-k2.6']).toBe(262144) expect(minimax?.apiKeyUrl).toBe('https://platform.minimaxi.com/subscribe/token-plan?code=1TG2Cseab2&source=link') expect(jiekouai?.apiKeyUrl).toBe('https://jiekou.ai/referral?invited_code=OBNU3K') expect(jiekouai?.promoText).toContain('官方 8 折') expect(jiekouai?.featured).toBe(true) expect(shengsuanyun?.apiKeyUrl).toBe('https://www.shengsuanyun.com/?from=CH_LEJ88KWR') expect(shengsuanyun?.promoText).toContain('首充 10%') expect(shengsuanyun?.featured).toBe(true) expect(shengsuanyun?.defaultEnv).toEqual({ API_TIMEOUT_MS: '3000000', CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES: 'none', }) expect(shengsuanyun?.modelContextWindows?.['anthropic/claude-opus-4.7']).toBe(1000000) expect(teamorouter?.apiKeyUrl).toBe( 'https://teamorouter.com/?utm_source=cc_haha&utm_medium=referral&utm_campaign=ai_directory', ) expect(teamorouter?.promoText).toContain('10% 折扣') expect(teamorouter?.featured).toBe(true) expect(teamorouter?.defaultEnv).toEqual({ CLAUDE_CODE_SUBAGENT_MODEL: 'claude-sonnet-5', ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES: 'none', }) expect(teamorouter?.modelContextWindows?.['claude-sonnet-5']).toBe(1000000) expect(custom?.promoText).toBeUndefined() expect(custom?.authStrategy).toBe('auth_token') expect(custom?.defaultEnv).toBeUndefined() }) test('GET and PUT /api/providers/settings read and write cc-haha settings.json', async () => { const initial = { env: { ANTHROPIC_MODEL: 'glm-5.1', }, model: 'glm-5.1', } await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true }) await fs.writeFile( path.join(tmpDir, 'cc-haha', 'settings.json'), JSON.stringify(initial, null, 2), 'utf-8', ) const getReq = makeRequest('GET', '/api/providers/settings') const getRes = await handleProvidersApi(getReq.req, getReq.url, getReq.segments) expect(getRes.status).toBe(200) expect(await getRes.json()).toEqual(initial) const updateBody = { model: 'kimi-k2.6', env: { ANTHROPIC_MODEL: 'kimi-k2.6', }, } const putReq = makeRequest('PUT', '/api/providers/settings', updateBody) const putRes = await handleProvidersApi(putReq.req, putReq.url, putReq.segments) expect(putRes.status).toBe(200) const updatedRaw = await fs.readFile(path.join(tmpDir, 'cc-haha', 'settings.json'), 'utf-8') expect(JSON.parse(updatedRaw)).toEqual(updateBody) }) test('provider presets carry docs-backed context windows for current coding models', () => { const byId = new Map(PROVIDER_PRESETS.map((preset) => [preset.id, preset])) for (const id of ['deepseek', 'zhipuglm', 'kimi', 'minimax']) { const preset = byId.get(id)! expect(preset.modelContextWindows?.[preset.defaultModels.main]).toBeGreaterThan(0) } }) })