mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
- provider presets 新增 teamorouter(Anthropic 兼容,auth_token 鉴权) - 默认模型 claude-opus-4-8 / claude-sonnet-5 / claude-haiku-4-5 - README 中英文赞助商表格新增 TeamoRouter 第三行及 logo
236 lines
12 KiB
TypeScript
236 lines
12 KiB
TypeScript
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<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 }
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
})
|