b0f5d6a51c
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
401 lines
13 KiB
JavaScript
401 lines
13 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
createLlmProviderService,
|
|
CUSTOM_PROVIDER_ID,
|
|
decryptSecret,
|
|
encryptSecret,
|
|
LLM_PROVIDER_CATALOG,
|
|
LOCAL_LLM_FALLBACK,
|
|
maskApiKey,
|
|
normalizeApiUrl,
|
|
parseModelList,
|
|
resolveChatCompletionsUrl,
|
|
testLocalLlmConnection,
|
|
testRelayConnection,
|
|
syncProfileToGoosed,
|
|
} from './llm-providers.mjs';
|
|
|
|
test('encryptSecret round-trips with derived key', () => {
|
|
const encrypted = encryptSecret('sk-test-key-12345678', 'unit-test-secret');
|
|
const plain = decryptSecret(encrypted, 'unit-test-secret');
|
|
assert.equal(plain, 'sk-test-key-12345678');
|
|
});
|
|
|
|
test('parseModelList accepts comma and newline separated values', () => {
|
|
assert.deepEqual(parseModelList('qwen2.5:3b, llama3.2:1b\nmistral'), [
|
|
'qwen2.5:3b',
|
|
'llama3.2:1b',
|
|
'mistral',
|
|
]);
|
|
});
|
|
|
|
test('normalizeApiUrl adds http scheme', () => {
|
|
assert.equal(
|
|
normalizeApiUrl('127.0.0.1:18300/relay/buyer/v1/chat/completions'),
|
|
'http://127.0.0.1:18300/relay/buyer/v1/chat/completions',
|
|
);
|
|
});
|
|
|
|
test('maskApiKey hides middle segment', () => {
|
|
const masked = maskApiKey('sk-1234567890abcdef');
|
|
assert.ok(masked.startsWith('sk-1'));
|
|
assert.ok(masked.endsWith('cdef'));
|
|
assert.ok(masked.includes('*'));
|
|
});
|
|
|
|
test('createKey selects first profile and syncs to goosed', async () => {
|
|
const rows = [];
|
|
let selectedId = null;
|
|
const syncCalls = [];
|
|
|
|
const pool = {
|
|
async query(sql, params = []) {
|
|
if (sql.includes('COUNT(*) AS total FROM h5_llm_provider_keys')) {
|
|
return [[{ total: rows.length }]];
|
|
}
|
|
if (sql.includes('UPDATE h5_llm_provider_keys SET is_selected = 0')) {
|
|
for (const row of rows) row.is_selected = 0;
|
|
return [[]];
|
|
}
|
|
if (sql.includes('INSERT INTO h5_llm_provider_keys')) {
|
|
const encrypted = {
|
|
ciphertext: params[9],
|
|
iv: params[10],
|
|
tag: params[11],
|
|
};
|
|
const row = {
|
|
id: params[0],
|
|
provider_id: params[1],
|
|
provider_kind: params[2],
|
|
api_url: params[3],
|
|
base_path: params[4],
|
|
models_json: params[5],
|
|
goosed_provider_id: null,
|
|
engine: params[6],
|
|
relay_provider: params[7],
|
|
name: params[8],
|
|
api_key_ciphertext: encrypted.ciphertext,
|
|
api_key_iv: encrypted.iv,
|
|
api_key_tag: encrypted.tag,
|
|
default_model: params[12],
|
|
status: 'active',
|
|
is_selected: params[13],
|
|
created_at: params[14],
|
|
updated_at: params[15],
|
|
};
|
|
rows.push(row);
|
|
if (row.is_selected) selectedId = row.id;
|
|
return [[]];
|
|
}
|
|
if (sql.includes('SELECT * FROM h5_llm_provider_keys WHERE id = ?')) {
|
|
return [[rows.find((row) => row.id === params[0]) ?? null].filter(Boolean)];
|
|
}
|
|
if (sql.includes('ORDER BY is_selected DESC')) {
|
|
return [rows];
|
|
}
|
|
if (sql.includes('SELECT id FROM h5_llm_provider_keys WHERE name = ?')) {
|
|
return [[rows.find((row) => row.name === params[0])].filter(Boolean)];
|
|
}
|
|
if (sql.includes('SET goosed_provider_id = ?')) {
|
|
const target = rows.find((row) => row.id === params[3]);
|
|
if (target) {
|
|
target.goosed_provider_id = params[0];
|
|
target.provider_id = params[1];
|
|
}
|
|
return [[]];
|
|
}
|
|
throw new Error(`Unexpected SQL: ${sql}`);
|
|
},
|
|
};
|
|
|
|
const mockFetch = async (url, init) => {
|
|
syncCalls.push({ url: String(url), body: init?.body ? JSON.parse(init.body) : null });
|
|
if (String(url).includes('/config/custom-providers') && init?.method === 'POST') {
|
|
return { ok: true, json: async () => ({ provider_name: 'relay_ollama' }) };
|
|
}
|
|
return { ok: true, text: async () => '' };
|
|
};
|
|
|
|
const service = createLlmProviderService(pool, {
|
|
apiTarget: 'https://127.0.0.1:18006',
|
|
apiSecret: 'secret',
|
|
encryptionKey: 'unit-test-secret',
|
|
apiFetchImpl: mockFetch,
|
|
});
|
|
const result = await service.createKey({
|
|
providerId: 'custom_deepseek',
|
|
name: 'DeepSeek 主账号',
|
|
apiKey: 'sk-deepseek-test',
|
|
defaultModel: 'deepseek-chat',
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.key.isSelected, true);
|
|
assert.equal(rows.length, 1);
|
|
assert.equal(selectedId, rows[0].id);
|
|
assert.equal(syncCalls.filter((item) => item.body?.key === 'DEEPSEEK_API_KEY').length, 1);
|
|
});
|
|
|
|
test('create custom relay profile registers goosed custom provider', async () => {
|
|
const rows = [];
|
|
const mockFetch = async (url, init) => {
|
|
if (String(url).includes('/config/custom-providers') && init?.method === 'POST') {
|
|
const body = JSON.parse(init.body);
|
|
assert.equal(body.api_url, 'http://127.0.0.1:18300/relay/buyer/v1/chat/completions');
|
|
assert.deepEqual(body.models, ['qwen2.5:3b', 'llama3.2:1b']);
|
|
assert.equal(body.headers['X-Provider'], 'ollama');
|
|
return { ok: true, json: async () => ({ provider_name: 'relay_buyer' }) };
|
|
}
|
|
return { ok: true, text: async () => '' };
|
|
};
|
|
|
|
const pool = {
|
|
async query(sql, params = []) {
|
|
if (sql.includes('COUNT(*) AS total FROM h5_llm_provider_keys')) return [[{ total: 0 }]];
|
|
if (sql.includes('UPDATE h5_llm_provider_keys SET is_selected = 0')) return [[]];
|
|
if (sql.includes('INSERT INTO h5_llm_provider_keys')) {
|
|
rows.push({
|
|
id: params[0],
|
|
provider_id: params[1],
|
|
provider_kind: params[2],
|
|
api_url: params[3],
|
|
base_path: params[4],
|
|
models_json: params[5],
|
|
goosed_provider_id: null,
|
|
engine: params[6],
|
|
relay_provider: params[7],
|
|
name: params[8],
|
|
api_key_ciphertext: params[9],
|
|
api_key_iv: params[10],
|
|
api_key_tag: params[11],
|
|
default_model: params[12],
|
|
status: 'active',
|
|
is_selected: params[13],
|
|
created_at: params[14],
|
|
updated_at: params[15],
|
|
});
|
|
return [[]];
|
|
}
|
|
if (sql.includes('SELECT * FROM h5_llm_provider_keys WHERE id = ?')) {
|
|
return [[rows.find((row) => row.id === params[0]) ?? null].filter(Boolean)];
|
|
}
|
|
if (sql.includes('SELECT id FROM h5_llm_provider_keys WHERE name = ?')) return [[]];
|
|
if (sql.includes('SET goosed_provider_id = ?')) {
|
|
rows[0].goosed_provider_id = params[0];
|
|
rows[0].provider_id = params[1];
|
|
return [[]];
|
|
}
|
|
throw new Error(`Unexpected SQL: ${sql}`);
|
|
},
|
|
};
|
|
|
|
const service = createLlmProviderService(pool, {
|
|
apiTarget: 'https://127.0.0.1:18006',
|
|
apiSecret: 'secret',
|
|
encryptionKey: 'unit-test-secret',
|
|
apiFetchImpl: mockFetch,
|
|
});
|
|
const result = await service.createKey({
|
|
providerId: CUSTOM_PROVIDER_ID,
|
|
name: 'Relay Ollama',
|
|
apiKey: 'UqyHPKSSEZq0-oPnl8sru-7hZcJ2anPUL1yAVk866Vo',
|
|
apiUrl: 'http://127.0.0.1:18300/relay/buyer/v1/chat/completions',
|
|
models: ['qwen2.5:3b', 'llama3.2:1b'],
|
|
defaultModel: 'qwen2.5:3b',
|
|
relayProvider: 'ollama',
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.key.providerKind, 'custom');
|
|
assert.equal(rows[0].provider_kind, 'custom');
|
|
});
|
|
|
|
test('selectKey rejects disabled profile', async () => {
|
|
const row = {
|
|
id: 'key-1',
|
|
provider_id: 'custom_deepseek',
|
|
provider_kind: 'builtin',
|
|
name: 'disabled',
|
|
api_key_ciphertext: encryptSecret('sk-x', 'unit-test-secret').ciphertext,
|
|
api_key_iv: encryptSecret('sk-x', 'unit-test-secret').iv,
|
|
api_key_tag: encryptSecret('sk-x', 'unit-test-secret').tag,
|
|
default_model: 'deepseek-chat',
|
|
status: 'disabled',
|
|
is_selected: 0,
|
|
created_at: 1,
|
|
updated_at: 1,
|
|
};
|
|
const pool = {
|
|
async query(sql, params = []) {
|
|
if (sql.includes('SELECT * FROM h5_llm_provider_keys WHERE id = ?')) {
|
|
return [[row]];
|
|
}
|
|
throw new Error(`Unexpected SQL: ${sql}`);
|
|
},
|
|
};
|
|
const service = createLlmProviderService(pool, {
|
|
apiTarget: 'https://127.0.0.1:18006',
|
|
apiSecret: 'secret',
|
|
encryptionKey: 'unit-test-secret',
|
|
});
|
|
const result = await service.selectKey('key-1');
|
|
assert.equal(result.ok, false);
|
|
assert.match(result.message, /禁用/);
|
|
});
|
|
|
|
test('catalog includes custom provider template', () => {
|
|
const custom = LLM_PROVIDER_CATALOG.find((item) => item.id === CUSTOM_PROVIDER_ID);
|
|
assert.ok(custom);
|
|
assert.equal(custom.kind, 'custom');
|
|
});
|
|
|
|
test('resolveChatCompletionsUrl keeps full completions path', () => {
|
|
assert.equal(
|
|
resolveChatCompletionsUrl('http://127.0.0.1:18300/relay/buyer/v1/chat/completions'),
|
|
'http://127.0.0.1:18300/relay/buyer/v1/chat/completions',
|
|
);
|
|
});
|
|
|
|
test('testRelayConnection surfaces 401 as token error', async () => {
|
|
const mockFetch = async () => ({
|
|
ok: false,
|
|
status: 401,
|
|
text: async () => '{}',
|
|
});
|
|
const result = await testRelayConnection(
|
|
{
|
|
apiUrl: 'http://127.0.0.1:18300/relay/buyer/v1/chat/completions',
|
|
apiKey: 'bad-token',
|
|
model: 'qwen2.5:3b',
|
|
relayProvider: 'ollama',
|
|
},
|
|
mockFetch,
|
|
);
|
|
assert.equal(result.ok, false);
|
|
assert.match(result.message ?? '', /401/);
|
|
assert.match(result.message ?? '', /Token/);
|
|
});
|
|
|
|
test('testRelayConnection parses OpenAI-style response', async () => {
|
|
const mockFetch = async () => ({
|
|
ok: true,
|
|
text: async () =>
|
|
JSON.stringify({
|
|
choices: [{ message: { content: 'Hi there' } }],
|
|
}),
|
|
});
|
|
const result = await testRelayConnection(
|
|
{
|
|
apiUrl: 'http://127.0.0.1:18300/relay/buyer/v1/chat/completions',
|
|
apiKey: 'token',
|
|
model: 'qwen2.5:3b',
|
|
relayProvider: 'ollama',
|
|
},
|
|
mockFetch,
|
|
);
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.reply, 'Hi there');
|
|
});
|
|
|
|
test('syncProfileToGoosed writes provider, model and secret keys for builtin', async () => {
|
|
const writes = [];
|
|
const mockFetch = async (_url, init) => {
|
|
writes.push(JSON.parse(init.body));
|
|
return { ok: true, text: async () => '' };
|
|
};
|
|
await syncProfileToGoosed(
|
|
'https://127.0.0.1:18006',
|
|
'secret',
|
|
{
|
|
providerKind: 'builtin',
|
|
providerId: 'custom_deepseek',
|
|
defaultModel: 'deepseek-chat',
|
|
apiKey: 'sk-test',
|
|
},
|
|
mockFetch,
|
|
);
|
|
assert.deepEqual(
|
|
writes.map((item) => item.key),
|
|
['DEEPSEEK_API_KEY', 'GOOSE_PROVIDER', 'GOOSE_MODEL', 'TKMIND_PROVIDER', 'TKMIND_MODEL'],
|
|
);
|
|
});
|
|
|
|
test('syncProfileToGoosed tolerates missing /config/upsert endpoint', async () => {
|
|
const mockFetch = async (_url, init) => {
|
|
if (String(_url).includes('/config/custom-providers')) {
|
|
return {
|
|
ok: true,
|
|
json: async () => ({ provider_name: 'custom_local_ollama_7b' }),
|
|
};
|
|
}
|
|
if (String(_url).includes('/config/upsert')) {
|
|
return { ok: false, status: 404, text: async () => '' };
|
|
}
|
|
return { ok: true, text: async () => '' };
|
|
};
|
|
const providerId = await syncProfileToGoosed(
|
|
'https://127.0.0.1:18006',
|
|
'secret',
|
|
{
|
|
providerKind: 'custom',
|
|
providerId: CUSTOM_PROVIDER_ID,
|
|
name: 'Local Ollama 7B',
|
|
apiUrl: LOCAL_LLM_FALLBACK.apiUrl,
|
|
apiKey: LOCAL_LLM_FALLBACK.apiKey,
|
|
models: [LOCAL_LLM_FALLBACK.model],
|
|
defaultModel: LOCAL_LLM_FALLBACK.model,
|
|
engine: 'openai',
|
|
},
|
|
mockFetch,
|
|
);
|
|
assert.equal(providerId, 'custom_local_ollama_7b');
|
|
});
|
|
|
|
test('LOCAL_LLM_FALLBACK defaults to local ollama 7b', () => {
|
|
assert.equal(LOCAL_LLM_FALLBACK.model, 'qwen2.5:3b');
|
|
assert.match(LOCAL_LLM_FALLBACK.apiUrl, /11434/);
|
|
assert.equal(LOCAL_LLM_FALLBACK.enabled, true);
|
|
});
|
|
|
|
test('syncSelectedToGoosed does not auto-fallback to local when selected relay is down', async () => {
|
|
const encrypted = encryptSecret('token', 'unit-test-secret');
|
|
const row = {
|
|
id: 'key-relay',
|
|
provider_id: '__custom__',
|
|
provider_kind: 'custom',
|
|
name: 'Relay Buyer Ollama',
|
|
api_url: 'http://127.0.0.1:18300/relay/buyer/v1/chat/completions',
|
|
base_path: null,
|
|
models_json: JSON.stringify(['qwen2.5:3b']),
|
|
goosed_provider_id: null,
|
|
engine: 'openai',
|
|
relay_provider: 'ollama',
|
|
api_key_ciphertext: encrypted.ciphertext,
|
|
api_key_iv: encrypted.iv,
|
|
api_key_tag: encrypted.tag,
|
|
default_model: 'qwen2.5:3b',
|
|
status: 'active',
|
|
is_selected: 1,
|
|
created_at: 1,
|
|
updated_at: 1,
|
|
};
|
|
const pool = {
|
|
async query(sql) {
|
|
if (sql.includes('is_selected = 1')) return [[row]];
|
|
throw new Error(`Unexpected SQL: ${sql}`);
|
|
},
|
|
};
|
|
const mockFetch = async () => {
|
|
throw Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' });
|
|
};
|
|
const service = createLlmProviderService(pool, {
|
|
apiTarget: 'https://127.0.0.1:18006',
|
|
apiSecret: 'secret',
|
|
encryptionKey: 'unit-test-secret',
|
|
apiFetchImpl: mockFetch,
|
|
});
|
|
const result = await service.syncSelectedToGoosed();
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.synced, false);
|
|
assert.ok(result.message);
|
|
});
|