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.
@@ -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
+ };