thumbgate 1.27.14 → 1.27.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +52 -6
- package/package.json +12 -3
- package/public/index.html +2 -2
- package/public/numbers.html +2 -2
- package/scripts/agent-readiness.js +6 -1
- package/scripts/cli-feedback.js +14 -4
- package/scripts/hob-pack.js +591 -0
- package/scripts/llm-client.js +114 -1
- package/scripts/omlx-smoke.js +192 -0
package/scripts/llm-client.js
CHANGED
|
@@ -16,6 +16,8 @@ const DEFAULT_MAX_TOKENS = 1024;
|
|
|
16
16
|
const DEFAULT_CACHE_TTL = '5m';
|
|
17
17
|
const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
|
|
18
18
|
const DEFAULT_ZAI_MODEL = MODELS.ZAI_CODING;
|
|
19
|
+
const DEFAULT_OMLX_BASE_URL = 'http://127.0.0.1:8000/v1';
|
|
20
|
+
const DEFAULT_OMLX_MODEL = 'qwen3-coder-next';
|
|
19
21
|
|
|
20
22
|
let _anthropicClient = null;
|
|
21
23
|
let _geminiClient = null;
|
|
@@ -32,14 +34,40 @@ function getZaiModel(env = process.env) {
|
|
|
32
34
|
return env.ZAI_API_MODEL || env.THUMBGATE_ZAI_MODEL || DEFAULT_ZAI_MODEL;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
function getOmlxApiKey(env = process.env) {
|
|
38
|
+
return env.OMLX_API_KEY || env.THUMBGATE_OMLX_API_KEY || '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getOmlxBaseUrl(env = process.env) {
|
|
42
|
+
return (env.OMLX_BASE_URL || env.THUMBGATE_OMLX_BASE_URL || DEFAULT_OMLX_BASE_URL)
|
|
43
|
+
.replace(/\/chat\/completions\/?$/i, '')
|
|
44
|
+
.replace(/\/+$/, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getOmlxModel(env = process.env) {
|
|
48
|
+
return env.OMLX_MODEL || env.THUMBGATE_OMLX_MODEL || DEFAULT_OMLX_MODEL;
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
function hasZaiApiKey(env = process.env) {
|
|
36
52
|
return Boolean(getZaiApiKey(env));
|
|
37
53
|
}
|
|
38
54
|
|
|
55
|
+
function hasOmlxConfig(env = process.env) {
|
|
56
|
+
return env.THUMBGATE_LLM_PROVIDER === 'omlx'
|
|
57
|
+
|| env.LLM_PROVIDER === 'omlx'
|
|
58
|
+
|| env.THUMBGATE_OMLX_ENABLED === '1'
|
|
59
|
+
|| env.OMLX_ENABLED === '1'
|
|
60
|
+
|| Boolean(env.OMLX_BASE_URL || env.THUMBGATE_OMLX_BASE_URL);
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
function isZaiProvider(provider = '') {
|
|
40
64
|
return /^(zai|z\.ai|glm|glm-coding)$/i.test(String(provider || '').trim());
|
|
41
65
|
}
|
|
42
66
|
|
|
67
|
+
function isOmlxProvider(provider = '') {
|
|
68
|
+
return /^(omlx|local-omlx|mlx-local)$/i.test(String(provider || '').trim());
|
|
69
|
+
}
|
|
70
|
+
|
|
43
71
|
function isZaiModel(model = '') {
|
|
44
72
|
return /^(glm|zai|z\.ai)/i.test(String(model || '').trim());
|
|
45
73
|
}
|
|
@@ -47,13 +75,14 @@ function isZaiModel(model = '') {
|
|
|
47
75
|
function isAvailable(provider = '') {
|
|
48
76
|
const normalizedProvider = String(provider || '').trim().toLowerCase();
|
|
49
77
|
if (isZaiProvider(normalizedProvider)) return hasZaiApiKey();
|
|
78
|
+
if (isOmlxProvider(normalizedProvider)) return hasOmlxConfig();
|
|
50
79
|
if (normalizedProvider === 'anthropic' || normalizedProvider === 'claude') {
|
|
51
80
|
return Boolean(process.env.ANTHROPIC_API_KEY);
|
|
52
81
|
}
|
|
53
82
|
if (normalizedProvider === 'gemini' || normalizedProvider === 'vertex') {
|
|
54
83
|
return Boolean(process.env.GEMINI_API_KEY || process.env.VERTEX_PROJECT_ID);
|
|
55
84
|
}
|
|
56
|
-
return Boolean(hasZaiApiKey() || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY || process.env.VERTEX_PROJECT_ID);
|
|
85
|
+
return Boolean(hasOmlxConfig() || hasZaiApiKey() || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY || process.env.VERTEX_PROJECT_ID);
|
|
57
86
|
}
|
|
58
87
|
|
|
59
88
|
function getClient() {
|
|
@@ -278,6 +307,51 @@ async function callZaiInternal(options = {}) {
|
|
|
278
307
|
}
|
|
279
308
|
}
|
|
280
309
|
|
|
310
|
+
async function callOmlxInternal(options = {}) {
|
|
311
|
+
if (!hasOmlxConfig()) return null;
|
|
312
|
+
if (typeof fetch !== 'function') return null;
|
|
313
|
+
|
|
314
|
+
const baseUrl = getOmlxBaseUrl();
|
|
315
|
+
const request = buildOpenAiCompatibleRequest({
|
|
316
|
+
...options,
|
|
317
|
+
model: options.model || getOmlxModel(),
|
|
318
|
+
});
|
|
319
|
+
const apiKey = getOmlxApiKey();
|
|
320
|
+
const headers = {
|
|
321
|
+
'Content-Type': 'application/json',
|
|
322
|
+
};
|
|
323
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const response = await runStep('llm.callOmlx', {
|
|
327
|
+
retries: 1,
|
|
328
|
+
logger: (msg) => console.warn(msg),
|
|
329
|
+
}, async () => fetch(`${baseUrl}/chat/completions`, {
|
|
330
|
+
method: 'POST',
|
|
331
|
+
headers,
|
|
332
|
+
body: JSON.stringify(request),
|
|
333
|
+
}));
|
|
334
|
+
|
|
335
|
+
if (!response || !response.ok) {
|
|
336
|
+
throw new Error(`oMLX request failed with status ${response?.status || 'unknown'}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const json = await response.json();
|
|
340
|
+
const text = stripCodeFences(json?.choices?.[0]?.message?.content || '');
|
|
341
|
+
return {
|
|
342
|
+
text,
|
|
343
|
+
usage: json?.usage || null,
|
|
344
|
+
stopReason: json?.choices?.[0]?.finish_reason || null,
|
|
345
|
+
id: json?.id || null,
|
|
346
|
+
model: json?.model || request.model,
|
|
347
|
+
provider: 'omlx',
|
|
348
|
+
};
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error('oMLX execution error:', err?.message || err);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
281
355
|
async function callGeminiInternal(options = {}) {
|
|
282
356
|
const env = process.env;
|
|
283
357
|
const { detectInferenceBackend } = require('./local-model-profile');
|
|
@@ -362,6 +436,9 @@ async function callClaudeInternal(options = {}) {
|
|
|
362
436
|
const modelName = options.model || '';
|
|
363
437
|
const provider = options.provider || process.env.THUMBGATE_LLM_PROVIDER || process.env.LLM_PROVIDER || '';
|
|
364
438
|
const requestedZai = isZaiProvider(provider) || isZaiModel(modelName);
|
|
439
|
+
if (isOmlxProvider(provider)) {
|
|
440
|
+
return callOmlxInternal(options);
|
|
441
|
+
}
|
|
365
442
|
if (requestedZai || (!process.env.ANTHROPIC_API_KEY && hasZaiApiKey() && !modelName.startsWith('gemini') && !modelName.startsWith('vertex'))) {
|
|
366
443
|
const zaiOptions = requestedZai ? options : { ...options, model: undefined };
|
|
367
444
|
return callZaiInternal(zaiOptions);
|
|
@@ -419,6 +496,7 @@ async function callClaudeJson(options = {}) {
|
|
|
419
496
|
stopReason: result.stopReason,
|
|
420
497
|
id: result.id,
|
|
421
498
|
model: result.model,
|
|
499
|
+
provider: result.provider || 'anthropic',
|
|
422
500
|
};
|
|
423
501
|
}
|
|
424
502
|
|
|
@@ -453,12 +531,42 @@ async function callZaiJson(options = {}) {
|
|
|
453
531
|
return parsed;
|
|
454
532
|
}
|
|
455
533
|
|
|
534
|
+
async function callOmlx(options = {}) {
|
|
535
|
+
const result = await callOmlxInternal(options);
|
|
536
|
+
if (!result) return null;
|
|
537
|
+
return options.returnMetadata ? result : result.text;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function callOmlxJson(options = {}) {
|
|
541
|
+
const result = await callOmlxInternal(options);
|
|
542
|
+
if (!result) return null;
|
|
543
|
+
|
|
544
|
+
const parsed = parseClaudeJson(result.text);
|
|
545
|
+
if (parsed === null) return null;
|
|
546
|
+
|
|
547
|
+
if (options.returnMetadata) {
|
|
548
|
+
return {
|
|
549
|
+
parsed,
|
|
550
|
+
text: result.text,
|
|
551
|
+
usage: result.usage,
|
|
552
|
+
stopReason: result.stopReason,
|
|
553
|
+
id: result.id,
|
|
554
|
+
model: result.model,
|
|
555
|
+
provider: result.provider,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return parsed;
|
|
560
|
+
}
|
|
561
|
+
|
|
456
562
|
module.exports = {
|
|
457
563
|
isAvailable,
|
|
458
564
|
callClaude,
|
|
459
565
|
callClaudeJson,
|
|
460
566
|
callZai,
|
|
461
567
|
callZaiJson,
|
|
568
|
+
callOmlx,
|
|
569
|
+
callOmlxJson,
|
|
462
570
|
stripCodeFences,
|
|
463
571
|
parseClaudeJson,
|
|
464
572
|
normalizeCacheOptions,
|
|
@@ -468,6 +576,11 @@ module.exports = {
|
|
|
468
576
|
getZaiApiKey,
|
|
469
577
|
getZaiBaseUrl,
|
|
470
578
|
getZaiModel,
|
|
579
|
+
getOmlxApiKey,
|
|
580
|
+
getOmlxBaseUrl,
|
|
581
|
+
getOmlxModel,
|
|
471
582
|
MODELS,
|
|
472
583
|
DEFAULT_ZAI_BASE_URL,
|
|
584
|
+
DEFAULT_OMLX_BASE_URL,
|
|
585
|
+
DEFAULT_OMLX_MODEL,
|
|
473
586
|
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
function loadDotEnv(filePath = path.join(process.cwd(), '.env')) {
|
|
8
|
+
if (!fs.existsSync(filePath)) return false;
|
|
9
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
10
|
+
for (const line of text.split(/\r?\n/)) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
13
|
+
const eq = trimmed.indexOf('=');
|
|
14
|
+
if (eq <= 0) continue;
|
|
15
|
+
const key = trimmed.slice(0, eq).trim();
|
|
16
|
+
const rawValue = trimmed.slice(eq + 1).trim();
|
|
17
|
+
if (!key || process.env[key]) continue;
|
|
18
|
+
process.env[key] = rawValue.replace(/^['"]|['"]$/g, '');
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv = []) {
|
|
24
|
+
const out = {
|
|
25
|
+
json: argv.includes('--json'),
|
|
26
|
+
requireLive: argv.includes('--require-live'),
|
|
27
|
+
chat: argv.includes('--chat'),
|
|
28
|
+
baseUrl: null,
|
|
29
|
+
model: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
+
if (argv[i] === '--base-url') out.baseUrl = argv[++i];
|
|
34
|
+
if (argv[i] === '--model') out.model = argv[++i];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeBaseUrl(baseUrl) {
|
|
41
|
+
return String(baseUrl || '')
|
|
42
|
+
.replace(/\/chat\/completions\/?$/i, '')
|
|
43
|
+
.replace(/\/models\/?$/i, '')
|
|
44
|
+
.replace(/\/+$/, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractModels(payload) {
|
|
48
|
+
const data = Array.isArray(payload?.data) ? payload.data : [];
|
|
49
|
+
return data
|
|
50
|
+
.map((entry) => {
|
|
51
|
+
if (typeof entry === 'string') return entry;
|
|
52
|
+
if (entry && typeof entry === 'object') return entry.id || entry.name || null;
|
|
53
|
+
return null;
|
|
54
|
+
})
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function fetchModels(baseUrl, fetchImpl = global.fetch) {
|
|
59
|
+
if (typeof fetchImpl !== 'function') {
|
|
60
|
+
return { ok: false, status: 'fetch_unavailable', models: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const url = `${normalizeBaseUrl(baseUrl)}/models`;
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetchImpl(url, { method: 'GET' });
|
|
66
|
+
if (!response || !response.ok) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
status: 'models_endpoint_failed',
|
|
70
|
+
httpStatus: response?.status || null,
|
|
71
|
+
url,
|
|
72
|
+
models: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const payload = await response.json();
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
status: 'models_ready',
|
|
79
|
+
httpStatus: response.status || 200,
|
|
80
|
+
url,
|
|
81
|
+
models: extractModels(payload),
|
|
82
|
+
};
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
status: 'models_endpoint_error',
|
|
87
|
+
url,
|
|
88
|
+
message: error?.message || String(error),
|
|
89
|
+
models: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function run(options = {}) {
|
|
95
|
+
loadDotEnv();
|
|
96
|
+
const {
|
|
97
|
+
callOmlx,
|
|
98
|
+
getOmlxBaseUrl,
|
|
99
|
+
getOmlxModel,
|
|
100
|
+
} = require('./llm-client');
|
|
101
|
+
|
|
102
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl || getOmlxBaseUrl());
|
|
103
|
+
const modelsResult = await fetchModels(baseUrl, options.fetchImpl || global.fetch);
|
|
104
|
+
const configuredModel = options.model || getOmlxModel();
|
|
105
|
+
const selectedModel = options.model || modelsResult.models[0] || configuredModel;
|
|
106
|
+
const result = {
|
|
107
|
+
ok: modelsResult.ok,
|
|
108
|
+
status: modelsResult.status,
|
|
109
|
+
provider: 'omlx',
|
|
110
|
+
baseUrl,
|
|
111
|
+
model: selectedModel,
|
|
112
|
+
models: modelsResult.models,
|
|
113
|
+
modelsUrl: modelsResult.url,
|
|
114
|
+
httpStatus: modelsResult.httpStatus || null,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (!modelsResult.ok) {
|
|
118
|
+
result.message = modelsResult.message || 'oMLX /v1/models is not reachable';
|
|
119
|
+
if (options.requireLive) process.exitCode = 1;
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!options.chat) {
|
|
124
|
+
result.status = modelsResult.models.length > 0 ? 'models_ready' : 'models_empty';
|
|
125
|
+
result.ok = !options.requireLive || modelsResult.models.length > 0;
|
|
126
|
+
if (!result.ok) process.exitCode = 1;
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
process.env.THUMBGATE_OMLX_ENABLED = '1';
|
|
131
|
+
process.env.THUMBGATE_OMLX_BASE_URL = baseUrl;
|
|
132
|
+
process.env.THUMBGATE_OMLX_MODEL = selectedModel;
|
|
133
|
+
|
|
134
|
+
const chat = await callOmlx({
|
|
135
|
+
systemPrompt: 'You are a local inference smoke test.',
|
|
136
|
+
userPrompt: 'Say that ThumbGate local oMLX inference is reachable.',
|
|
137
|
+
maxTokens: 64,
|
|
138
|
+
temperature: 0,
|
|
139
|
+
returnMetadata: true,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!chat || typeof chat.text !== 'string' || chat.text.trim().length < 1) {
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return {
|
|
145
|
+
...result,
|
|
146
|
+
ok: false,
|
|
147
|
+
status: 'chat_failed',
|
|
148
|
+
message: 'oMLX /v1/chat/completions did not return assistant text',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
...result,
|
|
154
|
+
ok: true,
|
|
155
|
+
status: 'ready',
|
|
156
|
+
model: chat.model || selectedModel,
|
|
157
|
+
textPreview: chat.text.slice(0, 200),
|
|
158
|
+
usage: chat.usage || null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function main(argv = process.argv.slice(2)) {
|
|
163
|
+
const options = parseArgs(argv);
|
|
164
|
+
const result = await run(options);
|
|
165
|
+
if (options.json) {
|
|
166
|
+
console.log(JSON.stringify(result, null, 2));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`oMLX smoke status: ${result.status}`);
|
|
171
|
+
console.log(`Provider: ${result.provider}`);
|
|
172
|
+
console.log(`Base URL: ${result.baseUrl}`);
|
|
173
|
+
console.log(`Model: ${result.model}`);
|
|
174
|
+
console.log(`Models: ${result.models.length}`);
|
|
175
|
+
console.log(`Result: ${result.ok ? 'ready' : result.message || 'not ready'}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (require.main === module) {
|
|
179
|
+
main().catch((error) => {
|
|
180
|
+
console.error(error?.message || error);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
loadDotEnv,
|
|
187
|
+
parseArgs,
|
|
188
|
+
normalizeBaseUrl,
|
|
189
|
+
extractModels,
|
|
190
|
+
fetchModels,
|
|
191
|
+
run,
|
|
192
|
+
};
|