natureco-cli 4.4.1 → 4.5.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/bin/natureco.js +8 -0
- package/package.json +1 -1
- package/src/commands/doctor.js +24 -7
- package/src/commands/repl.js +333 -0
- package/src/utils/api.js +5 -1
package/bin/natureco.js
CHANGED
|
@@ -41,6 +41,7 @@ const medium = require('../src/commands/medium');
|
|
|
41
41
|
const seo = require('../src/commands/seo');
|
|
42
42
|
const xp = require('../src/commands/xp');
|
|
43
43
|
const team = require('../src/commands/team');
|
|
44
|
+
const repl = require('../src/commands/repl');
|
|
44
45
|
const capability = require('../src/commands/capability');
|
|
45
46
|
const commitments = require('../src/commands/commitments');
|
|
46
47
|
const completion = require('../src/commands/completion');
|
|
@@ -623,6 +624,13 @@ program
|
|
|
623
624
|
team(action ? [action, ...(params || [])] : []);
|
|
624
625
|
});
|
|
625
626
|
|
|
627
|
+
program
|
|
628
|
+
.command('repl [options...]')
|
|
629
|
+
.description('İnteraktif REPL — bizim bu konuşmamız gibi sohbet modu')
|
|
630
|
+
.action((options) => {
|
|
631
|
+
repl(options || []);
|
|
632
|
+
});
|
|
633
|
+
|
|
626
634
|
program
|
|
627
635
|
.command('memory [action] [params...]')
|
|
628
636
|
.description('Manage memory (status|list|search|show|clear|index|export|import|semantic|wiki)')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.1",
|
|
4
4
|
"description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"natureco": "bin/natureco.js"
|
package/src/commands/doctor.js
CHANGED
|
@@ -238,16 +238,33 @@ function runCheck(name) {
|
|
|
238
238
|
|
|
239
239
|
case 'secretsClean': {
|
|
240
240
|
try {
|
|
241
|
-
// Mevcut çalışma dizinini tara —
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
// Mevcut çalışma dizinini tara — sadece kritik bulguları rapor et
|
|
242
|
+
// Whitelist: .git, node_modules, .DS_Store, dist, build, *.md (dokümanlar),
|
|
243
|
+
// *.example, *.test, package-lock.json, audit-*.jsonl
|
|
244
|
+
// SKIP_DIRS secret-scanner.js'de zaten var (.git, node_modules, dist, build)
|
|
245
|
+
// Ama .DS_Store, .env.example gibi dosyaları atlamamız gerek
|
|
246
|
+
const findings = secrets.scanDir(process.cwd(), { maxFiles: 500 });
|
|
247
|
+
// False positive azaltma: sadece severity critical VEYA (.env/.key/secret içeren dosyalar)
|
|
248
|
+
const realSecrets = findings.filter(f => {
|
|
249
|
+
// .DS_Store, .md, .txt gibi dokümanları atla
|
|
250
|
+
const fname = (f.file || '').toLowerCase();
|
|
251
|
+
if (fname.endsWith('.md') || fname.endsWith('.txt')) return false;
|
|
252
|
+
if (fname.includes('.ds_store') || fname.includes('package-lock')) return false;
|
|
253
|
+
if (fname.includes('changelog') || fname.includes('readme')) return false;
|
|
254
|
+
// 'high' severity çoğunlukla false positive (40-char hex gibi)
|
|
255
|
+
// Sadece 'critical' VEYA bilinen provider pattern'i kabul et
|
|
256
|
+
if (f.severity === 'critical') return true;
|
|
257
|
+
// .env dosyalarında yüksek severity kabul
|
|
258
|
+
if (fname.includes('.env') && !fname.includes('.example')) return true;
|
|
259
|
+
return false;
|
|
260
|
+
});
|
|
261
|
+
if (realSecrets.length === 0) {
|
|
262
|
+
return { pass: true, message: 'Çalışma dizininde gerçek secret bulunamadı ✓' };
|
|
246
263
|
}
|
|
247
|
-
const sample =
|
|
264
|
+
const sample = realSecrets.slice(0, 3).map(f => `${f.type}@${path.basename(f.file || '?')}`).join(', ');
|
|
248
265
|
return {
|
|
249
266
|
pass: false,
|
|
250
|
-
message: `${
|
|
267
|
+
message: `${realSecrets.length} gerçek secret: ${sample}${realSecrets.length > 3 ? '...' : ''}`,
|
|
251
268
|
};
|
|
252
269
|
} catch (e) {
|
|
253
270
|
return { pass: false, message: e.message };
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* natureco repl — Interactive REPL mode (bizim bu konuşmamız gibi)
|
|
3
|
+
*
|
|
4
|
+
* Kullanım:
|
|
5
|
+
* natureco repl # interaktif sohbet başlat
|
|
6
|
+
* natureco repl --model M2.5 # farklı model
|
|
7
|
+
* natureco repl --no-stream # streaming kapalı
|
|
8
|
+
*
|
|
9
|
+
* Özellikler:
|
|
10
|
+
* - Mesaj yaz → Enter → AI cevap verir
|
|
11
|
+
* - Streaming response (token token gelir)
|
|
12
|
+
* - Mesaj geçmişi (sıra) context olarak gönderilir
|
|
13
|
+
* - Slash komutlar: /help /clear /exit /model /system
|
|
14
|
+
* - Ctrl+C ile temiz çıkış
|
|
15
|
+
* - Konuşma ~/.natureco/repl/<timestamp>.json'a kaydedilir
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const readline = require('readline');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const https = require('https');
|
|
23
|
+
const chalk = require('chalk');
|
|
24
|
+
const tui = require('../utils/tui');
|
|
25
|
+
|
|
26
|
+
const REPL_DIR = path.join(os.homedir(), '.natureco', 'repl');
|
|
27
|
+
|
|
28
|
+
function getConfig() {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.natureco', 'config.json'), 'utf8'));
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isMiniMax(url) {
|
|
37
|
+
return url && (url.includes('minimax.io') || url.includes('minimaxi.com') || url.includes('minimax.cn'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function apiRequest(providerUrl, providerApiKey, body, stream = false) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const isMM = isMiniMax(providerUrl);
|
|
43
|
+
const endpoint = isMM
|
|
44
|
+
? `${providerUrl.replace(/\/+$/, '')}/v1/text/chatcompletion_v2`
|
|
45
|
+
: `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
46
|
+
const req = https.request(endpoint, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Authorization': `Bearer ${providerApiKey}`,
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
},
|
|
52
|
+
timeout: 60000,
|
|
53
|
+
}, (res) => {
|
|
54
|
+
if (stream) {
|
|
55
|
+
resolve(res); // Stream response
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let data = '';
|
|
59
|
+
res.on('data', c => data += c);
|
|
60
|
+
res.on('end', () => {
|
|
61
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
62
|
+
try {
|
|
63
|
+
resolve(JSON.parse(data));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
reject(new Error(`Parse hatası: ${e.message}`));
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
req.on('error', reject);
|
|
73
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
74
|
+
req.write(JSON.stringify(body));
|
|
75
|
+
req.end();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stripAnsi(str) {
|
|
80
|
+
return String(str || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function sendStreaming(providerUrl, providerApiKey, messages, model, onChunk) {
|
|
84
|
+
const isMM = isMiniMax(providerUrl);
|
|
85
|
+
const body = {
|
|
86
|
+
model,
|
|
87
|
+
messages,
|
|
88
|
+
stream: !isMM, // MiniMax streaming farklı (bu basit versiyonda non-stream)
|
|
89
|
+
temperature: 0.7,
|
|
90
|
+
max_tokens: 2048,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!body.stream) {
|
|
94
|
+
// Non-stream fallback (MiniMax için)
|
|
95
|
+
const res = await apiRequest(providerUrl, providerApiKey, body, false);
|
|
96
|
+
const content = res.choices?.[0]?.message?.content || '';
|
|
97
|
+
for (const char of content) {
|
|
98
|
+
onChunk(char);
|
|
99
|
+
await new Promise(r => setTimeout(r, 8)); // Typewriter efekti
|
|
100
|
+
}
|
|
101
|
+
return content;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// OpenAI-compatible streaming
|
|
105
|
+
const endpoint = `${providerUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const req = https.request(endpoint, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Authorization': `Bearer ${providerApiKey}`,
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
},
|
|
113
|
+
timeout: 60000,
|
|
114
|
+
}, (res) => {
|
|
115
|
+
if (res.statusCode !== 200) {
|
|
116
|
+
let data = '';
|
|
117
|
+
res.on('data', c => data += c);
|
|
118
|
+
res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let buffer = '';
|
|
122
|
+
let full = '';
|
|
123
|
+
res.on('data', (chunk) => {
|
|
124
|
+
buffer += chunk.toString('utf8');
|
|
125
|
+
const lines = buffer.split('\n');
|
|
126
|
+
buffer = lines.pop() || '';
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
const trimmed = line.trim();
|
|
129
|
+
if (!trimmed || !trimmed.startsWith('data:')) continue;
|
|
130
|
+
const data = trimmed.slice(5).trim();
|
|
131
|
+
if (data === '[DONE]') {
|
|
132
|
+
resolve(full);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(data);
|
|
137
|
+
const delta = parsed.choices?.[0]?.delta?.content || '';
|
|
138
|
+
if (delta) {
|
|
139
|
+
full += delta;
|
|
140
|
+
onChunk(delta);
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
res.on('end', () => resolve(full));
|
|
146
|
+
});
|
|
147
|
+
req.on('error', reject);
|
|
148
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
149
|
+
req.write(JSON.stringify(body));
|
|
150
|
+
req.end();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function printHelp() {
|
|
155
|
+
console.log(chalk.cyan('\n 📚 REPL Komutları:\n'));
|
|
156
|
+
const cmds = [
|
|
157
|
+
['/help', 'Bu yardım mesajı'],
|
|
158
|
+
['/clear', 'Ekranı temizle'],
|
|
159
|
+
['/history', 'Konuşma geçmişini göster'],
|
|
160
|
+
['/system <text>', 'System prompt ayarla'],
|
|
161
|
+
['/model <name>', 'Model değiştir (örn: /model MiniMax-M3)'],
|
|
162
|
+
['/tokens', 'Token kullanımı'],
|
|
163
|
+
['/save', 'Konuşmayı kaydet'],
|
|
164
|
+
['/exit veya /quit', 'Çıkış (Ctrl+C de çalışır)'],
|
|
165
|
+
];
|
|
166
|
+
for (const [cmd, desc] of cmds) {
|
|
167
|
+
console.log(' ' + chalk.yellow(cmd.padEnd(20)) + chalk.gray(' ' + desc));
|
|
168
|
+
}
|
|
169
|
+
console.log('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function saveRepl(messages, provider, model) {
|
|
173
|
+
if (!fs.existsSync(REPL_DIR)) fs.mkdirSync(REPL_DIR, { recursive: true });
|
|
174
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
175
|
+
const file = path.join(REPL_DIR, `chat-${ts}.json`);
|
|
176
|
+
const data = {
|
|
177
|
+
timestamp: new Date().toISOString(),
|
|
178
|
+
provider,
|
|
179
|
+
model,
|
|
180
|
+
messages: messages.filter(m => m.role !== 'system' || !m._internal),
|
|
181
|
+
totalMessages: messages.length,
|
|
182
|
+
};
|
|
183
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
184
|
+
return file;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function startRepl(args) {
|
|
188
|
+
const cfg = getConfig();
|
|
189
|
+
let providerUrl = cfg.providerUrl;
|
|
190
|
+
let providerApiKey = cfg.providerApiKey;
|
|
191
|
+
let model = cfg.providerModel;
|
|
192
|
+
|
|
193
|
+
// Arg parse
|
|
194
|
+
for (let i = 0; i < args.length; i++) {
|
|
195
|
+
if (args[i] === '--model' && args[i + 1]) model = args[++i];
|
|
196
|
+
if (args[i] === '--no-stream') global.NO_STREAM = true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!providerUrl || !providerApiKey) {
|
|
200
|
+
console.log(chalk.red('\n ❌ Provider ayarlı değil. Önce: natureco setup\n'));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const messages = [];
|
|
205
|
+
let systemPrompt = 'Sen yardımcı bir AI asistansın. Türkçe konuş, kısa ve net cevap ver. Markdown kullanabilirsin.';
|
|
206
|
+
messages.push({ role: 'system', content: systemPrompt, _internal: true });
|
|
207
|
+
|
|
208
|
+
// Header
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(tui.styled(' 🌿 NatureCo REPL · İnteraktif Sohbet', { color: tui.PALETTE.primary, bold: true }));
|
|
211
|
+
console.log(tui.styled(' ' + '─'.repeat(50), { color: tui.PALETTE.border }));
|
|
212
|
+
console.log(tui.C.muted(' Provider: ') + tui.C.brand(providerUrl.replace(/https?:\/\//, '')));
|
|
213
|
+
console.log(tui.C.muted(' Model: ') + tui.C.brand(model));
|
|
214
|
+
console.log(tui.C.muted(' Komutlar için ') + tui.C.yellow('/help') + tui.C.muted(' · Çıkış için ') + tui.C.yellow('/exit') + tui.C.muted(' veya Ctrl+C'));
|
|
215
|
+
console.log('');
|
|
216
|
+
|
|
217
|
+
let totalInputTokens = 0;
|
|
218
|
+
let totalOutputTokens = 0;
|
|
219
|
+
|
|
220
|
+
const rl = readline.createInterface({
|
|
221
|
+
input: process.stdin,
|
|
222
|
+
output: process.stdout,
|
|
223
|
+
prompt: tui.styled('\n You ', { color: tui.PALETTE.primary, bold: true }),
|
|
224
|
+
terminal: true,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
rl.prompt();
|
|
228
|
+
|
|
229
|
+
const cleanup = () => {
|
|
230
|
+
if (messages.length > 1) {
|
|
231
|
+
const file = saveRepl(messages, providerUrl, model);
|
|
232
|
+
console.log(chalk.gray(`\n 💾 Konuşma kaydedildi: ${file}`));
|
|
233
|
+
}
|
|
234
|
+
console.log(chalk.gray('\n 👋 Görüşürüz!\n'));
|
|
235
|
+
process.exit(0);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
rl.on('SIGINT', cleanup);
|
|
239
|
+
process.on('SIGINT', cleanup);
|
|
240
|
+
process.on('SIGTERM', cleanup);
|
|
241
|
+
|
|
242
|
+
rl.on('line', async (input) => {
|
|
243
|
+
const line = input.trim();
|
|
244
|
+
if (!line) {
|
|
245
|
+
rl.prompt();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Slash komutlar
|
|
250
|
+
if (line.startsWith('/')) {
|
|
251
|
+
const [cmd, ...rest] = line.slice(1).split(/\s+/);
|
|
252
|
+
const arg = rest.join(' ');
|
|
253
|
+
|
|
254
|
+
switch (cmd) {
|
|
255
|
+
case 'help':
|
|
256
|
+
printHelp();
|
|
257
|
+
break;
|
|
258
|
+
case 'clear':
|
|
259
|
+
console.clear();
|
|
260
|
+
break;
|
|
261
|
+
case 'exit':
|
|
262
|
+
case 'quit':
|
|
263
|
+
case 'q':
|
|
264
|
+
cleanup();
|
|
265
|
+
return;
|
|
266
|
+
case 'history':
|
|
267
|
+
console.log(chalk.cyan('\n 📜 Konuşma Geçmişi:\n'));
|
|
268
|
+
for (const m of messages.filter(m => !m._internal)) {
|
|
269
|
+
const role = m.role === 'user' ? chalk.green('You') : chalk.blue('AI ');
|
|
270
|
+
const content = m.content.slice(0, 100) + (m.content.length > 100 ? '...' : '');
|
|
271
|
+
console.log(` ${role} ${content}`);
|
|
272
|
+
}
|
|
273
|
+
console.log('');
|
|
274
|
+
break;
|
|
275
|
+
case 'system':
|
|
276
|
+
if (!arg) {
|
|
277
|
+
console.log(chalk.yellow(' Kullanım: /system <text>'));
|
|
278
|
+
} else {
|
|
279
|
+
systemPrompt = arg;
|
|
280
|
+
messages[0] = { role: 'system', content: systemPrompt, _internal: true };
|
|
281
|
+
console.log(chalk.green(' ✓ System prompt güncellendi'));
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
case 'model':
|
|
285
|
+
if (!arg) {
|
|
286
|
+
console.log(chalk.yellow(' Kullanım: /model <name>'));
|
|
287
|
+
} else {
|
|
288
|
+
model = arg;
|
|
289
|
+
console.log(chalk.green(' ✓ Model: ') + chalk.cyan(model));
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
case 'tokens':
|
|
293
|
+
console.log(chalk.gray(` Token: ~${totalInputTokens} in / ~${totalOutputTokens} out`));
|
|
294
|
+
break;
|
|
295
|
+
case 'save':
|
|
296
|
+
const f = saveRepl(messages, providerUrl, model);
|
|
297
|
+
console.log(chalk.green(' ✓ Kaydedildi: ') + chalk.cyan(f));
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
console.log(chalk.yellow(` Bilinmeyen komut: /${cmd}. /help yazın.`));
|
|
301
|
+
}
|
|
302
|
+
rl.prompt();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// User mesajı ekle
|
|
307
|
+
messages.push({ role: 'user', content: line });
|
|
308
|
+
|
|
309
|
+
// AI cevabı al (streaming)
|
|
310
|
+
process.stdout.write(tui.styled('\n AI ', { color: tui.PALETTE.secondary, bold: true }));
|
|
311
|
+
try {
|
|
312
|
+
const apiMessages = messages.filter(m => !m._internal);
|
|
313
|
+
const reply = await sendStreaming(providerUrl, providerApiKey, apiMessages, model, (chunk) => {
|
|
314
|
+
process.stdout.write(chunk);
|
|
315
|
+
});
|
|
316
|
+
process.stdout.write('\n');
|
|
317
|
+
// Reply'i geçmişe ekle
|
|
318
|
+
messages.push({ role: 'assistant', content: reply });
|
|
319
|
+
// Token tahmini (basit: 4 char ≈ 1 token)
|
|
320
|
+
totalInputTokens += apiMessages.reduce((s, m) => s + Math.ceil((m.content || '').length / 4), 0);
|
|
321
|
+
totalOutputTokens += Math.ceil((reply || '').length / 4);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
process.stdout.write('\n');
|
|
324
|
+
console.log(chalk.red(' ❌ Hata: ' + err.message));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
rl.prompt();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
rl.on('close', cleanup);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = startRepl;
|
package/src/utils/api.js
CHANGED
|
@@ -888,7 +888,11 @@ async function streamProviderCompletion(providerConfig, messages, tools) {
|
|
|
888
888
|
|
|
889
889
|
async function streamOpenAICompletion(providerConfig, messages, tools) {
|
|
890
890
|
const baseUrl = providerConfig.url.replace(/\/+$/, '');
|
|
891
|
-
|
|
891
|
+
// MiniMax özel endpoint tespiti (streaming için de aynı)
|
|
892
|
+
const isMiniMax = baseUrl.includes('minimax.io') || baseUrl.includes('minimaxi.com') || baseUrl.includes('minimax.cn');
|
|
893
|
+
const endpoint = isMiniMax
|
|
894
|
+
? `${baseUrl}/v1/text/chatcompletion_v2`
|
|
895
|
+
: `${baseUrl}/chat/completions`;
|
|
892
896
|
|
|
893
897
|
const requestBody = {
|
|
894
898
|
model: providerConfig.model,
|