feat: configure Pi models for CutOS agent
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const piProvider = require('../lib/pi-provider');
|
||||
|
||||
function tempConfig() {
|
||||
return path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'clawd-pi-')), 'models.json');
|
||||
}
|
||||
|
||||
function apply(provider, options) {
|
||||
return new Promise((resolve) => piProvider.applyFullProviderFromVps(provider, resolve, options));
|
||||
}
|
||||
|
||||
function refresh(options) {
|
||||
return new Promise((resolve) => piProvider.refreshModelsIfChanged(resolve, options));
|
||||
}
|
||||
|
||||
function read(file) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: 'cutos',
|
||||
'base-url': 'https://api.example.test',
|
||||
'api-key': 'test-key',
|
||||
};
|
||||
|
||||
test('creates a Pi models file and maps models to reasoning entries', async () => {
|
||||
const configFile = tempConfig();
|
||||
await apply(payload, {
|
||||
configFile,
|
||||
fetchModels: (_url, _key, done) => done(null, [
|
||||
{ id: 'gpt-test', reasoning: true },
|
||||
{ id: 'glm-test', reasoning: true },
|
||||
]),
|
||||
});
|
||||
assert.deepEqual(read(configFile), {
|
||||
providers: {
|
||||
cutos: {
|
||||
baseUrl: 'https://api.example.test/v1',
|
||||
api: 'openai-responses',
|
||||
apiKey: 'test-key',
|
||||
models: [
|
||||
{ id: 'gpt-test', reasoning: true },
|
||||
{ id: 'glm-test', reasoning: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves unrelated top-level fields and providers when upserting', async () => {
|
||||
const configFile = tempConfig();
|
||||
fs.writeFileSync(configFile, JSON.stringify({
|
||||
theme: 'dark',
|
||||
providers: { custom: { baseUrl: 'http://localhost:1234' } },
|
||||
}));
|
||||
await apply(payload, {
|
||||
configFile,
|
||||
fetchModels: (_url, _key, done) => done(null, [{ id: 'gpt-test', reasoning: true }]),
|
||||
});
|
||||
const config = read(configFile);
|
||||
assert.equal(config.theme, 'dark');
|
||||
assert.deepEqual(config.providers.custom, { baseUrl: 'http://localhost:1234' });
|
||||
assert.equal(config.providers.cutos.api, 'openai-responses');
|
||||
});
|
||||
|
||||
test('retains existing models when model refresh fails while updating credentials', async () => {
|
||||
const configFile = tempConfig();
|
||||
fs.writeFileSync(configFile, JSON.stringify({ providers: { cutos: {
|
||||
baseUrl: 'https://old.example/v1',
|
||||
api: 'openai-responses',
|
||||
apiKey: 'old-key',
|
||||
models: [{ id: 'existing-model', reasoning: true }],
|
||||
} } }));
|
||||
await apply(payload, {
|
||||
configFile,
|
||||
fetchModels: (_url, _key, done) => done(new Error('offline')),
|
||||
});
|
||||
const provider = read(configFile).providers.cutos;
|
||||
assert.equal(provider.apiKey, 'test-key');
|
||||
assert.deepEqual(provider.models, [{ id: 'existing-model', reasoning: true }]);
|
||||
});
|
||||
|
||||
test('removes only the named provider on unbind', () => {
|
||||
const configFile = tempConfig();
|
||||
fs.writeFileSync(configFile, JSON.stringify({ providers: { cutos: {}, custom: { keep: true } } }));
|
||||
piProvider.removeProviderByName('cutos', { configFile });
|
||||
assert.deepEqual(read(configFile), { providers: { custom: { keep: true } } });
|
||||
});
|
||||
|
||||
test('unbind cancels an in-flight provider write', async () => {
|
||||
const configFile = tempConfig();
|
||||
fs.writeFileSync(configFile, JSON.stringify({ providers: { cutos: {}, custom: { keep: true } } }));
|
||||
let finishFetch;
|
||||
const pending = apply(payload, {
|
||||
configFile,
|
||||
fetchModels: (_url, _key, done) => { finishFetch = done; },
|
||||
});
|
||||
piProvider.removeProviderByName('cutos', { configFile });
|
||||
finishFetch(null, [{ id: 'late-model', reasoning: true }]);
|
||||
await pending;
|
||||
assert.deepEqual(read(configFile), { providers: { custom: { keep: true } } });
|
||||
});
|
||||
|
||||
test('refresh updates models only when their ids change', async () => {
|
||||
const configFile = tempConfig();
|
||||
fs.writeFileSync(configFile, JSON.stringify({ providers: { cutos: {
|
||||
baseUrl: 'https://api.example.test/v1', api: 'openai-responses', apiKey: 'key',
|
||||
models: [{ id: 'old', reasoning: true }],
|
||||
} } }));
|
||||
await refresh({
|
||||
configFile,
|
||||
fetchModels: (_url, _key, done) => done(null, [{ id: 'new', reasoning: true }]),
|
||||
});
|
||||
assert.deepEqual(read(configFile).providers.cutos.models, [{ id: 'new', reasoning: true }]);
|
||||
});
|
||||
|
||||
test('fetchModels reads OpenAI model responses without exposing the key', async () => {
|
||||
const server = http.createServer((req, res) => {
|
||||
assert.equal(req.url, '/v1/models');
|
||||
assert.equal(req.headers.authorization, 'Bearer private-test-key');
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ data: [{ id: 'model-a' }, { id: 'model-b' }] }));
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const address = server.address();
|
||||
const models = await new Promise((resolve, reject) => {
|
||||
piProvider.fetchModels(`http://127.0.0.1:${address.port}`, 'private-test-key', (error, result) => {
|
||||
if (error) reject(error); else resolve(result);
|
||||
});
|
||||
});
|
||||
assert.deepEqual(models, [
|
||||
{ id: 'model-a', reasoning: true },
|
||||
{ id: 'model-b', reasoning: true },
|
||||
]);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user