outlet-orm 7.0.0 → 9.0.1
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/README.md +130 -2
- package/docs/skills/outlet-orm/AI.md +452 -102
- package/docs/skills/outlet-orm/API.md +108 -0
- package/docs/skills/outlet-orm/QUERIES.md +64 -0
- package/docs/skills/outlet-orm/SEEDS.md +47 -0
- package/docs/skills/outlet-orm/SKILL.md +15 -7
- package/package.json +1 -1
- package/src/AI/AIPromptEnhancer.js +170 -0
- package/src/AI/AIQueryBuilder.js +234 -0
- package/src/AI/AIQueryOptimizer.js +185 -0
- package/src/AI/AISeeder.js +181 -0
- package/src/AI/AiBridgeManager.js +287 -0
- package/src/AI/Builders/TextBuilder.js +170 -0
- package/src/AI/Contracts/AudioProviderContract.js +29 -0
- package/src/AI/Contracts/ChatProviderContract.js +38 -0
- package/src/AI/Contracts/EmbeddingsProviderContract.js +19 -0
- package/src/AI/Contracts/ImageProviderContract.js +19 -0
- package/src/AI/Contracts/ModelsProviderContract.js +26 -0
- package/src/AI/Contracts/ToolContract.js +25 -0
- package/src/AI/Facades/AiBridge.js +79 -0
- package/src/AI/MCPServer.js +113 -0
- package/src/AI/Providers/ClaudeProvider.js +64 -0
- package/src/AI/Providers/CustomOpenAIProvider.js +238 -0
- package/src/AI/Providers/GeminiProvider.js +68 -0
- package/src/AI/Providers/GrokProvider.js +46 -0
- package/src/AI/Providers/MistralProvider.js +21 -0
- package/src/AI/Providers/OllamaProvider.js +249 -0
- package/src/AI/Providers/OllamaTurboProvider.js +32 -0
- package/src/AI/Providers/OnnProvider.js +46 -0
- package/src/AI/Providers/OpenAIProvider.js +471 -0
- package/src/AI/Support/AudioNormalizer.js +37 -0
- package/src/AI/Support/ChatNormalizer.js +42 -0
- package/src/AI/Support/Document.js +77 -0
- package/src/AI/Support/DocumentAttachmentMapper.js +101 -0
- package/src/AI/Support/EmbeddingsNormalizer.js +30 -0
- package/src/AI/Support/Exceptions/ProviderError.js +22 -0
- package/src/AI/Support/FileSecurity.js +56 -0
- package/src/AI/Support/ImageNormalizer.js +62 -0
- package/src/AI/Support/JsonSchemaValidator.js +73 -0
- package/src/AI/Support/Message.js +40 -0
- package/src/AI/Support/StreamChunk.js +45 -0
- package/src/AI/Support/ToolChatRunner.js +160 -0
- package/src/AI/Support/ToolRegistry.js +62 -0
- package/src/AI/Tools/SystemInfoTool.js +25 -0
- package/src/index.js +67 -1
- package/types/index.d.ts +326 -0
package/src/AI/MCPServer.js
CHANGED
|
@@ -112,6 +112,33 @@ const TOOL_DEFINITIONS = [
|
|
|
112
112
|
},
|
|
113
113
|
required: ['filePath', 'consent']
|
|
114
114
|
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'ai_query',
|
|
118
|
+
description: 'Convert a natural language question into SQL and execute it. Requires an AI provider (AiBridge).',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
question: { type: 'string', description: 'Natural language question, e.g. "Show me the top 5 users by order count"' },
|
|
123
|
+
provider: { type: 'string', description: 'AI provider to use (default: openai)' },
|
|
124
|
+
model: { type: 'string', description: 'AI model to use (default: gpt-4o-mini)' },
|
|
125
|
+
safe_mode: { type: 'boolean', description: 'Only allow SELECT queries (default: true)' }
|
|
126
|
+
},
|
|
127
|
+
required: ['question']
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'query_optimize',
|
|
132
|
+
description: 'Analyze a SQL query using AI and return optimization suggestions, rewritten query, and index recommendations.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
sql: { type: 'string', description: 'The SQL query to optimize' },
|
|
137
|
+
provider: { type: 'string', description: 'AI provider to use (default: openai)' },
|
|
138
|
+
model: { type: 'string', description: 'AI model to use (default: gpt-4o-mini)' }
|
|
139
|
+
},
|
|
140
|
+
required: ['sql']
|
|
141
|
+
}
|
|
115
142
|
}
|
|
116
143
|
];
|
|
117
144
|
|
|
@@ -313,6 +340,8 @@ class MCPServer extends EventEmitter {
|
|
|
313
340
|
case 'model_list': return this._toolModelList();
|
|
314
341
|
case 'backup_create': return this._toolBackupCreate(args);
|
|
315
342
|
case 'backup_restore': return this._toolBackupRestore(args);
|
|
343
|
+
case 'ai_query': return this._toolAiQuery(args);
|
|
344
|
+
case 'query_optimize': return this._toolQueryOptimize(args);
|
|
316
345
|
default:
|
|
317
346
|
throw new Error(`Unknown tool: ${name}`);
|
|
318
347
|
}
|
|
@@ -610,6 +639,90 @@ class MCPServer extends EventEmitter {
|
|
|
610
639
|
return `Backup restored from: ${args.filePath}`;
|
|
611
640
|
}
|
|
612
641
|
|
|
642
|
+
// ── ai_query (NL → SQL) ───────────────────────────────────────
|
|
643
|
+
|
|
644
|
+
async _toolAiQuery(args) {
|
|
645
|
+
if (!args.question) throw new Error('A natural language question is required.');
|
|
646
|
+
const conn = await this._getConnection();
|
|
647
|
+
const manager = this._getAiBridgeManager();
|
|
648
|
+
if (!manager) throw new Error('AiBridge is not configured. Set OPENAI_API_KEY or configure a provider.');
|
|
649
|
+
|
|
650
|
+
const AIQueryBuilder = require('./AIQueryBuilder');
|
|
651
|
+
const builder = new AIQueryBuilder(manager, conn);
|
|
652
|
+
|
|
653
|
+
if (args.provider || args.model) {
|
|
654
|
+
builder.using(args.provider || 'openai', args.model || 'gpt-4o-mini');
|
|
655
|
+
}
|
|
656
|
+
if (args.safe_mode === false) {
|
|
657
|
+
builder.safeMode(false);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const result = await builder.query(args.question);
|
|
661
|
+
return {
|
|
662
|
+
sql: result.sql,
|
|
663
|
+
params: result.params,
|
|
664
|
+
explanation: result.explanation,
|
|
665
|
+
results: result.results,
|
|
666
|
+
error: result.error || null
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ── query_optimize ─────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
async _toolQueryOptimize(args) {
|
|
673
|
+
if (!args.sql) throw new Error('SQL query is required.');
|
|
674
|
+
const conn = await this._getConnection();
|
|
675
|
+
const manager = this._getAiBridgeManager();
|
|
676
|
+
if (!manager) throw new Error('AiBridge is not configured. Set OPENAI_API_KEY or configure a provider.');
|
|
677
|
+
|
|
678
|
+
const AIQueryOptimizer = require('./AIQueryOptimizer');
|
|
679
|
+
const optimizer = new AIQueryOptimizer(manager, conn);
|
|
680
|
+
|
|
681
|
+
if (args.provider || args.model) {
|
|
682
|
+
optimizer.using(args.provider || 'openai', args.model || 'gpt-4o-mini');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const result = await optimizer.optimize(args.sql);
|
|
686
|
+
return {
|
|
687
|
+
original: result.original,
|
|
688
|
+
optimized: result.optimized,
|
|
689
|
+
suggestions: result.suggestions,
|
|
690
|
+
explanation: result.explanation,
|
|
691
|
+
indexes: result.indexes
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ── AiBridge manager helper ────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Lazily creates an AiBridge manager from environment variables.
|
|
699
|
+
* @returns {import('./Bridge/AiBridgeManager')|null}
|
|
700
|
+
*/
|
|
701
|
+
_getAiBridgeManager() {
|
|
702
|
+
if (this._aiBridgeManager) return this._aiBridgeManager;
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
const AiBridgeManager = require('./AiBridgeManager');
|
|
706
|
+
const config = {};
|
|
707
|
+
|
|
708
|
+
// Auto-detect providers from env
|
|
709
|
+
if (process.env.OPENAI_API_KEY) config.openai = { api_key: process.env.OPENAI_API_KEY };
|
|
710
|
+
if (process.env.OLLAMA_ENDPOINT) config.ollama = { endpoint: process.env.OLLAMA_ENDPOINT };
|
|
711
|
+
if (process.env.CLAUDE_API_KEY) config.claude = { api_key: process.env.CLAUDE_API_KEY };
|
|
712
|
+
if (process.env.GEMINI_API_KEY) config.gemini = { api_key: process.env.GEMINI_API_KEY };
|
|
713
|
+
if (process.env.GROK_API_KEY) config.grok = { api_key: process.env.GROK_API_KEY };
|
|
714
|
+
if (process.env.MISTRAL_API_KEY) config.mistral = { api_key: process.env.MISTRAL_API_KEY };
|
|
715
|
+
if (process.env.ONN_API_KEY) config.onn = { api_key: process.env.ONN_API_KEY };
|
|
716
|
+
|
|
717
|
+
if (Object.keys(config).length === 0) return null;
|
|
718
|
+
|
|
719
|
+
this._aiBridgeManager = new AiBridgeManager(config);
|
|
720
|
+
return this._aiBridgeManager;
|
|
721
|
+
} catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
613
726
|
// ─── Template helpers ──────────────────────────────────────────
|
|
614
727
|
|
|
615
728
|
_extractTableName(migrationName) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ClaudeProvider
|
|
5
|
+
* Anthropic Messages API. System messages are converted to user role.
|
|
6
|
+
* Streaming is simulated via chunk-splitting (60-char chunks).
|
|
7
|
+
*/
|
|
8
|
+
class ClaudeProvider {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} apiKey
|
|
11
|
+
* @param {string} [endpoint='https://api.anthropic.com/v1/messages']
|
|
12
|
+
*/
|
|
13
|
+
constructor(apiKey, endpoint = 'https://api.anthropic.com/v1/messages') {
|
|
14
|
+
this.apiKey = apiKey;
|
|
15
|
+
this.endpoint = endpoint;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @private */
|
|
19
|
+
_headers() {
|
|
20
|
+
return {
|
|
21
|
+
'x-api-key': this.apiKey,
|
|
22
|
+
'anthropic-version': '2023-06-01',
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'Accept': 'application/json',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async chat(messages, options = {}) {
|
|
29
|
+
// Convert system messages to user role (Claude requirement)
|
|
30
|
+
const converted = messages.map(m => {
|
|
31
|
+
if ((m.role || '') === 'system') return { role: 'user', content: m.content };
|
|
32
|
+
return m;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const payload = {
|
|
36
|
+
model: options.model || 'claude-3-opus-20240229',
|
|
37
|
+
max_tokens: options.max_tokens || 512,
|
|
38
|
+
messages: converted,
|
|
39
|
+
};
|
|
40
|
+
if (options.temperature !== undefined) payload.temperature = options.temperature;
|
|
41
|
+
|
|
42
|
+
const res = await fetch(this.endpoint, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: this._headers(),
|
|
45
|
+
body: JSON.stringify(payload),
|
|
46
|
+
});
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
return data || {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async *stream(messages, options = {}) {
|
|
52
|
+
const full = await this.chat(messages, options);
|
|
53
|
+
let text = '';
|
|
54
|
+
if (full?.content?.[0]?.text) text = full.content[0].text;
|
|
55
|
+
// Simulated: yield 60-char chunks
|
|
56
|
+
for (let i = 0; i < text.length; i += 60) {
|
|
57
|
+
yield text.slice(i, i + 60);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
supportsStreaming() { return true; } // simulated
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = ClaudeProvider;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const JsonSchemaValidator = require('../Support/JsonSchemaValidator');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CustomOpenAIProvider
|
|
7
|
+
* Fully configurable OpenAI-compatible provider.
|
|
8
|
+
* Works with Azure OpenAI, proxies, OpenRouter, self-hosted endpoints, etc.
|
|
9
|
+
* Supports chat, streaming (SSE), embeddings, images, audio TTS/STT, and models.
|
|
10
|
+
*/
|
|
11
|
+
class CustomOpenAIProvider {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} apiKey
|
|
14
|
+
* @param {string} baseUrl
|
|
15
|
+
* @param {Object} [paths={}]
|
|
16
|
+
* @param {string} [authHeader='Authorization']
|
|
17
|
+
* @param {string} [authPrefix='Bearer ']
|
|
18
|
+
* @param {Object} [extraHeaders={}]
|
|
19
|
+
*/
|
|
20
|
+
constructor(apiKey, baseUrl, paths = {}, authHeader = 'Authorization', authPrefix = 'Bearer ', extraHeaders = {}) {
|
|
21
|
+
this.apiKey = apiKey;
|
|
22
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
23
|
+
this.paths = paths;
|
|
24
|
+
this.authHeader = authHeader;
|
|
25
|
+
this.authPrefix = authPrefix;
|
|
26
|
+
this.extraHeaders = extraHeaders;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @private */
|
|
30
|
+
_endpoint(key) {
|
|
31
|
+
const p = this.paths[key] || '';
|
|
32
|
+
return this.baseUrl + p;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @private */
|
|
36
|
+
_headers() {
|
|
37
|
+
return Object.assign({
|
|
38
|
+
[this.authHeader]: this.authPrefix + this.apiKey,
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'Accept': 'application/json',
|
|
41
|
+
}, this.extraHeaders);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @private */
|
|
45
|
+
async _post(url, body, stream = false) {
|
|
46
|
+
const opts = { method: 'POST', headers: this._headers(), body: JSON.stringify(body) };
|
|
47
|
+
if (stream) return fetch(url, opts);
|
|
48
|
+
const res = await fetch(url, opts);
|
|
49
|
+
return res.json();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @private */
|
|
53
|
+
async _get(url) {
|
|
54
|
+
const res = await fetch(url, { method: 'GET', headers: this._headers() });
|
|
55
|
+
return res.json();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Chat ───
|
|
59
|
+
async chat(messages, options = {}) {
|
|
60
|
+
const payload = this._buildChatPayload(messages, options);
|
|
61
|
+
const res = await this._post(this._endpoint('chat'), payload);
|
|
62
|
+
this._normalizeToolCallsOnResponse(res);
|
|
63
|
+
return res || {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @private */
|
|
67
|
+
_buildChatPayload(messages, options) {
|
|
68
|
+
const payload = {
|
|
69
|
+
model: options.model || options.deployment || 'gpt-like',
|
|
70
|
+
messages,
|
|
71
|
+
};
|
|
72
|
+
this._applySamplingOptions(payload, options);
|
|
73
|
+
this._applyResponseFormatOptions(payload, options);
|
|
74
|
+
this._applyToolsOptions(payload, options);
|
|
75
|
+
return payload;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @private */
|
|
79
|
+
_applySamplingOptions(payload, options) {
|
|
80
|
+
for (const k of ['temperature', 'top_p', 'max_tokens', 'frequency_penalty', 'presence_penalty', 'stop', 'seed', 'user']) {
|
|
81
|
+
if (options[k] !== undefined) payload[k] = options[k];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @private */
|
|
86
|
+
_applyResponseFormatOptions(payload, options) {
|
|
87
|
+
if (options.response_format === 'json') {
|
|
88
|
+
const schema = (options.json_schema || {}).schema || { type: 'object' };
|
|
89
|
+
payload.response_format = {
|
|
90
|
+
type: 'json_schema',
|
|
91
|
+
json_schema: options.json_schema || { name: 'auto_schema', schema },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @private */
|
|
97
|
+
_applyToolsOptions(payload, options) {
|
|
98
|
+
if (!options.tools || !Array.isArray(options.tools)) return;
|
|
99
|
+
payload.tools = options.tools.map(tool => ({
|
|
100
|
+
type: 'function',
|
|
101
|
+
function: {
|
|
102
|
+
name: tool.name,
|
|
103
|
+
description: tool.description || '',
|
|
104
|
+
parameters: tool.parameters || tool.schema || { type: 'object', properties: {} },
|
|
105
|
+
},
|
|
106
|
+
}));
|
|
107
|
+
if (options.tool_choice) payload.tool_choice = options.tool_choice;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @private */
|
|
111
|
+
_normalizeToolCallsOnResponse(res) {
|
|
112
|
+
if (!res || !res.choices?.[0]?.message?.tool_calls) return;
|
|
113
|
+
res.tool_calls = res.choices[0].message.tool_calls.map(tc => ({
|
|
114
|
+
id: tc.id || null,
|
|
115
|
+
name: (tc.function || {}).name || null,
|
|
116
|
+
arguments: (() => { try { return JSON.parse((tc.function || {}).arguments || '{}'); } catch { return {}; } })(),
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Streaming ───
|
|
121
|
+
async *stream(messages, options = {}) {
|
|
122
|
+
const payload = { model: options.model || 'gpt-like', messages, stream: true };
|
|
123
|
+
const res = await this._post(this._endpoint('chat'), payload, true);
|
|
124
|
+
yield* this._readSse(res.body);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async *streamEvents(messages, options = {}) {
|
|
128
|
+
const payload = { model: options.model || 'gpt-like', messages, stream: true };
|
|
129
|
+
const res = await this._post(this._endpoint('chat'), payload, true);
|
|
130
|
+
for await (const delta of this._readSse(res.body)) {
|
|
131
|
+
yield { type: 'delta', data: delta };
|
|
132
|
+
}
|
|
133
|
+
yield { type: 'end', data: null };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @private */
|
|
137
|
+
async *_readSse(body) {
|
|
138
|
+
const reader = body.getReader();
|
|
139
|
+
const decoder = new TextDecoder();
|
|
140
|
+
let buffer = '';
|
|
141
|
+
try {
|
|
142
|
+
while (true) {
|
|
143
|
+
const { done, value } = await reader.read();
|
|
144
|
+
if (done) break;
|
|
145
|
+
buffer += decoder.decode(value, { stream: true });
|
|
146
|
+
const lines = buffer.split(/\r?\n/);
|
|
147
|
+
buffer = lines.pop() || '';
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
const trimmed = line.trim();
|
|
150
|
+
if (trimmed === '' || trimmed.startsWith(':') || !trimmed.startsWith('data:')) continue;
|
|
151
|
+
const json = trimmed.slice(5).trim();
|
|
152
|
+
if (json === '[DONE]') return;
|
|
153
|
+
try {
|
|
154
|
+
const decoded = JSON.parse(json);
|
|
155
|
+
const delta = decoded?.choices?.[0]?.delta?.content || null;
|
|
156
|
+
if (delta !== null) yield delta;
|
|
157
|
+
} catch { /* skip */ }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
reader.releaseLock();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
supportsStreaming() { return true; }
|
|
166
|
+
|
|
167
|
+
// ─── Models ───
|
|
168
|
+
async listModels() {
|
|
169
|
+
const url = this.baseUrl + this._modelsPath();
|
|
170
|
+
return (await this._get(url)) || {};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getModel(id) {
|
|
174
|
+
const url = this.baseUrl + this._modelsPath() + '/' + encodeURIComponent(id);
|
|
175
|
+
return (await this._get(url)) || {};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @private */
|
|
179
|
+
_modelsPath() {
|
|
180
|
+
if (this.paths.models) return this.paths.models;
|
|
181
|
+
if (/\/v\d+(?:$|\/)/.test(this.baseUrl)) return '/models';
|
|
182
|
+
return '/v1/models';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Embeddings ───
|
|
186
|
+
async embeddings(inputs, options = {}) {
|
|
187
|
+
const payload = { model: options.model || 'embedding-model', input: inputs };
|
|
188
|
+
const res = await this._post(this._endpoint('embeddings'), payload);
|
|
189
|
+
return {
|
|
190
|
+
embeddings: (res.data || []).map(d => d.embedding || []),
|
|
191
|
+
usage: res.usage || {},
|
|
192
|
+
raw: res,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── Images ───
|
|
197
|
+
async generateImage(prompt, options = {}) {
|
|
198
|
+
const payload = { prompt, model: options.model || 'image-model', n: 1 };
|
|
199
|
+
const res = await this._post(this._endpoint('image'), payload);
|
|
200
|
+
return { images: res.data || [], raw: res };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Audio ───
|
|
204
|
+
async textToSpeech(text, options = {}) {
|
|
205
|
+
const payload = {
|
|
206
|
+
model: options.model || 'tts-model',
|
|
207
|
+
input: text,
|
|
208
|
+
voice: options.voice || 'alloy',
|
|
209
|
+
format: options.format || 'mp3',
|
|
210
|
+
};
|
|
211
|
+
const res = await fetch(this._endpoint('tts'), {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: this._headers(),
|
|
214
|
+
body: JSON.stringify(payload),
|
|
215
|
+
});
|
|
216
|
+
const arrayBuf = await res.arrayBuffer();
|
|
217
|
+
return { audio: Buffer.from(arrayBuf).toString('base64'), mime: 'audio/mpeg' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async speechToText(filePath, options = {}) {
|
|
221
|
+
const fs = require('fs');
|
|
222
|
+
const path = require('path');
|
|
223
|
+
const formData = new FormData();
|
|
224
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
225
|
+
const blob = new Blob([fileBuffer], { type: 'application/octet-stream' });
|
|
226
|
+
formData.append('file', blob, path.basename(filePath));
|
|
227
|
+
formData.append('model', options.model || 'stt-model');
|
|
228
|
+
formData.append('response_format', 'json');
|
|
229
|
+
|
|
230
|
+
const headers = { [this.authHeader]: this.authPrefix + this.apiKey };
|
|
231
|
+
Object.assign(headers, this.extraHeaders);
|
|
232
|
+
const res = await fetch(this._endpoint('stt'), { method: 'POST', headers, body: formData });
|
|
233
|
+
const data = await res.json();
|
|
234
|
+
return { text: data.text || '', raw: data };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = CustomOpenAIProvider;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GeminiProvider
|
|
5
|
+
* Google Generative Language API. Supports chat, simulated streaming, and embeddings.
|
|
6
|
+
*/
|
|
7
|
+
class GeminiProvider {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} apiKey
|
|
10
|
+
* @param {string} [chatEndpoint='https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent']
|
|
11
|
+
*/
|
|
12
|
+
constructor(apiKey, chatEndpoint = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent') {
|
|
13
|
+
this.apiKey = apiKey;
|
|
14
|
+
this.chatEndpoint = chatEndpoint;
|
|
15
|
+
this.embedEndpoint = 'https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @private */
|
|
19
|
+
_keyQuery() { return `?key=${this.apiKey}`; }
|
|
20
|
+
|
|
21
|
+
async chat(messages, options = {}) {
|
|
22
|
+
const userTexts = messages
|
|
23
|
+
.filter(m => (m.role || '') !== 'system')
|
|
24
|
+
.map(m => m.content);
|
|
25
|
+
|
|
26
|
+
const payload = {
|
|
27
|
+
contents: [{ parts: [{ text: userTexts.join('\n') }] }],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const res = await fetch(this.chatEndpoint + this._keyQuery(), {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify(payload),
|
|
34
|
+
});
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
return data || {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async *stream(messages, options = {}) {
|
|
40
|
+
const full = await this.chat(messages, options);
|
|
41
|
+
const text = full?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
42
|
+
for (let i = 0; i < text.length; i += 80) {
|
|
43
|
+
yield text.slice(i, i + 80);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
supportsStreaming() { return true; } // simulated
|
|
48
|
+
|
|
49
|
+
async embeddings(inputs, options = {}) {
|
|
50
|
+
const vectors = [];
|
|
51
|
+
for (const input of inputs) {
|
|
52
|
+
const payload = {
|
|
53
|
+
model: 'text-embedding-004',
|
|
54
|
+
content: { parts: [{ text: input }] },
|
|
55
|
+
};
|
|
56
|
+
const res = await fetch(this.embedEndpoint + this._keyQuery(), {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify(payload),
|
|
60
|
+
});
|
|
61
|
+
const data = await res.json();
|
|
62
|
+
vectors.push((data?.embedding || {}).values || []);
|
|
63
|
+
}
|
|
64
|
+
return { embeddings: vectors };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = GeminiProvider;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GrokProvider
|
|
5
|
+
* Prompt-based chat with simulated streaming (80-char chunks).
|
|
6
|
+
*/
|
|
7
|
+
class GrokProvider {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} apiKey
|
|
10
|
+
* @param {string} [endpoint='https://api.grok.com/v1/chat']
|
|
11
|
+
*/
|
|
12
|
+
constructor(apiKey, endpoint = 'https://api.grok.com/v1/chat') {
|
|
13
|
+
this.apiKey = apiKey;
|
|
14
|
+
this.endpoint = endpoint;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async chat(messages, options = {}) {
|
|
18
|
+
const joined = messages.map(m => m.content).join('\n');
|
|
19
|
+
const payload = {
|
|
20
|
+
prompt: joined,
|
|
21
|
+
model: options.model || 'grok-default',
|
|
22
|
+
};
|
|
23
|
+
const res = await fetch(this.endpoint, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(payload),
|
|
30
|
+
});
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
return data || {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async *stream(messages, options = {}) {
|
|
36
|
+
const full = await this.chat(messages, options);
|
|
37
|
+
const text = full.response || '';
|
|
38
|
+
for (let i = 0; i < text.length; i += 80) {
|
|
39
|
+
yield text.slice(i, i + 80);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
supportsStreaming() { return true; } // simulated
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = GrokProvider;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const OpenAIProvider = require('./OpenAIProvider');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MistralProvider
|
|
7
|
+
* Targets Mistral AI API (https://api.mistral.ai) — OpenAI-compatible endpoints.
|
|
8
|
+
*/
|
|
9
|
+
class MistralProvider extends OpenAIProvider {
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} apiKey
|
|
12
|
+
* @param {string} [chatEndpoint='https://api.mistral.ai/v1/chat/completions']
|
|
13
|
+
*/
|
|
14
|
+
constructor(apiKey, chatEndpoint = 'https://api.mistral.ai/v1/chat/completions') {
|
|
15
|
+
super(apiKey, chatEndpoint);
|
|
16
|
+
this.modelsEndpoint = 'https://api.mistral.ai/v1/models';
|
|
17
|
+
this.embeddingsEndpoint = 'https://api.mistral.ai/v1/embeddings';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = MistralProvider;
|