outlet-orm 6.5.0 → 9.0.0
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/bin/init.js +122 -0
- package/bin/mcp.js +78 -0
- package/bin/migrate.js +25 -0
- package/docs/skills/outlet-orm/ADVANCED.md +575 -0
- package/docs/skills/outlet-orm/AI.md +220 -0
- package/docs/skills/outlet-orm/API.md +522 -0
- package/docs/skills/outlet-orm/BACKUP.md +150 -0
- package/docs/skills/outlet-orm/MIGRATIONS.md +605 -0
- package/docs/skills/outlet-orm/MODELS.md +427 -0
- package/docs/skills/outlet-orm/QUERIES.md +345 -0
- package/docs/skills/outlet-orm/RELATIONS.md +555 -0
- package/docs/skills/outlet-orm/SECURITY.md +386 -0
- package/docs/skills/outlet-orm/SEEDS.md +98 -0
- package/docs/skills/outlet-orm/SKILL.md +205 -0
- package/docs/skills/outlet-orm/TYPESCRIPT.md +480 -0
- package/package.json +7 -3
- package/src/AI/AIPromptEnhancer.js +170 -0
- package/src/AI/AIQueryBuilder.js +234 -0
- package/src/AI/AIQueryOptimizer.js +185 -0
- package/src/AI/AISafetyGuardrails.js +146 -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 +798 -0
- package/src/AI/PromptGenerator.js +318 -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 +77 -1
- package/types/index.d.ts +432 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outlet ORM — Prompt-based Model Generator
|
|
3
|
+
* Parses natural language descriptions and generates models + migrations.
|
|
4
|
+
*
|
|
5
|
+
* Used by `outlet-init --prompt "..."` to bootstrap projects from descriptions.
|
|
6
|
+
*
|
|
7
|
+
* @since 7.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// ─── Domain pattern recognition ──────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const DOMAIN_PATTERNS = {
|
|
16
|
+
// E-commerce
|
|
17
|
+
'e-?commerce|shop|store|product|cart|order|payment|checkout': {
|
|
18
|
+
tables: {
|
|
19
|
+
users: { columns: ['name:string', 'email:string:unique', 'password:string', 'role:string:default(customer)'] },
|
|
20
|
+
products: { columns: ['name:string', 'description:text:nullable', 'price:decimal(10,2)', 'stock:integer:default(0)', 'sku:string:unique', 'category_id:foreignId'] },
|
|
21
|
+
categories: { columns: ['name:string', 'slug:string:unique', 'parent_id:integer:nullable'] },
|
|
22
|
+
orders: { columns: ['user_id:foreignId', 'status:string:default(pending)', 'total:decimal(10,2)', 'shipping_address:text'] },
|
|
23
|
+
order_items:{ columns: ['order_id:foreignId', 'product_id:foreignId', 'quantity:integer', 'price:decimal(10,2)'] },
|
|
24
|
+
payments: { columns: ['order_id:foreignId', 'method:string', 'amount:decimal(10,2)', 'status:string:default(pending)', 'transaction_id:string:nullable'] }
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Blog / CMS
|
|
29
|
+
'blog|article|post|cms|content|comment|tag': {
|
|
30
|
+
tables: {
|
|
31
|
+
users: { columns: ['name:string', 'email:string:unique', 'password:string', 'bio:text:nullable', 'avatar:string:nullable'] },
|
|
32
|
+
posts: { columns: ['user_id:foreignId', 'title:string', 'slug:string:unique', 'content:text', 'excerpt:text:nullable', 'status:string:default(draft)', 'published_at:timestamp:nullable'] },
|
|
33
|
+
categories: { columns: ['name:string', 'slug:string:unique', 'description:text:nullable'] },
|
|
34
|
+
tags: { columns: ['name:string', 'slug:string:unique'] },
|
|
35
|
+
post_tag: { columns: ['post_id:foreignId', 'tag_id:foreignId'], pivot: true },
|
|
36
|
+
comments: { columns: ['post_id:foreignId', 'user_id:foreignId:nullable', 'author_name:string:nullable', 'body:text', 'approved:boolean:default(false)'] }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Task / Project management
|
|
41
|
+
'task|project|todo|kanban|board|sprint|ticket': {
|
|
42
|
+
tables: {
|
|
43
|
+
users: { columns: ['name:string', 'email:string:unique', 'password:string', 'avatar:string:nullable'] },
|
|
44
|
+
projects: { columns: ['name:string', 'description:text:nullable', 'owner_id:foreignId', 'status:string:default(active)'] },
|
|
45
|
+
tasks: { columns: ['project_id:foreignId', 'assigned_to:integer:nullable', 'title:string', 'description:text:nullable', 'status:string:default(todo)', 'priority:string:default(medium)', 'due_date:date:nullable'] },
|
|
46
|
+
labels: { columns: ['name:string', 'color:string:default(#3498db)'] },
|
|
47
|
+
task_label: { columns: ['task_id:foreignId', 'label_id:foreignId'], pivot: true }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Social network
|
|
52
|
+
'social|friend|follow|like|feed|profile|message|chat': {
|
|
53
|
+
tables: {
|
|
54
|
+
users: { columns: ['name:string', 'email:string:unique', 'password:string', 'username:string:unique', 'bio:text:nullable', 'avatar:string:nullable'] },
|
|
55
|
+
posts: { columns: ['user_id:foreignId', 'content:text', 'media_url:string:nullable', 'visibility:string:default(public)'] },
|
|
56
|
+
comments: { columns: ['post_id:foreignId', 'user_id:foreignId', 'body:text'] },
|
|
57
|
+
likes: { columns: ['user_id:foreignId', 'likeable_id:integer', 'likeable_type:string'] },
|
|
58
|
+
follows: { columns: ['follower_id:foreignId', 'following_id:foreignId'] },
|
|
59
|
+
messages: { columns: ['sender_id:foreignId', 'receiver_id:foreignId', 'body:text', 'read_at:timestamp:nullable'] }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// SaaS / Multi-tenant
|
|
64
|
+
'saas|tenant|subscription|plan|billing|organization': {
|
|
65
|
+
tables: {
|
|
66
|
+
organizations: { columns: ['name:string', 'slug:string:unique', 'plan_id:foreignId:nullable'] },
|
|
67
|
+
users: { columns: ['organization_id:foreignId', 'name:string', 'email:string:unique', 'password:string', 'role:string:default(member)'] },
|
|
68
|
+
plans: { columns: ['name:string', 'slug:string:unique', 'price:decimal(8,2)', 'features:json:nullable', 'max_users:integer:default(5)'] },
|
|
69
|
+
subscriptions: { columns: ['organization_id:foreignId', 'plan_id:foreignId', 'status:string:default(active)', 'starts_at:timestamp', 'ends_at:timestamp:nullable', 'trial_ends_at:timestamp:nullable'] },
|
|
70
|
+
invoices: { columns: ['subscription_id:foreignId', 'amount:decimal(10,2)', 'status:string:default(pending)', 'paid_at:timestamp:nullable'] }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Habit tracker / Health
|
|
75
|
+
'habit|tracker|health|fitness|goal|streak|log': {
|
|
76
|
+
tables: {
|
|
77
|
+
users: { columns: ['name:string', 'email:string:unique', 'password:string', 'timezone:string:default(UTC)'] },
|
|
78
|
+
habits: { columns: ['user_id:foreignId', 'name:string', 'description:text:nullable', 'frequency:string:default(daily)', 'color:string:default(#3498db)', 'target:integer:default(1)'] },
|
|
79
|
+
logs: { columns: ['habit_id:foreignId', 'date:date', 'completed:boolean:default(false)', 'value:integer:default(0)', 'notes:text:nullable'] },
|
|
80
|
+
goals: { columns: ['user_id:foreignId', 'title:string', 'target_value:integer', 'current_value:integer:default(0)', 'deadline:date:nullable'] }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// API / Auth / Generic
|
|
85
|
+
'api|auth|rest|user': {
|
|
86
|
+
tables: {
|
|
87
|
+
users: { columns: ['name:string', 'email:string:unique', 'password:string', 'role:string:default(user)', 'email_verified_at:timestamp:nullable'] },
|
|
88
|
+
tokens: { columns: ['user_id:foreignId', 'token:string:unique', 'type:string:default(api)', 'expires_at:timestamp:nullable'] },
|
|
89
|
+
password_resets:{ columns: ['email:string', 'token:string', 'created_at:timestamp'] }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ─── Generator ──────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
class PromptGenerator {
|
|
97
|
+
/**
|
|
98
|
+
* Parse a natural language prompt and return a project blueprint.
|
|
99
|
+
* @param {string} prompt - e.g. "Create a blog application with comments and tags"
|
|
100
|
+
* @returns {{ domain: string, tables: object }}
|
|
101
|
+
*/
|
|
102
|
+
static parse(prompt) {
|
|
103
|
+
const lower = prompt.toLowerCase();
|
|
104
|
+
let bestMatch = null;
|
|
105
|
+
let bestScore = 0;
|
|
106
|
+
|
|
107
|
+
for (const [pattern, blueprint] of Object.entries(DOMAIN_PATTERNS)) {
|
|
108
|
+
const regex = new RegExp(pattern, 'i');
|
|
109
|
+
const keywords = pattern.split('|').map(k => k.replace(/[^a-z]/g, ''));
|
|
110
|
+
let score = 0;
|
|
111
|
+
|
|
112
|
+
for (const kw of keywords) {
|
|
113
|
+
if (lower.includes(kw)) score++;
|
|
114
|
+
}
|
|
115
|
+
if (regex.test(lower)) score += 2;
|
|
116
|
+
|
|
117
|
+
if (score > bestScore) {
|
|
118
|
+
bestScore = score;
|
|
119
|
+
bestMatch = { pattern, blueprint, score };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Default to generic API/Auth if no match
|
|
124
|
+
if (!bestMatch || bestScore < 1) {
|
|
125
|
+
bestMatch = { pattern: 'api|auth', blueprint: DOMAIN_PATTERNS['api|auth|rest|user'], score: 0 };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
domain: bestMatch.pattern.split('|')[0],
|
|
130
|
+
tables: bestMatch.blueprint.tables,
|
|
131
|
+
score: bestMatch.score
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate model files from a blueprint.
|
|
137
|
+
* @param {{ tables: object }} blueprint
|
|
138
|
+
* @param {string} outputDir - e.g. process.cwd() + '/models'
|
|
139
|
+
* @returns {string[]} created file paths
|
|
140
|
+
*/
|
|
141
|
+
static generateModels(blueprint, outputDir) {
|
|
142
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
143
|
+
const created = [];
|
|
144
|
+
|
|
145
|
+
for (const [tableName, config] of Object.entries(blueprint.tables)) {
|
|
146
|
+
if (config.pivot) continue; // Skip pivot tables for model files
|
|
147
|
+
|
|
148
|
+
const className = this._toClassName(tableName);
|
|
149
|
+
const fillable = config.columns
|
|
150
|
+
.map(c => c.split(':')[0])
|
|
151
|
+
.filter(c => !['id', 'created_at', 'updated_at'].includes(c));
|
|
152
|
+
|
|
153
|
+
const hidden = fillable.filter(c => c === 'password' || c.includes('token') || c.includes('secret'));
|
|
154
|
+
|
|
155
|
+
const content = `const { Model } = require('outlet-orm');
|
|
156
|
+
|
|
157
|
+
class ${className} extends Model {
|
|
158
|
+
static table = '${tableName}';
|
|
159
|
+
static fillable = ${JSON.stringify(fillable)};${hidden.length > 0 ? `\n static hidden = ${JSON.stringify(hidden)};` : ''}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = ${className};
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
const filePath = path.join(outputDir, `${className}.js`);
|
|
166
|
+
fs.writeFileSync(filePath, content);
|
|
167
|
+
created.push(filePath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return created;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Generate migration files from a blueprint.
|
|
175
|
+
* @param {{ tables: object }} blueprint
|
|
176
|
+
* @param {string} outputDir - e.g. process.cwd() + '/database/migrations'
|
|
177
|
+
* @returns {string[]} created file paths
|
|
178
|
+
*/
|
|
179
|
+
static generateMigrations(blueprint, outputDir) {
|
|
180
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
181
|
+
const created = [];
|
|
182
|
+
let index = 0;
|
|
183
|
+
|
|
184
|
+
for (const [tableName, config] of Object.entries(blueprint.tables)) {
|
|
185
|
+
index++;
|
|
186
|
+
const timestamp = new Date(Date.now() + index * 1000)
|
|
187
|
+
.toISOString()
|
|
188
|
+
.replace(/[-:]/g, '')
|
|
189
|
+
.replace(/T/, '_')
|
|
190
|
+
.replace(/\..+/, '');
|
|
191
|
+
|
|
192
|
+
const className = `Create${this._toClassName(tableName)}Table`;
|
|
193
|
+
const fileName = `${timestamp}_create_${tableName}_table.js`;
|
|
194
|
+
const filePath = path.join(outputDir, fileName);
|
|
195
|
+
|
|
196
|
+
const columnDefs = config.columns.map(c => this._columnToSchema(c)).join('\n ');
|
|
197
|
+
|
|
198
|
+
const content = `const { Migration } = require('outlet-orm');
|
|
199
|
+
|
|
200
|
+
class ${className} extends Migration {
|
|
201
|
+
async up() {
|
|
202
|
+
const schema = this.getSchema();
|
|
203
|
+
await schema.create('${tableName}', (table) => {
|
|
204
|
+
table.id();
|
|
205
|
+
${columnDefs}
|
|
206
|
+
table.timestamps();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async down() {
|
|
211
|
+
const schema = this.getSchema();
|
|
212
|
+
await schema.dropIfExists('${tableName}');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = ${className};
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
fs.writeFileSync(filePath, content);
|
|
220
|
+
created.push(filePath);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return created;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Generate a seed file from a blueprint.
|
|
228
|
+
* @param {{ tables: object }} blueprint
|
|
229
|
+
* @param {string} outputDir
|
|
230
|
+
* @returns {string}
|
|
231
|
+
*/
|
|
232
|
+
static generateSeeder(blueprint, outputDir) {
|
|
233
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
234
|
+
const tables = Object.keys(blueprint.tables);
|
|
235
|
+
|
|
236
|
+
const seedCalls = tables
|
|
237
|
+
.filter(t => !blueprint.tables[t].pivot)
|
|
238
|
+
.map(t => ` // await this.call('${this._toClassName(t)}Seeder');`)
|
|
239
|
+
.join('\n');
|
|
240
|
+
|
|
241
|
+
const content = `const { Seeder } = require('outlet-orm');
|
|
242
|
+
|
|
243
|
+
class DatabaseSeeder extends Seeder {
|
|
244
|
+
async run() {
|
|
245
|
+
${seedCalls}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = DatabaseSeeder;
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
const filePath = path.join(outputDir, 'DatabaseSeeder.js');
|
|
253
|
+
fs.writeFileSync(filePath, content);
|
|
254
|
+
return filePath;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Helpers ───────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
static _toClassName(tableName) {
|
|
260
|
+
// users -> User, order_items -> OrderItem
|
|
261
|
+
return tableName
|
|
262
|
+
.split('_')
|
|
263
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
264
|
+
.join('')
|
|
265
|
+
.replace(/s$/, ''); // naive singularize (plural -> singular)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static _columnToSchema(columnDef) {
|
|
269
|
+
// Format: name:type:modifier1:modifier2
|
|
270
|
+
// e.g. "email:string:unique", "price:decimal(10,2)", "status:string:default(pending)"
|
|
271
|
+
const parts = columnDef.split(':');
|
|
272
|
+
const name = parts[0];
|
|
273
|
+
const type = parts[1] || 'string';
|
|
274
|
+
const modifiers = parts.slice(2);
|
|
275
|
+
|
|
276
|
+
// Parse type and arguments
|
|
277
|
+
const typeMatch = type.match(/^(\w+)(?:\((.+)\))?$/);
|
|
278
|
+
const typeName = typeMatch ? typeMatch[1] : type;
|
|
279
|
+
const typeArgs = typeMatch && typeMatch[2] ? `, ${typeMatch[2]}` : '';
|
|
280
|
+
|
|
281
|
+
// Map to schema builder methods
|
|
282
|
+
const typeMap = {
|
|
283
|
+
string: 'string',
|
|
284
|
+
text: 'text',
|
|
285
|
+
integer: 'integer',
|
|
286
|
+
int: 'integer',
|
|
287
|
+
boolean: 'boolean',
|
|
288
|
+
bool: 'boolean',
|
|
289
|
+
decimal: 'decimal',
|
|
290
|
+
float: 'float',
|
|
291
|
+
date: 'date',
|
|
292
|
+
timestamp: 'timestamp',
|
|
293
|
+
json: 'json',
|
|
294
|
+
foreignId: 'integer'
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const schemaType = typeMap[typeName] || 'string';
|
|
298
|
+
let line = `table.${schemaType}('${name}'${typeArgs})`;
|
|
299
|
+
|
|
300
|
+
// Apply modifiers
|
|
301
|
+
for (const mod of modifiers) {
|
|
302
|
+
if (mod === 'unique') line += '.unique()';
|
|
303
|
+
else if (mod === 'nullable') line += '.nullable()';
|
|
304
|
+
else if (mod.startsWith('default(')) {
|
|
305
|
+
const val = mod.match(/default\((.+)\)/)?.[1] || '';
|
|
306
|
+
// Smart quoting: don't quote numbers or booleans
|
|
307
|
+
const isNum = !isNaN(val);
|
|
308
|
+
const isBool = val === 'true' || val === 'false';
|
|
309
|
+
const quoted = isNum || isBool ? val : `'${val}'`;
|
|
310
|
+
line += `.default(${quoted})`;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return line + ';';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = PromptGenerator;
|
|
@@ -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;
|