@xisui-pa/chatserver 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +558 -2
- package/THIRD_PARTY_NOTICES.txt +179 -0
- package/dist/index.js +12295 -0
- package/dist/index.js.map +1 -0
- package/dist/models/chatserver-models.js +2740 -0
- package/dist/models/chatserver-models.js.map +1 -0
- package/package.json +67 -4
- package/schemas/config.schema.json +355 -0
- package/schemas/models.schema.json +103 -0
|
@@ -0,0 +1,2740 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/******/ (() => { // webpackBootstrap
|
|
3
|
+
/******/ "use strict";
|
|
4
|
+
/******/ var __webpack_modules__ = ({
|
|
5
|
+
|
|
6
|
+
/***/ 1977
|
|
7
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
14
|
+
exports.getDefaultConfigDir = getDefaultConfigDir;
|
|
15
|
+
const node_os_1 = __importDefault(__webpack_require__(8161));
|
|
16
|
+
const node_path_1 = __importDefault(__webpack_require__(6760));
|
|
17
|
+
/**
|
|
18
|
+
* Returns the platform-appropriate per-user config folder.
|
|
19
|
+
*/
|
|
20
|
+
function getDefaultConfigDir(platform = process.platform) {
|
|
21
|
+
const home = node_os_1.default.homedir();
|
|
22
|
+
switch (platform) {
|
|
23
|
+
case 'win32': {
|
|
24
|
+
const appData = process.env.APPDATA ?? node_path_1.default.join(home, 'AppData', 'Roaming');
|
|
25
|
+
return node_path_1.default.join(appData, 'chatserver');
|
|
26
|
+
}
|
|
27
|
+
case 'darwin':
|
|
28
|
+
return node_path_1.default.join(home, 'Library', 'Application Support', 'chatserver');
|
|
29
|
+
default: {
|
|
30
|
+
const configHome = process.env.XDG_CONFIG_HOME ?? node_path_1.default.join(home, '.config');
|
|
31
|
+
return node_path_1.default.join(configHome, 'chatserver');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/***/ },
|
|
38
|
+
|
|
39
|
+
/***/ 9258
|
|
40
|
+
(__unused_webpack_module, exports) {
|
|
41
|
+
|
|
42
|
+
var __webpack_unused_export__;
|
|
43
|
+
|
|
44
|
+
// Keep matching config schema defaults and bounds in sync when changing them here.
|
|
45
|
+
__webpack_unused_export__ = ({ value: true });
|
|
46
|
+
__webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = exports.MAX_TTS_INPUT_CHARS = exports.MAX_VOICE_PADDING_MS = exports.MIN_VOICE_PADDING_MS = exports.AUDIO_CLEANUP_RETRY_DELAY_MS = exports.AUDIO_CLEANUP_RETRIES = exports.AUDIO_CONVERSION_STDERR_CHARS = exports.AUDIO_CONVERSION_TIMEOUT_MS = exports.MAX_TRANSCRIPT_CHARS = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = exports.MAX_ENCODED_AUDIO_BYTES = exports.MAX_AUDIO_BYTES = __webpack_unused_export__ = exports.MAX_IMAGE_BYTES = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = void 0;
|
|
47
|
+
__webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = exports.MAX_WEB_TEXT_CHARS = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = exports.MAX_SETUP_CHOICES = exports.MODEL_DISCOVERY_TIMEOUT_MS = __webpack_unused_export__ = exports.MODEL_RETRY_DELAY_MS = exports.MAX_MODEL_RETRIES = exports.DEFAULT_MODEL_TIMEOUT_MS = exports.MAX_TIMER_MS = void 0;
|
|
48
|
+
// Model input and conversation snapshots.
|
|
49
|
+
/**
|
|
50
|
+
* Budget calculations with the current values:
|
|
51
|
+
* - Recent-message JSON: CONVERSATION_TEXT_TOKENS - CONVERSATION_METADATA_TOKENS
|
|
52
|
+
* = 8,000 - 1,500 = 6,500 tokens; the snapshot including metadata gets 8,000.
|
|
53
|
+
* - Combined text allowance: CONVERSATION_TEXT_TOKENS + MESSAGER_CONTEXT_TOKENS
|
|
54
|
+
* + MAX_MEMORY_TOKEN_LIMIT = 8,000 + 2,000 + 16,000 = 26,000 tokens.
|
|
55
|
+
* With the default memory limit: 8,000 + 2,000 + 1,000 = 11,000 tokens.
|
|
56
|
+
* Metadata is already included in CONVERSATION_TEXT_TOKENS; do not add it again.
|
|
57
|
+
* Actual memory uses messager.memoryTokenLimit; context and memory may be absent.
|
|
58
|
+
* These sums exclude instructions, tool definitions/results, image estimates, and
|
|
59
|
+
* wrapping serialization overhead. The complete input must fit MODEL_INPUT_TOKENS
|
|
60
|
+
* (64,000 tokens), checked separately for each model request.
|
|
61
|
+
*/
|
|
62
|
+
/** Text budget for the recent-message snapshot and its metadata. */
|
|
63
|
+
__webpack_unused_export__ = 8_000;
|
|
64
|
+
/** Snapshot tokens reserved for metadata rather than message text. */
|
|
65
|
+
__webpack_unused_export__ = 1_500;
|
|
66
|
+
/** Maximum recent source messages retained per conversation. */
|
|
67
|
+
__webpack_unused_export__ = 30;
|
|
68
|
+
/** Maximum text tokens retained from one source message. */
|
|
69
|
+
__webpack_unused_export__ = 2_000;
|
|
70
|
+
/** Fallback text budget when the last snapshot message is still too large. */
|
|
71
|
+
__webpack_unused_export__ = 500;
|
|
72
|
+
/** Maximum tokens for a sender or channel identifier in model input. */
|
|
73
|
+
__webpack_unused_export__ = 100;
|
|
74
|
+
/** Maximum tokens for one sender display name in conversational context. */
|
|
75
|
+
__webpack_unused_export__ = 100;
|
|
76
|
+
/** Separate token budget for the optional messager context file. */
|
|
77
|
+
__webpack_unused_export__ = 2_000;
|
|
78
|
+
/** Total application input budget, including instructions, tools, and images. */
|
|
79
|
+
__webpack_unused_export__ = 64_000;
|
|
80
|
+
/** Maximum retained tokens from one tool result. */
|
|
81
|
+
__webpack_unused_export__ = 8_000;
|
|
82
|
+
/** Estimated input tokens reserved per image, independent of provider billing. */
|
|
83
|
+
__webpack_unused_export__ = 2_048;
|
|
84
|
+
/** Tokens reserved for the truncation marker when bounding text. */
|
|
85
|
+
__webpack_unused_export__ = 8;
|
|
86
|
+
// Public replies and persistent conversation memory.
|
|
87
|
+
/** Prompt target for a complete, concise public reply. */
|
|
88
|
+
__webpack_unused_export__ = 150;
|
|
89
|
+
/** Hard token cap applied to outgoing reply text. */
|
|
90
|
+
__webpack_unused_export__ = 200;
|
|
91
|
+
/** Default per-conversation budget for memory notes and confirmed replies. */
|
|
92
|
+
__webpack_unused_export__ = 1_000;
|
|
93
|
+
/** Smallest configurable memory budget, allowing room for entry metadata. */
|
|
94
|
+
__webpack_unused_export__ = 100;
|
|
95
|
+
/** Largest configurable memory budget and memory input allowance. */
|
|
96
|
+
__webpack_unused_export__ = 16_000;
|
|
97
|
+
/** Memory lifetime measured from the earliest original source. */
|
|
98
|
+
__webpack_unused_export__ = 3 * 24 * 60 * 60 * 1_000;
|
|
99
|
+
/** Interval between cleanup passes over inactive memory files. */
|
|
100
|
+
__webpack_unused_export__ = 60 * 60 * 1_000;
|
|
101
|
+
/** Maximum memory file size accepted from disk. */
|
|
102
|
+
__webpack_unused_export__ = 256 * 1_024;
|
|
103
|
+
/** Maximum retained text tokens in a single memory note. */
|
|
104
|
+
__webpack_unused_export__ = 300;
|
|
105
|
+
/** Maximum characters accepted in a model-proposed memory note. */
|
|
106
|
+
__webpack_unused_export__ = 4_000;
|
|
107
|
+
/** Maximum new or replacement memory notes proposed per turn. */
|
|
108
|
+
__webpack_unused_export__ = 20;
|
|
109
|
+
/** Maximum existing note IDs the model may forget per turn. */
|
|
110
|
+
__webpack_unused_export__ = 50;
|
|
111
|
+
/** Maximum supporting source IDs per proposed memory note. */
|
|
112
|
+
__webpack_unused_export__ = 64;
|
|
113
|
+
/** Maximum characters in a stored memory ID or cited source ID. */
|
|
114
|
+
__webpack_unused_export__ = 128;
|
|
115
|
+
// Images, voice input, and local audio conversion.
|
|
116
|
+
/** Default maximum generated image width in pixels. */
|
|
117
|
+
__webpack_unused_export__ = 1024;
|
|
118
|
+
/** Default maximum generated image height in pixels. */
|
|
119
|
+
__webpack_unused_export__ = 1024;
|
|
120
|
+
/** Daily image generation allowance shared by a messager's conversations. */
|
|
121
|
+
__webpack_unused_export__ = 10;
|
|
122
|
+
/** Maximum decoded bytes in one received image. */
|
|
123
|
+
exports.MAX_IMAGE_BYTES = 5 * 1_024 * 1_024;
|
|
124
|
+
/** Maximum base64 characters encoding an allowed image. */
|
|
125
|
+
__webpack_unused_export__ = 4 * Math.ceil(exports.MAX_IMAGE_BYTES / 3);
|
|
126
|
+
/** Maximum decoded bytes in one voice recording or converted audio file. */
|
|
127
|
+
exports.MAX_AUDIO_BYTES = 20 * 1_024 * 1_024;
|
|
128
|
+
/** Maximum base64 characters encoding an allowed voice recording. */
|
|
129
|
+
exports.MAX_ENCODED_AUDIO_BYTES = 4 * Math.ceil(exports.MAX_AUDIO_BYTES / 3);
|
|
130
|
+
/** Maximum images attached to a single model snapshot. */
|
|
131
|
+
__webpack_unused_export__ = 4;
|
|
132
|
+
/** Maximum persisted images per conversation; oldest images are evicted first. */
|
|
133
|
+
__webpack_unused_export__ = 30;
|
|
134
|
+
/** Maximum bytes in a conversation's image map, separate from its text memory. */
|
|
135
|
+
__webpack_unused_export__ = 64 * 1_024 * 1_024;
|
|
136
|
+
/** Prompt allowance for references to older cached images, excluding image bytes. */
|
|
137
|
+
__webpack_unused_export__ = 1_000;
|
|
138
|
+
/** Maximum download attempts per image or voice source. */
|
|
139
|
+
__webpack_unused_export__ = 3;
|
|
140
|
+
/** Extra JSON response bytes allowed around base64 media. */
|
|
141
|
+
__webpack_unused_export__ = 64 * 1_024;
|
|
142
|
+
/** Maximum characters accepted from a speech-to-text model. */
|
|
143
|
+
exports.MAX_TRANSCRIPT_CHARS = 32_000;
|
|
144
|
+
/** Default deadline for a local audio conversion command. */
|
|
145
|
+
exports.AUDIO_CONVERSION_TIMEOUT_MS = 30_000;
|
|
146
|
+
/** Maximum stderr characters retained from an audio converter. */
|
|
147
|
+
exports.AUDIO_CONVERSION_STDERR_CHARS = 8_192;
|
|
148
|
+
/** Maximum retries when deleting audio conversion temporary files. */
|
|
149
|
+
exports.AUDIO_CLEANUP_RETRIES = 3;
|
|
150
|
+
/** Delay between retries deleting conversion temporary files. */
|
|
151
|
+
exports.AUDIO_CLEANUP_RETRY_DELAY_MS = 100;
|
|
152
|
+
/** Minimum independently sampled silence before and after a synthesized reply. */
|
|
153
|
+
exports.MIN_VOICE_PADDING_MS = 500;
|
|
154
|
+
/** Inclusive maximum independently sampled silence at each end of a voice reply. */
|
|
155
|
+
exports.MAX_VOICE_PADDING_MS = 2_000;
|
|
156
|
+
/** Maximum characters submitted to a speech synthesis endpoint. */
|
|
157
|
+
exports.MAX_TTS_INPUT_CHARS = 4_096;
|
|
158
|
+
/** Maximum wait for a WeChat voice job to confirm delivery. */
|
|
159
|
+
__webpack_unused_export__ = 180_000;
|
|
160
|
+
/** Interval between status checks of a submitted WeChat voice job. */
|
|
161
|
+
__webpack_unused_export__ = 1_000;
|
|
162
|
+
/** Deadline for a best-effort voice-job cancellation after failure or shutdown. */
|
|
163
|
+
__webpack_unused_export__ = 5_000;
|
|
164
|
+
// Model requests, pacing, and transport defaults.
|
|
165
|
+
/** Largest duration supported by Node timers; a platform bound. */
|
|
166
|
+
exports.MAX_TIMER_MS = 2_147_483_647;
|
|
167
|
+
/** Default provider deadline for generation and transcription requests. */
|
|
168
|
+
exports.DEFAULT_MODEL_TIMEOUT_MS = 180_000;
|
|
169
|
+
/** Retries after the initial attempt of a complete model generation. */
|
|
170
|
+
exports.MAX_MODEL_RETRIES = 3;
|
|
171
|
+
/** Delay between complete model-generation retries. */
|
|
172
|
+
exports.MODEL_RETRY_DELAY_MS = 10_000;
|
|
173
|
+
/** Maximum calls per tool category and local tool-producing rounds per generation attempt. */
|
|
174
|
+
__webpack_unused_export__ = 8;
|
|
175
|
+
/** Maximum wait for model discovery in guided setup. */
|
|
176
|
+
exports.MODEL_DISCOVERY_TIMEOUT_MS = 10_000;
|
|
177
|
+
/** Maximum choices printed when guided setup also accepts a custom value. */
|
|
178
|
+
exports.MAX_SETUP_CHOICES = 30;
|
|
179
|
+
/** Minimum random delay before a reply slot becomes available. */
|
|
180
|
+
__webpack_unused_export__ = 60_000;
|
|
181
|
+
/** Inclusive maximum random delay before a reply slot becomes available. */
|
|
182
|
+
__webpack_unused_export__ = 180_000;
|
|
183
|
+
/** Maximum deferred conversation retries for pending media. */
|
|
184
|
+
__webpack_unused_export__ = 2;
|
|
185
|
+
/** Initial delay for pending-media retries; doubles on each retry. */
|
|
186
|
+
__webpack_unused_export__ = 30_000;
|
|
187
|
+
/** Default delay after a completed WeChat poll. */
|
|
188
|
+
__webpack_unused_export__ = 30_000;
|
|
189
|
+
/** Default deadline for each WeChat REST request. */
|
|
190
|
+
__webpack_unused_export__ = 30_000;
|
|
191
|
+
/** Default maximum timestamp gap for adjacent same-sender bundles. */
|
|
192
|
+
__webpack_unused_export__ = 20_000;
|
|
193
|
+
/** Maximum exponent used by WeChat polling failure backoff. */
|
|
194
|
+
__webpack_unused_export__ = 6;
|
|
195
|
+
/** Chat rows requested per WeChat API page. */
|
|
196
|
+
__webpack_unused_export__ = 100;
|
|
197
|
+
/** Message rows requested per WeChat API page. */
|
|
198
|
+
__webpack_unused_export__ = 50;
|
|
199
|
+
/** Default interval between ignore-file reload checks; zero disables caching. */
|
|
200
|
+
__webpack_unused_export__ = 1_000;
|
|
201
|
+
// Tool execution and web content.
|
|
202
|
+
/** Maximum characters in one serialized tool argument object. */
|
|
203
|
+
__webpack_unused_export__ = 16_384;
|
|
204
|
+
/** Maximum characters in one serialized tool execution result. */
|
|
205
|
+
__webpack_unused_export__ = 32_768;
|
|
206
|
+
/** Maximum transferred bytes in a downloaded web page. */
|
|
207
|
+
__webpack_unused_export__ = 1_048_576;
|
|
208
|
+
/** Maximum decoded bytes in a web page after decompression. */
|
|
209
|
+
__webpack_unused_export__ = 1_048_576;
|
|
210
|
+
/** Shared deadline for a web download and all its redirects. */
|
|
211
|
+
__webpack_unused_export__ = 15_000;
|
|
212
|
+
/** Maximum redirects followed while reading a public web page. */
|
|
213
|
+
__webpack_unused_export__ = 5;
|
|
214
|
+
/** Maximum characters in a URL accepted by read_url. */
|
|
215
|
+
__webpack_unused_export__ = 2_048;
|
|
216
|
+
/** Maximum readable characters extracted from one web page. */
|
|
217
|
+
exports.MAX_WEB_TEXT_CHARS = 20_000;
|
|
218
|
+
/** Text-length threshold for marking a page as possibly truncated. */
|
|
219
|
+
__webpack_unused_export__ = exports.MAX_WEB_TEXT_CHARS - 100;
|
|
220
|
+
/** Maximum HTML nodes visited during traversal or text extraction. */
|
|
221
|
+
__webpack_unused_export__ = 100_000;
|
|
222
|
+
/** Maximum characters retained in a web page title. */
|
|
223
|
+
__webpack_unused_export__ = 500;
|
|
224
|
+
/** Maximum results returned by the built-in web search provider. */
|
|
225
|
+
__webpack_unused_export__ = 5;
|
|
226
|
+
/** Maximum characters accepted in a web search query. */
|
|
227
|
+
__webpack_unused_export__ = 500;
|
|
228
|
+
/** Maximum characters retained in one search-result snippet. */
|
|
229
|
+
__webpack_unused_export__ = 1_000;
|
|
230
|
+
/** Maximum characters retained in one search-result title. */
|
|
231
|
+
__webpack_unused_export__ = 300;
|
|
232
|
+
/** Maximum characters accepted in a calculator expression. */
|
|
233
|
+
__webpack_unused_export__ = 1_000;
|
|
234
|
+
/** Maximum arithmetic grammar tokens accepted by the calculator. */
|
|
235
|
+
__webpack_unused_export__ = 256;
|
|
236
|
+
/** Maximum arguments accepted by the calculator's min/max functions. */
|
|
237
|
+
__webpack_unused_export__ = 16;
|
|
238
|
+
/** Maximum characters accepted in a current_time timezone name. */
|
|
239
|
+
__webpack_unused_export__ = 100;
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
/***/ },
|
|
243
|
+
|
|
244
|
+
/***/ 796
|
|
245
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
246
|
+
|
|
247
|
+
var __webpack_unused_export__;
|
|
248
|
+
|
|
249
|
+
__webpack_unused_export__ = ({ value: true });
|
|
250
|
+
exports.audioFile = audioFile;
|
|
251
|
+
const limit_1 = __webpack_require__(9258);
|
|
252
|
+
const media_1 = __webpack_require__(2358);
|
|
253
|
+
const formats = {
|
|
254
|
+
mp3: 'audio/mpeg', mpeg: 'audio/mpeg', mpga: 'audio/mpeg',
|
|
255
|
+
wav: 'audio/wav', ogg: 'audio/ogg', flac: 'audio/flac',
|
|
256
|
+
m4a: 'audio/mp4', mp4: 'audio/mp4', webm: 'audio/webm'
|
|
257
|
+
};
|
|
258
|
+
/** Decodes supported upstream audio without writing files or attempting local conversion. */
|
|
259
|
+
function audioFile(data, format, id) {
|
|
260
|
+
const extension = format.toLowerCase();
|
|
261
|
+
const mimeType = formats[extension];
|
|
262
|
+
if (!Object.hasOwn(formats, extension)) {
|
|
263
|
+
throw new media_1.MediaUnavailableError(`Unsupported voice format "${format}"; agent-wechat must convert SILK to MP3.`);
|
|
264
|
+
}
|
|
265
|
+
if (!data || data.length > limit_1.MAX_ENCODED_AUDIO_BYTES || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data)) {
|
|
266
|
+
throw new media_1.MediaUnavailableError(`Voice data must be valid base64 no larger than ${limit_1.MAX_AUDIO_BYTES / (1024 * 1024)} MiB.`);
|
|
267
|
+
}
|
|
268
|
+
const bytes = Buffer.from(data, 'base64');
|
|
269
|
+
if (bytes.length > limit_1.MAX_AUDIO_BYTES || bytes.toString('base64') !== data ||
|
|
270
|
+
bytes.subarray(0, 16).toString('ascii').includes('#!SILK')) {
|
|
271
|
+
throw new media_1.MediaUnavailableError('Voice data is invalid, oversized, or unconverted SILK.');
|
|
272
|
+
}
|
|
273
|
+
return { data: bytes, filename: `voice_${id}.${extension}`, mimeType };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
/***/ },
|
|
278
|
+
|
|
279
|
+
/***/ 2358
|
|
280
|
+
(__unused_webpack_module, exports) {
|
|
281
|
+
|
|
282
|
+
var __webpack_unused_export__;
|
|
283
|
+
|
|
284
|
+
__webpack_unused_export__ = ({ value: true });
|
|
285
|
+
exports.MediaUnavailableError = void 0;
|
|
286
|
+
/** A terminal media failure; retrying the same bytes will not help. */
|
|
287
|
+
class MediaUnavailableError extends Error {
|
|
288
|
+
}
|
|
289
|
+
exports.MediaUnavailableError = MediaUnavailableError;
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
/***/ },
|
|
293
|
+
|
|
294
|
+
/***/ 6115
|
|
295
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
296
|
+
|
|
297
|
+
var __webpack_unused_export__;
|
|
298
|
+
|
|
299
|
+
__webpack_unused_export__ = ({ value: true });
|
|
300
|
+
exports.C = void 0;
|
|
301
|
+
exports.runAdd = runAdd;
|
|
302
|
+
const limit_1 = __webpack_require__(9258);
|
|
303
|
+
const credentials_1 = __webpack_require__(854);
|
|
304
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
305
|
+
const command_support_1 = __webpack_require__(2327);
|
|
306
|
+
const setup_1 = __webpack_require__(2993);
|
|
307
|
+
const model_store_1 = __webpack_require__(317);
|
|
308
|
+
/**
|
|
309
|
+
* Help text for `chatserver-models add`.
|
|
310
|
+
*/
|
|
311
|
+
exports.C = `Usage: chatserver-models add [options]
|
|
312
|
+
|
|
313
|
+
Adds a model to a provider in models.json and stores the provider's API key in the OS
|
|
314
|
+
credential manager. The provider is created when it does not exist yet.
|
|
315
|
+
Run without options for guided setup. Missing provider/model details are prompted
|
|
316
|
+
in a terminal; available model IDs are fetched from the provider when possible.
|
|
317
|
+
|
|
318
|
+
Options:
|
|
319
|
+
--provider <name> Provider name, also the credential account
|
|
320
|
+
--model <id> Model identifier sent to the API
|
|
321
|
+
--name <name> Name used to refer to the model (default: the model id)
|
|
322
|
+
--base-url <url> OpenAI-compatible base URL (required for new providers)
|
|
323
|
+
--api-key <key> API key to store for the provider
|
|
324
|
+
--timeout <ms> Provider model timeout (default: ${limit_1.DEFAULT_MODEL_TIMEOUT_MS} for new providers)
|
|
325
|
+
--config_dir <dir> Config folder
|
|
326
|
+
--force Overwrite an existing model without asking
|
|
327
|
+
-i, --interactive Prompt for the optional model name too
|
|
328
|
+
-h, --help Show this help
|
|
329
|
+
|
|
330
|
+
When the provider has no stored key, the API key is read from --api-key,
|
|
331
|
+
OPENAI_API_KEY, or an interactive prompt, in that order. It is never written to
|
|
332
|
+
models.json.
|
|
333
|
+
Without a terminal, pass --provider and --model, plus --base-url for new providers.
|
|
334
|
+
`;
|
|
335
|
+
/**
|
|
336
|
+
* Adds a model to a provider, creating the provider when needed.
|
|
337
|
+
*/
|
|
338
|
+
async function runAdd(argv) {
|
|
339
|
+
const cli = (0, command_support_1.parseCommandOptions)(argv, {
|
|
340
|
+
valueFlags: {
|
|
341
|
+
'--provider': 'provider',
|
|
342
|
+
'--name': 'name',
|
|
343
|
+
'--model': 'model',
|
|
344
|
+
'--base-url': 'baseUrl',
|
|
345
|
+
'--api-key': 'apiKey',
|
|
346
|
+
'--timeout': 'timeout'
|
|
347
|
+
},
|
|
348
|
+
booleanFlags: { '--force': 'force', '--interactive': 'interactive', '-i': 'interactive' }
|
|
349
|
+
});
|
|
350
|
+
if ((0, cli_support_1.hasFlag)(cli, 'help')) {
|
|
351
|
+
console.log(exports.C);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const configDir = (0, cli_support_1.resolveConfigDir)((0, cli_support_1.optionalValue)(cli, 'configDir'));
|
|
355
|
+
const guided = (0, setup_1.setupInteractive)(cli, (0, cli_support_1.optionalValue)(cli, 'provider') === undefined || (0, cli_support_1.optionalValue)(cli, 'model') === undefined);
|
|
356
|
+
const suppliedId = (0, setup_1.setupValue)(cli, 'model', '--model');
|
|
357
|
+
const suppliedName = (0, setup_1.setupValue)(cli, 'name', '--name');
|
|
358
|
+
const suppliedKey = (0, setup_1.setupValue)(cli, 'apiKey', '--api-key');
|
|
359
|
+
const urlValue = (0, setup_1.setupValue)(cli, 'baseUrl', '--base-url');
|
|
360
|
+
let baseUrl = urlValue === undefined ? undefined : (0, setup_1.setupBaseUrl)(urlValue);
|
|
361
|
+
const timeoutValue = (0, cli_support_1.optionalValue)(cli, 'timeout');
|
|
362
|
+
const timeoutMs = timeoutValue === undefined ? undefined : (0, command_support_1.modelTimeoutOption)(timeoutValue);
|
|
363
|
+
const file = await (0, model_store_1.loadModels)(configDir);
|
|
364
|
+
const providerName = await (0, setup_1.setupProvider)(file, cli, true);
|
|
365
|
+
if (providerName.includes('/')) {
|
|
366
|
+
throw new cli_support_1.CliError('Provider names cannot contain / because config references use provider/model-id.');
|
|
367
|
+
}
|
|
368
|
+
if (suppliedId === undefined && !process.stdin.isTTY) {
|
|
369
|
+
throw new cli_support_1.CliError('--model is required without a terminal.');
|
|
370
|
+
}
|
|
371
|
+
const existingProvider = (0, model_store_1.findProvider)(file, providerName);
|
|
372
|
+
if (existingProvider === undefined && baseUrl === undefined) {
|
|
373
|
+
if (!process.stdin.isTTY) {
|
|
374
|
+
throw new cli_support_1.CliError('--base-url is required when creating a provider.');
|
|
375
|
+
}
|
|
376
|
+
baseUrl = await (0, setup_1.setupPrompt)('API base URL (for example http://host.local:1234/v1)', undefined, setup_1.setupBaseUrl);
|
|
377
|
+
}
|
|
378
|
+
const provider = existingProvider ?? (0, model_store_1.ensureProvider)(file, providerName, baseUrl ?? '');
|
|
379
|
+
if (existingProvider !== undefined && baseUrl !== undefined) {
|
|
380
|
+
provider.baseUrl = baseUrl;
|
|
381
|
+
}
|
|
382
|
+
if (timeoutMs !== undefined) {
|
|
383
|
+
provider.timeoutMs = timeoutMs;
|
|
384
|
+
}
|
|
385
|
+
// Reuse the provider's saved key. An unrelated OPENAI_API_KEY must not rotate it.
|
|
386
|
+
const credentials = (0, credentials_1.createCredentialStore)();
|
|
387
|
+
const storedKey = suppliedKey === undefined ? await credentials.get(providerName) : undefined;
|
|
388
|
+
const needsKey = suppliedKey !== undefined || !storedKey;
|
|
389
|
+
const apiKey = suppliedKey ?? (storedKey || await (0, command_support_1.newApiKey)(cli, providerName));
|
|
390
|
+
const modelId = suppliedId ?? await (0, setup_1.setupModelId)(provider, apiKey);
|
|
391
|
+
const name = suppliedName ?? (guided ? await (0, setup_1.setupPrompt)('Model name', modelId) : modelId);
|
|
392
|
+
if ((0, model_store_1.findModel)(provider, name) !== undefined && !(0, cli_support_1.hasFlag)(cli, 'force')) {
|
|
393
|
+
if (!process.stdin.isTTY) {
|
|
394
|
+
throw new cli_support_1.CliError(`Model "${name}" already exists. Re-run with --force to overwrite it.`);
|
|
395
|
+
}
|
|
396
|
+
const answer = await (0, cli_support_1.promptLine)(`Model "${name}" already exists. Overwrite? [y/N] `);
|
|
397
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
398
|
+
console.log('Cancelled.');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (needsKey) {
|
|
403
|
+
await credentials.set(providerName, apiKey);
|
|
404
|
+
console.log(`API key stored in the ${credentials.name} (account: "${providerName}").`);
|
|
405
|
+
}
|
|
406
|
+
(0, model_store_1.upsertModel)(provider, { id: modelId, name });
|
|
407
|
+
const modelsPath = await (0, model_store_1.saveModels)(configDir, file);
|
|
408
|
+
console.log(`Saved model "${name}" (${modelId}) to ${modelsPath}.`);
|
|
409
|
+
console.log(`Config reference: ${providerName}/${modelId}`);
|
|
410
|
+
console.log(`Test it with: chatserver-models test --provider "${providerName}" --name "${name}" --config_dir "${configDir}"`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
/***/ },
|
|
415
|
+
|
|
416
|
+
/***/ 1515
|
|
417
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
421
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
422
|
+
};
|
|
423
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
424
|
+
exports.convertAudio = convertAudio;
|
|
425
|
+
const limit_1 = __webpack_require__(9258);
|
|
426
|
+
const node_child_process_1 = __webpack_require__(1421);
|
|
427
|
+
const node_fs_1 = __webpack_require__(3024);
|
|
428
|
+
const node_os_1 = __importDefault(__webpack_require__(8161));
|
|
429
|
+
const node_path_1 = __importDefault(__webpack_require__(6760));
|
|
430
|
+
/** Runs a configured command with bounded diagnostics, cancellation, and a process-tree deadline. */
|
|
431
|
+
async function runConversion(command, directory, timeoutMs, signal) {
|
|
432
|
+
signal?.throwIfAborted();
|
|
433
|
+
return new Promise((resolve, reject) => {
|
|
434
|
+
const child = (0, node_child_process_1.spawn)(command, {
|
|
435
|
+
cwd: directory, shell: true, windowsHide: true,
|
|
436
|
+
detached: process.platform !== 'win32', stdio: ['ignore', 'ignore', 'pipe']
|
|
437
|
+
});
|
|
438
|
+
let stderr = '';
|
|
439
|
+
let settled = false;
|
|
440
|
+
const finish = (error) => {
|
|
441
|
+
if (settled) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
settled = true;
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
signal?.removeEventListener('abort', abort);
|
|
447
|
+
if (error === undefined) {
|
|
448
|
+
resolve();
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
reject(error);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const stop = (error) => {
|
|
455
|
+
if (settled) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (child.pid !== undefined) {
|
|
459
|
+
if (process.platform === 'win32') {
|
|
460
|
+
const killer = (0, node_child_process_1.spawn)('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
461
|
+
killer.on('error', () => child.kill('SIGKILL'));
|
|
462
|
+
killer.unref();
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
try {
|
|
466
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
child.kill('SIGKILL');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
child.stderr?.destroy();
|
|
474
|
+
child.unref();
|
|
475
|
+
finish(error);
|
|
476
|
+
};
|
|
477
|
+
const abort = () => stop(signal?.reason instanceof Error ? signal.reason : new Error('Audio conversion cancelled.'));
|
|
478
|
+
const timer = setTimeout(() => stop(new Error(`Audio conversion timed out after ${timeoutMs} ms.`)), timeoutMs);
|
|
479
|
+
child.stderr?.setEncoding('utf8');
|
|
480
|
+
child.stderr?.on('data', (chunk) => {
|
|
481
|
+
stderr = (stderr + chunk).slice(0, limit_1.AUDIO_CONVERSION_STDERR_CHARS);
|
|
482
|
+
});
|
|
483
|
+
child.on('error', () => finish(new Error('Unable to start audioConversionCmd. Check the command and installed converter.')));
|
|
484
|
+
child.on('close', (code) => finish(code === 0 ? undefined :
|
|
485
|
+
new Error(`Audio conversion failed (exit ${code ?? 'unknown'})${stderr.trim() ? `: ${stderr.trim()}` : '.'}`)));
|
|
486
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
487
|
+
if (signal?.aborted) {
|
|
488
|
+
abort();
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/** Converts input to a real WAV file in an isolated temporary directory, then removes both files. */
|
|
493
|
+
async function convertAudio(audio, command, signal, timeoutMs = limit_1.AUDIO_CONVERSION_TIMEOUT_MS) {
|
|
494
|
+
signal?.throwIfAborted();
|
|
495
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > limit_1.MAX_TIMER_MS) {
|
|
496
|
+
throw new Error(`Audio conversion timeout must be a positive integer up to ${limit_1.MAX_TIMER_MS}.`);
|
|
497
|
+
}
|
|
498
|
+
if (!command.includes('{input}') || !command.includes('{output}')) {
|
|
499
|
+
throw new Error('audioConversionCmd must contain {input} and {output} placeholders.');
|
|
500
|
+
}
|
|
501
|
+
if (!audio.data.byteLength || audio.data.byteLength > limit_1.MAX_AUDIO_BYTES) {
|
|
502
|
+
throw new Error(`Audio conversion input must be nonempty and no larger than ${limit_1.MAX_AUDIO_BYTES / (1024 * 1024)} MiB.`);
|
|
503
|
+
}
|
|
504
|
+
const directory = await node_fs_1.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'chatserver-audio-'));
|
|
505
|
+
const extension = node_path_1.default.extname(audio.filename);
|
|
506
|
+
const inputName = `input${/^\.[a-z0-9]+$/i.test(extension) ? extension : '.bin'}`;
|
|
507
|
+
const outputName = 'output.wav';
|
|
508
|
+
try {
|
|
509
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(directory, inputName), audio.data, { mode: 0o600 });
|
|
510
|
+
// Only generated ASCII filenames enter the command; message content and paths never do.
|
|
511
|
+
const expanded = command.replaceAll('{input}', inputName).replaceAll('{output}', outputName);
|
|
512
|
+
await runConversion(expanded, directory, timeoutMs, signal);
|
|
513
|
+
signal?.throwIfAborted();
|
|
514
|
+
const outputPath = node_path_1.default.join(directory, outputName);
|
|
515
|
+
let stat;
|
|
516
|
+
try {
|
|
517
|
+
stat = await node_fs_1.promises.lstat(outputPath);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
throw new Error('audioConversionCmd did not create its {output} WAV file.');
|
|
521
|
+
}
|
|
522
|
+
if (!stat.isFile() || stat.size < 44 || stat.size > limit_1.MAX_AUDIO_BYTES) {
|
|
523
|
+
throw new Error(`Converted audio must be a nonempty WAV file no larger than ${limit_1.MAX_AUDIO_BYTES / (1024 * 1024)} MiB.`);
|
|
524
|
+
}
|
|
525
|
+
const data = await node_fs_1.promises.readFile(outputPath);
|
|
526
|
+
if (data.subarray(0, 4).toString('ascii') !== 'RIFF' || data.subarray(8, 12).toString('ascii') !== 'WAVE') {
|
|
527
|
+
throw new Error('audioConversionCmd must produce WAV data, not just rename the input.');
|
|
528
|
+
}
|
|
529
|
+
return { data, mimeType: 'audio/wav', filename: `${node_path_1.default.basename(audio.filename, extension) || 'voice'}.wav` };
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
await node_fs_1.promises.rm(directory, { recursive: true, force: true, maxRetries: limit_1.AUDIO_CLEANUP_RETRIES, retryDelay: limit_1.AUDIO_CLEANUP_RETRY_DELAY_MS });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
/***/ },
|
|
538
|
+
|
|
539
|
+
/***/ 8256
|
|
540
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
544
|
+
if (k2 === undefined) k2 = k;
|
|
545
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
546
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
547
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
548
|
+
}
|
|
549
|
+
Object.defineProperty(o, k2, desc);
|
|
550
|
+
}) : (function(o, m, k, k2) {
|
|
551
|
+
if (k2 === undefined) k2 = k;
|
|
552
|
+
o[k2] = m[k];
|
|
553
|
+
}));
|
|
554
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
555
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
556
|
+
}) : function(o, v) {
|
|
557
|
+
o["default"] = v;
|
|
558
|
+
});
|
|
559
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
560
|
+
var ownKeys = function(o) {
|
|
561
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
562
|
+
var ar = [];
|
|
563
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
564
|
+
return ar;
|
|
565
|
+
};
|
|
566
|
+
return ownKeys(o);
|
|
567
|
+
};
|
|
568
|
+
return function (mod) {
|
|
569
|
+
if (mod && mod.__esModule) return mod;
|
|
570
|
+
var result = {};
|
|
571
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
572
|
+
__setModuleDefault(result, mod);
|
|
573
|
+
return result;
|
|
574
|
+
};
|
|
575
|
+
})();
|
|
576
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
577
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
578
|
+
};
|
|
579
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
580
|
+
exports.CliError = void 0;
|
|
581
|
+
exports.parseCommandLine = parseCommandLine;
|
|
582
|
+
exports.optionalValue = optionalValue;
|
|
583
|
+
exports.requireValue = requireValue;
|
|
584
|
+
exports.hasFlag = hasFlag;
|
|
585
|
+
exports.resolveConfigDir = resolveConfigDir;
|
|
586
|
+
exports.promptLine = promptLine;
|
|
587
|
+
exports.promptHidden = promptHidden;
|
|
588
|
+
exports.reportError = reportError;
|
|
589
|
+
const node_path_1 = __importDefault(__webpack_require__(6760));
|
|
590
|
+
const readline = __importStar(__webpack_require__(6848));
|
|
591
|
+
const config_dir_1 = __webpack_require__(1977);
|
|
592
|
+
/**
|
|
593
|
+
* Raised for invalid command-line usage.
|
|
594
|
+
*/
|
|
595
|
+
class CliError extends Error {
|
|
596
|
+
constructor(message) {
|
|
597
|
+
super(message);
|
|
598
|
+
this.name = 'CliError';
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
exports.CliError = CliError;
|
|
602
|
+
/**
|
|
603
|
+
* Splits `--flag=value` into its name and inline value.
|
|
604
|
+
*/
|
|
605
|
+
function splitFlag(arg) {
|
|
606
|
+
const index = arg.indexOf('=');
|
|
607
|
+
if (index === -1) {
|
|
608
|
+
return [arg, undefined];
|
|
609
|
+
}
|
|
610
|
+
return [arg.slice(0, index), arg.slice(index + 1)];
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Parses `--flag value`, `--flag=value`, and boolean flags.
|
|
614
|
+
*/
|
|
615
|
+
function parseCommandLine(argv, spec) {
|
|
616
|
+
const values = new Map();
|
|
617
|
+
const flags = new Set();
|
|
618
|
+
const positionals = [];
|
|
619
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
620
|
+
const arg = argv[i];
|
|
621
|
+
if (!arg.startsWith('-')) {
|
|
622
|
+
positionals.push(arg);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const [name, inlineValue] = splitFlag(arg);
|
|
626
|
+
const valueKey = spec.valueFlags[name];
|
|
627
|
+
if (valueKey !== undefined) {
|
|
628
|
+
const value = inlineValue ?? argv[i + 1];
|
|
629
|
+
if (value === undefined || (inlineValue === undefined && value.startsWith('-'))) {
|
|
630
|
+
throw new CliError(`${name} requires a value.`);
|
|
631
|
+
}
|
|
632
|
+
values.set(valueKey, value);
|
|
633
|
+
if (inlineValue === undefined) {
|
|
634
|
+
i += 1;
|
|
635
|
+
}
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
const flagKey = spec.booleanFlags[name];
|
|
639
|
+
if (flagKey !== undefined) {
|
|
640
|
+
if (inlineValue !== undefined) {
|
|
641
|
+
throw new CliError(`${name} does not take a value.`);
|
|
642
|
+
}
|
|
643
|
+
flags.add(flagKey);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
throw new CliError(`Unknown option: ${name}. Run with --help for usage.`);
|
|
647
|
+
}
|
|
648
|
+
return { values, flags, positionals };
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Reads an option's value.
|
|
652
|
+
*/
|
|
653
|
+
function optionalValue(cli, key) {
|
|
654
|
+
return cli.values.get(key);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Reads an option's value, failing when it is missing.
|
|
658
|
+
*/
|
|
659
|
+
function requireValue(cli, key, label) {
|
|
660
|
+
const value = cli.values.get(key);
|
|
661
|
+
if (value === undefined || value === '') {
|
|
662
|
+
throw new CliError(`${label} is required.`);
|
|
663
|
+
}
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Returns true when a boolean flag was supplied.
|
|
668
|
+
*/
|
|
669
|
+
function hasFlag(cli, key) {
|
|
670
|
+
return cli.flags.has(key);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Resolves the config folder from an option, the environment, or the per-user default.
|
|
674
|
+
*/
|
|
675
|
+
function resolveConfigDir(option) {
|
|
676
|
+
return node_path_1.default.resolve(option ?? process.env.CHATSERVER_CONFIG_DIR ?? (0, config_dir_1.getDefaultConfigDir)());
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Prompts for a line of text.
|
|
680
|
+
*/
|
|
681
|
+
async function promptLine(question) {
|
|
682
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
683
|
+
try {
|
|
684
|
+
return (await rl.question(question)).trim();
|
|
685
|
+
}
|
|
686
|
+
finally {
|
|
687
|
+
rl.close();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Prompts for a secret without echoing keystrokes when stdin is a terminal.
|
|
692
|
+
*/
|
|
693
|
+
function promptHidden(question) {
|
|
694
|
+
const stdin = process.stdin;
|
|
695
|
+
if (!stdin.isTTY) {
|
|
696
|
+
throw new CliError('Cannot prompt for a secret without a terminal. Pass --api-key instead.');
|
|
697
|
+
}
|
|
698
|
+
return new Promise((resolve, reject) => {
|
|
699
|
+
process.stdout.write(question);
|
|
700
|
+
stdin.setRawMode(true);
|
|
701
|
+
stdin.resume();
|
|
702
|
+
stdin.setEncoding('utf8');
|
|
703
|
+
let value = '';
|
|
704
|
+
const onData = (chunk) => {
|
|
705
|
+
for (const char of chunk) {
|
|
706
|
+
if (char === '\r' || char === '\n') {
|
|
707
|
+
finish(value);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (char === '\u0003') {
|
|
711
|
+
finish(undefined, new CliError('Cancelled.'));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (char === '\u007f' || char === '\b') {
|
|
715
|
+
value = value.slice(0, -1);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
value += char;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
const finish = (result, error) => {
|
|
722
|
+
stdin.setRawMode(false);
|
|
723
|
+
stdin.pause();
|
|
724
|
+
stdin.removeListener('data', onData);
|
|
725
|
+
process.stdout.write('\n');
|
|
726
|
+
if (error !== undefined) {
|
|
727
|
+
reject(error);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
resolve(result ?? '');
|
|
731
|
+
};
|
|
732
|
+
stdin.on('data', onData);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Prints an error and marks the process as failed.
|
|
737
|
+
*/
|
|
738
|
+
function reportError(error) {
|
|
739
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
740
|
+
console.error(`[models] ${message}`);
|
|
741
|
+
process.exitCode = 1;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
/***/ },
|
|
746
|
+
|
|
747
|
+
/***/ 2327
|
|
748
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
749
|
+
|
|
750
|
+
var __webpack_unused_export__;
|
|
751
|
+
|
|
752
|
+
__webpack_unused_export__ = ({ value: true });
|
|
753
|
+
exports.DEFAULT_PROMPT = void 0;
|
|
754
|
+
exports.parseCommandOptions = parseCommandOptions;
|
|
755
|
+
exports.requireProvider = requireProvider;
|
|
756
|
+
exports.requireModel = requireModel;
|
|
757
|
+
exports.modelTimeoutOption = modelTimeoutOption;
|
|
758
|
+
exports.newApiKey = newApiKey;
|
|
759
|
+
__webpack_unused_export__ = existingApiKey;
|
|
760
|
+
exports.loadTestModel = loadTestModel;
|
|
761
|
+
const limit_1 = __webpack_require__(9258);
|
|
762
|
+
const credentials_1 = __webpack_require__(854);
|
|
763
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
764
|
+
const model_store_1 = __webpack_require__(317);
|
|
765
|
+
const responses_client_1 = __webpack_require__(102);
|
|
766
|
+
/**
|
|
767
|
+
* Default prompt used by `test` when --prompt is not supplied.
|
|
768
|
+
*/
|
|
769
|
+
exports.DEFAULT_PROMPT = 'Say hello in one short sentence.';
|
|
770
|
+
const COMMON_VALUE_FLAGS = {
|
|
771
|
+
'--config_dir': 'configDir',
|
|
772
|
+
'--config-dir': 'configDir',
|
|
773
|
+
'-c': 'configDir'
|
|
774
|
+
};
|
|
775
|
+
const COMMON_BOOLEAN_FLAGS = {
|
|
776
|
+
'-h': 'help',
|
|
777
|
+
'--help': 'help'
|
|
778
|
+
};
|
|
779
|
+
/**
|
|
780
|
+
* Parses options for one command, merging in the shared flags.
|
|
781
|
+
*/
|
|
782
|
+
function parseCommandOptions(argv, spec) {
|
|
783
|
+
return (0, cli_support_1.parseCommandLine)(argv, {
|
|
784
|
+
valueFlags: { ...COMMON_VALUE_FLAGS, ...spec.valueFlags },
|
|
785
|
+
booleanFlags: { ...COMMON_BOOLEAN_FLAGS, ...spec.booleanFlags }
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Looks up a provider or fails with the available names.
|
|
790
|
+
*/
|
|
791
|
+
async function requireProvider(configDir, name) {
|
|
792
|
+
const file = await (0, model_store_1.loadModels)(configDir);
|
|
793
|
+
const provider = (0, model_store_1.findProvider)(file, name);
|
|
794
|
+
if (provider === undefined) {
|
|
795
|
+
const available = file.providers.map((entry) => entry.provider).join(', ') || '(none)';
|
|
796
|
+
throw new cli_support_1.CliError(`Provider "${name}" not found in ${(0, model_store_1.getModelsPath)(configDir)}. Available: ${available}.`);
|
|
797
|
+
}
|
|
798
|
+
return { file, provider };
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Looks up a model within a provider or fails with the available names.
|
|
802
|
+
*/
|
|
803
|
+
function requireModel(provider, name) {
|
|
804
|
+
const model = (0, model_store_1.findModel)(provider, name);
|
|
805
|
+
if (model === undefined) {
|
|
806
|
+
const available = provider.models.map((entry) => entry.name).join(', ') || '(none)';
|
|
807
|
+
throw new cli_support_1.CliError(`Model "${name}" not found for provider "${provider.provider}". Available: ${available}.`);
|
|
808
|
+
}
|
|
809
|
+
return model;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Reads the CLI timeout using the same validation as models.json and HTTP calls.
|
|
813
|
+
*/
|
|
814
|
+
function modelTimeoutOption(value, fallback = limit_1.DEFAULT_MODEL_TIMEOUT_MS) {
|
|
815
|
+
try {
|
|
816
|
+
return (0, responses_client_1.resolveModelTimeoutMs)(value === undefined ? fallback : Number(value));
|
|
817
|
+
}
|
|
818
|
+
catch (error) {
|
|
819
|
+
throw new cli_support_1.CliError(`--timeout: ${error.message}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Reads a new API key from the command line, the environment, or a prompt.
|
|
824
|
+
*/
|
|
825
|
+
async function newApiKey(cli, provider) {
|
|
826
|
+
const fromOption = (0, cli_support_1.optionalValue)(cli, 'apiKey');
|
|
827
|
+
if (fromOption !== undefined && fromOption !== '') {
|
|
828
|
+
return fromOption;
|
|
829
|
+
}
|
|
830
|
+
const fromEnvironment = process.env.OPENAI_API_KEY;
|
|
831
|
+
if (fromEnvironment !== undefined && fromEnvironment !== '') {
|
|
832
|
+
return fromEnvironment;
|
|
833
|
+
}
|
|
834
|
+
const value = await (0, cli_support_1.promptHidden)(`API key for provider "${provider}": `);
|
|
835
|
+
if (value === '') {
|
|
836
|
+
throw new cli_support_1.CliError('An API key is required.');
|
|
837
|
+
}
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Finds the API key used to call a provider's models.
|
|
842
|
+
*/
|
|
843
|
+
async function existingApiKey(provider) {
|
|
844
|
+
return (0, credentials_1.getModelApiKey)(provider.provider);
|
|
845
|
+
}
|
|
846
|
+
/** Resolves the same model alias, provider key, and timeout for every model-test command. */
|
|
847
|
+
async function loadTestModel(cli) {
|
|
848
|
+
const configDir = (0, cli_support_1.resolveConfigDir)((0, cli_support_1.optionalValue)(cli, 'configDir'));
|
|
849
|
+
const providerName = (0, cli_support_1.requireValue)(cli, 'provider', '--provider');
|
|
850
|
+
const name = (0, cli_support_1.requireValue)(cli, 'name', '--name');
|
|
851
|
+
const { provider } = await requireProvider(configDir, providerName);
|
|
852
|
+
const model = requireModel(provider, name);
|
|
853
|
+
const timeoutMs = modelTimeoutOption((0, cli_support_1.optionalValue)(cli, 'timeout'), provider.timeoutMs ?? limit_1.DEFAULT_MODEL_TIMEOUT_MS);
|
|
854
|
+
const apiKey = await existingApiKey(provider);
|
|
855
|
+
return { provider, model, apiKey, timeoutMs };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
/***/ },
|
|
860
|
+
|
|
861
|
+
/***/ 854
|
|
862
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
863
|
+
|
|
864
|
+
var __webpack_unused_export__;
|
|
865
|
+
|
|
866
|
+
__webpack_unused_export__ = ({ value: true });
|
|
867
|
+
__webpack_unused_export__ = exports.CW = void 0;
|
|
868
|
+
exports.createCredentialStore = createCredentialStore;
|
|
869
|
+
exports.getModelApiKey = getModelApiKey;
|
|
870
|
+
const node_child_process_1 = __webpack_require__(1421);
|
|
871
|
+
/**
|
|
872
|
+
* Service name used for every credential this project stores, keeping ChatServer keys
|
|
873
|
+
* separate from anything else in the OS credential manager.
|
|
874
|
+
*/
|
|
875
|
+
exports.CW = 'chatserver-models';
|
|
876
|
+
/**
|
|
877
|
+
* Raised when credentials cannot be read from or written to the OS credential manager.
|
|
878
|
+
*/
|
|
879
|
+
class CredentialError extends Error {
|
|
880
|
+
constructor(message) {
|
|
881
|
+
super(message);
|
|
882
|
+
this.name = 'CredentialError';
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
__webpack_unused_export__ = CredentialError;
|
|
886
|
+
/**
|
|
887
|
+
* Runs a command and resolves with its standard output.
|
|
888
|
+
*/
|
|
889
|
+
function run(command, args, environment) {
|
|
890
|
+
const env = environment === undefined ? process.env : { ...process.env, ...environment };
|
|
891
|
+
return new Promise((resolve, reject) => {
|
|
892
|
+
(0, node_child_process_1.execFile)(command, args, { encoding: 'utf8', env }, (error, stdout) => {
|
|
893
|
+
if (error !== null) {
|
|
894
|
+
reject(error);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
resolve(stdout);
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Runs a command, writing `input` to its standard input.
|
|
903
|
+
*/
|
|
904
|
+
function runWithInput(command, args, input) {
|
|
905
|
+
return new Promise((resolve, reject) => {
|
|
906
|
+
const child = (0, node_child_process_1.spawn)(command, args, { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
907
|
+
let stderr = '';
|
|
908
|
+
child.stderr.setEncoding('utf8');
|
|
909
|
+
child.stderr.on('data', (chunk) => {
|
|
910
|
+
stderr += chunk;
|
|
911
|
+
});
|
|
912
|
+
child.on('error', reject);
|
|
913
|
+
child.on('close', (code) => {
|
|
914
|
+
if (code === 0) {
|
|
915
|
+
resolve();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code ?? 'unknown'}`));
|
|
919
|
+
});
|
|
920
|
+
child.stdin.end(input);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Removes a single trailing newline without touching other whitespace in a secret.
|
|
925
|
+
*/
|
|
926
|
+
function stripTrailingNewline(value) {
|
|
927
|
+
return value.replace(/\r?\n$/, '');
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Escapes a value for use inside a single-quoted PowerShell string.
|
|
931
|
+
*/
|
|
932
|
+
function powerShellQuote(value) {
|
|
933
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* macOS backend built on the `security` command (Keychain).
|
|
937
|
+
*/
|
|
938
|
+
class MacOsKeychainStore {
|
|
939
|
+
name = 'macOS Keychain';
|
|
940
|
+
async get(account) {
|
|
941
|
+
try {
|
|
942
|
+
const stdout = await run('security', [
|
|
943
|
+
'find-generic-password',
|
|
944
|
+
'-s',
|
|
945
|
+
exports.CW,
|
|
946
|
+
'-a',
|
|
947
|
+
account,
|
|
948
|
+
'-w'
|
|
949
|
+
]);
|
|
950
|
+
return stripTrailingNewline(stdout) || undefined;
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// The item does not exist, or the user denied access.
|
|
954
|
+
return undefined;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async set(account, secret) {
|
|
958
|
+
try {
|
|
959
|
+
await run('security', [
|
|
960
|
+
'add-generic-password',
|
|
961
|
+
'-s',
|
|
962
|
+
exports.CW,
|
|
963
|
+
'-a',
|
|
964
|
+
account,
|
|
965
|
+
'-w',
|
|
966
|
+
secret,
|
|
967
|
+
'-U'
|
|
968
|
+
]);
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
// execFile errors include the full command, including the password argument.
|
|
972
|
+
throw new CredentialError(`Unable to store the API key in the ${this.name}. Check Keychain access permissions.`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async delete(account) {
|
|
976
|
+
try {
|
|
977
|
+
await run('security', [
|
|
978
|
+
'delete-generic-password',
|
|
979
|
+
'-s',
|
|
980
|
+
exports.CW,
|
|
981
|
+
'-a',
|
|
982
|
+
account
|
|
983
|
+
]);
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
// Nothing stored: nothing to remove.
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Linux backend built on `secret-tool` from libsecret (GNOME Keyring, KWallet, ...).
|
|
992
|
+
*/
|
|
993
|
+
class SecretServiceStore {
|
|
994
|
+
name = 'Secret Service (secret-tool)';
|
|
995
|
+
async get(account) {
|
|
996
|
+
try {
|
|
997
|
+
const stdout = await run('secret-tool', [
|
|
998
|
+
'lookup',
|
|
999
|
+
'service',
|
|
1000
|
+
exports.CW,
|
|
1001
|
+
'account',
|
|
1002
|
+
account
|
|
1003
|
+
]);
|
|
1004
|
+
return stripTrailingNewline(stdout) || undefined;
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
return undefined;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async set(account, secret) {
|
|
1011
|
+
try {
|
|
1012
|
+
await runWithInput('secret-tool', [
|
|
1013
|
+
'store',
|
|
1014
|
+
'--label',
|
|
1015
|
+
`ChatServer model ${account}`,
|
|
1016
|
+
'service',
|
|
1017
|
+
exports.CW,
|
|
1018
|
+
'account',
|
|
1019
|
+
account
|
|
1020
|
+
], secret);
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
throw new CredentialError(`Unable to store the API key in the ${this.name}. ` +
|
|
1024
|
+
'Install libsecret and make sure a keyring is running.');
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
async delete(account) {
|
|
1028
|
+
try {
|
|
1029
|
+
await run('secret-tool', [
|
|
1030
|
+
'clear',
|
|
1031
|
+
'service',
|
|
1032
|
+
exports.CW,
|
|
1033
|
+
'account',
|
|
1034
|
+
account
|
|
1035
|
+
]);
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
// Nothing stored: nothing to remove.
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Windows backend using the CredentialManager PowerShell module.
|
|
1044
|
+
*
|
|
1045
|
+
* The module is not part of Windows, so the error explains how to install it.
|
|
1046
|
+
*/
|
|
1047
|
+
class WindowsCredentialStore {
|
|
1048
|
+
name = 'Windows Credential Manager';
|
|
1049
|
+
async get(account) {
|
|
1050
|
+
const script = [
|
|
1051
|
+
'$ErrorActionPreference = "Stop"',
|
|
1052
|
+
'Import-Module CredentialManager -ErrorAction Stop',
|
|
1053
|
+
`$cred = Get-StoredCredential -Target ${powerShellQuote(this.target(account))}`,
|
|
1054
|
+
'if ($null -eq $cred) { exit 3 }',
|
|
1055
|
+
'[Console]::Out.Write($cred.GetNetworkCredential().Password)'
|
|
1056
|
+
].join('; ');
|
|
1057
|
+
try {
|
|
1058
|
+
const stdout = await run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
1059
|
+
return stdout || undefined;
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
if (error.code === 3) {
|
|
1063
|
+
return undefined;
|
|
1064
|
+
}
|
|
1065
|
+
throw new CredentialError(this.installHint());
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async set(account, secret) {
|
|
1069
|
+
const script = [
|
|
1070
|
+
'$ErrorActionPreference = "Stop"',
|
|
1071
|
+
'Import-Module CredentialManager -ErrorAction Stop',
|
|
1072
|
+
`New-StoredCredential -Target ${powerShellQuote(this.target(account))} ` +
|
|
1073
|
+
`-UserName ${powerShellQuote(account)} -Password $env:CHATSERVER_SECRET ` +
|
|
1074
|
+
'-Persist LocalMachine | Out-Null'
|
|
1075
|
+
].join('; ');
|
|
1076
|
+
try {
|
|
1077
|
+
await run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1078
|
+
CHATSERVER_SECRET: secret
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
throw new CredentialError(this.installHint());
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async delete(account) {
|
|
1086
|
+
const script = [
|
|
1087
|
+
'$ErrorActionPreference = "Stop"',
|
|
1088
|
+
'Import-Module CredentialManager -ErrorAction Stop',
|
|
1089
|
+
`Remove-StoredCredential -Target ${powerShellQuote(this.target(account))} | Out-Null`
|
|
1090
|
+
].join('; ');
|
|
1091
|
+
try {
|
|
1092
|
+
await run('powershell', ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
// Nothing stored: nothing to remove.
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
target(account) {
|
|
1099
|
+
return `${exports.CW}:${account}`;
|
|
1100
|
+
}
|
|
1101
|
+
installHint() {
|
|
1102
|
+
return (`Unable to use the ${this.name}. Install the CredentialManager PowerShell module ` +
|
|
1103
|
+
'(Install-Module CredentialManager -Scope CurrentUser). Check credential manager access permissions.');
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Returns the credential manager integration for the current platform.
|
|
1108
|
+
*/
|
|
1109
|
+
function createCredentialStore(platform = process.platform) {
|
|
1110
|
+
switch (platform) {
|
|
1111
|
+
case 'darwin':
|
|
1112
|
+
return new MacOsKeychainStore();
|
|
1113
|
+
case 'linux':
|
|
1114
|
+
return new SecretServiceStore();
|
|
1115
|
+
case 'win32':
|
|
1116
|
+
return new WindowsCredentialStore();
|
|
1117
|
+
default:
|
|
1118
|
+
throw new CredentialError(`No credential manager integration for platform "${platform}". ` +
|
|
1119
|
+
'Set CHATSERVER_MODEL_API_KEY or OPENAI_API_KEY instead.');
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/** Reads a provider API key, allowing the same override as the model CLI. */
|
|
1123
|
+
async function getModelApiKey(provider) {
|
|
1124
|
+
const override = process.env.CHATSERVER_MODEL_API_KEY;
|
|
1125
|
+
if (override !== undefined && override !== '') {
|
|
1126
|
+
return override;
|
|
1127
|
+
}
|
|
1128
|
+
const credentials = createCredentialStore();
|
|
1129
|
+
const stored = await credentials.get(provider);
|
|
1130
|
+
if (stored === undefined || stored === '') {
|
|
1131
|
+
throw new CredentialError(`No API key found in the ${credentials.name} for provider "${provider}". ` +
|
|
1132
|
+
'Run "chatserver-models add" or "chatserver-models update --api-key" to store one, ' +
|
|
1133
|
+
'or set CHATSERVER_MODEL_API_KEY.');
|
|
1134
|
+
}
|
|
1135
|
+
return stored;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
/***/ },
|
|
1140
|
+
|
|
1141
|
+
/***/ 9598
|
|
1142
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
1143
|
+
|
|
1144
|
+
var __webpack_unused_export__;
|
|
1145
|
+
|
|
1146
|
+
__webpack_unused_export__ = ({ value: true });
|
|
1147
|
+
exports.D = void 0;
|
|
1148
|
+
exports.runList = runList;
|
|
1149
|
+
const limit_1 = __webpack_require__(9258);
|
|
1150
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
1151
|
+
const command_support_1 = __webpack_require__(2327);
|
|
1152
|
+
const model_store_1 = __webpack_require__(317);
|
|
1153
|
+
/**
|
|
1154
|
+
* Column widths used by `list` output.
|
|
1155
|
+
*/
|
|
1156
|
+
const PROVIDER_COLUMN_WIDTH = 16;
|
|
1157
|
+
const NAME_COLUMN_WIDTH = 20;
|
|
1158
|
+
const MODEL_ID_COLUMN_WIDTH = 24;
|
|
1159
|
+
const TIMEOUT_COLUMN_WIDTH = 14;
|
|
1160
|
+
/**
|
|
1161
|
+
* Help text for `chatserver-models list`.
|
|
1162
|
+
*/
|
|
1163
|
+
exports.D = `Usage: chatserver-models list [options]
|
|
1164
|
+
|
|
1165
|
+
Lists the providers and models in models.json. API keys are not printed.
|
|
1166
|
+
|
|
1167
|
+
Options:
|
|
1168
|
+
--json Print the configuration as JSON
|
|
1169
|
+
--config_dir <dir> Config folder
|
|
1170
|
+
-h, --help Show this help
|
|
1171
|
+
`;
|
|
1172
|
+
/**
|
|
1173
|
+
* Prints the configured providers and models.
|
|
1174
|
+
*/
|
|
1175
|
+
async function runList(argv) {
|
|
1176
|
+
const cli = (0, command_support_1.parseCommandOptions)(argv, { valueFlags: {}, booleanFlags: { '--json': 'json' } });
|
|
1177
|
+
if ((0, cli_support_1.hasFlag)(cli, 'help')) {
|
|
1178
|
+
console.log(exports.D);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
const configDir = (0, cli_support_1.resolveConfigDir)((0, cli_support_1.optionalValue)(cli, 'configDir'));
|
|
1182
|
+
const file = await (0, model_store_1.loadModels)(configDir);
|
|
1183
|
+
if ((0, cli_support_1.hasFlag)(cli, 'json')) {
|
|
1184
|
+
console.log(JSON.stringify(file, null, 2));
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (file.providers.length === 0) {
|
|
1188
|
+
console.log(`No providers configured in ${(0, model_store_1.getModelsPath)(configDir)}.`);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
console.log(`Providers in ${(0, model_store_1.getModelsPath)(configDir)}:`);
|
|
1192
|
+
console.log('');
|
|
1193
|
+
console.log(`${'PROVIDER'.padEnd(PROVIDER_COLUMN_WIDTH)}${'MODEL'.padEnd(NAME_COLUMN_WIDTH)}` +
|
|
1194
|
+
`${'MODEL ID'.padEnd(MODEL_ID_COLUMN_WIDTH)}${'TIMEOUT (MS)'.padEnd(TIMEOUT_COLUMN_WIDTH)}BASE URL`);
|
|
1195
|
+
let modelCount = 0;
|
|
1196
|
+
for (const provider of file.providers) {
|
|
1197
|
+
if (provider.models.length === 0) {
|
|
1198
|
+
console.log(`${provider.provider.padEnd(PROVIDER_COLUMN_WIDTH)}${'(no models)'.padEnd(NAME_COLUMN_WIDTH)}` +
|
|
1199
|
+
`${''.padEnd(MODEL_ID_COLUMN_WIDTH)}` +
|
|
1200
|
+
`${String(provider.timeoutMs ?? limit_1.DEFAULT_MODEL_TIMEOUT_MS).padEnd(TIMEOUT_COLUMN_WIDTH)}${provider.baseUrl}`);
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
for (const model of provider.models) {
|
|
1204
|
+
modelCount += 1;
|
|
1205
|
+
console.log(`${provider.provider.padEnd(PROVIDER_COLUMN_WIDTH)}` +
|
|
1206
|
+
`${model.name.padEnd(NAME_COLUMN_WIDTH)}` +
|
|
1207
|
+
`${model.id.padEnd(MODEL_ID_COLUMN_WIDTH)}` +
|
|
1208
|
+
`${String(provider.timeoutMs ?? limit_1.DEFAULT_MODEL_TIMEOUT_MS).padEnd(TIMEOUT_COLUMN_WIDTH)}${provider.baseUrl}`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (modelCount === 0) {
|
|
1212
|
+
console.log('');
|
|
1213
|
+
console.log('No models configured yet.');
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
/***/ },
|
|
1219
|
+
|
|
1220
|
+
/***/ 317
|
|
1221
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
1225
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
1226
|
+
};
|
|
1227
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1228
|
+
exports.ModelStoreError = exports.MODELS_FILE_NAME = void 0;
|
|
1229
|
+
exports.getModelsPath = getModelsPath;
|
|
1230
|
+
exports.loadModels = loadModels;
|
|
1231
|
+
exports.saveModels = saveModels;
|
|
1232
|
+
exports.findProvider = findProvider;
|
|
1233
|
+
exports.ensureProvider = ensureProvider;
|
|
1234
|
+
exports.removeProvider = removeProvider;
|
|
1235
|
+
exports.findModel = findModel;
|
|
1236
|
+
exports.upsertModel = upsertModel;
|
|
1237
|
+
exports.removeModel = removeModel;
|
|
1238
|
+
const limit_1 = __webpack_require__(9258);
|
|
1239
|
+
const node_fs_1 = __webpack_require__(3024);
|
|
1240
|
+
const node_path_1 = __importDefault(__webpack_require__(6760));
|
|
1241
|
+
const responses_client_1 = __webpack_require__(102);
|
|
1242
|
+
/**
|
|
1243
|
+
* Name of the file that holds model configurations inside the config folder.
|
|
1244
|
+
*/
|
|
1245
|
+
exports.MODELS_FILE_NAME = 'models.json';
|
|
1246
|
+
/**
|
|
1247
|
+
* Raised when `models.json` is malformed.
|
|
1248
|
+
*/
|
|
1249
|
+
class ModelStoreError extends Error {
|
|
1250
|
+
constructor(message) {
|
|
1251
|
+
super(message);
|
|
1252
|
+
this.name = 'ModelStoreError';
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
exports.ModelStoreError = ModelStoreError;
|
|
1256
|
+
/**
|
|
1257
|
+
* Returns the path of `models.json` inside a config folder.
|
|
1258
|
+
*/
|
|
1259
|
+
function getModelsPath(configDir) {
|
|
1260
|
+
return node_path_1.default.join(configDir, exports.MODELS_FILE_NAME);
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Returns true for plain JSON objects (not null, arrays, or primitives).
|
|
1264
|
+
*/
|
|
1265
|
+
function isPlainObject(value) {
|
|
1266
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Validates one model entry.
|
|
1270
|
+
*/
|
|
1271
|
+
function parseModelEntry(value, where, seen) {
|
|
1272
|
+
if (!isPlainObject(value)) {
|
|
1273
|
+
throw new ModelStoreError(`${where} must be an object.`);
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof value.id !== 'string' || value.id.trim() === '') {
|
|
1276
|
+
throw new ModelStoreError(`${where}: "id" must be a non-empty string.`);
|
|
1277
|
+
}
|
|
1278
|
+
const id = value.id.trim();
|
|
1279
|
+
const name = value.name ?? id;
|
|
1280
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
1281
|
+
throw new ModelStoreError(`${where}: "name" must be a non-empty string.`);
|
|
1282
|
+
}
|
|
1283
|
+
if (value.supportsImageToolResults !== undefined && typeof value.supportsImageToolResults !== 'boolean') {
|
|
1284
|
+
throw new ModelStoreError(`${where}: "supportsImageToolResults" must be a boolean.`);
|
|
1285
|
+
}
|
|
1286
|
+
const entry = {
|
|
1287
|
+
id, name: name.trim(),
|
|
1288
|
+
...(value.supportsImageToolResults === undefined ? {} : { supportsImageToolResults: value.supportsImageToolResults })
|
|
1289
|
+
};
|
|
1290
|
+
if (seen.has(entry.name)) {
|
|
1291
|
+
throw new ModelStoreError(`${where}: duplicate model name "${entry.name}".`);
|
|
1292
|
+
}
|
|
1293
|
+
seen.add(entry.name);
|
|
1294
|
+
return entry;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Validates one provider entry.
|
|
1298
|
+
*/
|
|
1299
|
+
function parseProvider(value, index, modelsPath) {
|
|
1300
|
+
const where = `${modelsPath}: providers[${index}]`;
|
|
1301
|
+
if (!isPlainObject(value)) {
|
|
1302
|
+
throw new ModelStoreError(`${where} must be an object.`);
|
|
1303
|
+
}
|
|
1304
|
+
if (typeof value.provider !== 'string' || value.provider.trim() === '') {
|
|
1305
|
+
throw new ModelStoreError(`${where}: "provider" must be a non-empty string.`);
|
|
1306
|
+
}
|
|
1307
|
+
const baseUrl = value.baseUrl;
|
|
1308
|
+
if (typeof baseUrl !== 'string' || baseUrl.trim() === '') {
|
|
1309
|
+
throw new ModelStoreError(`${where}: "baseUrl" must be a non-empty string.`);
|
|
1310
|
+
}
|
|
1311
|
+
if (!Array.isArray(value.models)) {
|
|
1312
|
+
throw new ModelStoreError(`${where}: "models" must be an array.`);
|
|
1313
|
+
}
|
|
1314
|
+
let timeoutMs;
|
|
1315
|
+
try {
|
|
1316
|
+
timeoutMs = (0, responses_client_1.resolveModelTimeoutMs)(value.timeoutMs);
|
|
1317
|
+
}
|
|
1318
|
+
catch (error) {
|
|
1319
|
+
throw new ModelStoreError(`${where}: "timeoutMs": ${error.message}`);
|
|
1320
|
+
}
|
|
1321
|
+
const seen = new Set();
|
|
1322
|
+
return {
|
|
1323
|
+
provider: value.provider.trim(),
|
|
1324
|
+
baseUrl: baseUrl.trim(),
|
|
1325
|
+
timeoutMs,
|
|
1326
|
+
models: value.models.map((model, modelIndex) => parseModelEntry(model, `${where}.models[${modelIndex}]`, seen))
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Reads `models.json`, returning an empty list when the file does not exist.
|
|
1331
|
+
*/
|
|
1332
|
+
async function loadModels(configDir) {
|
|
1333
|
+
const modelsPath = getModelsPath(configDir);
|
|
1334
|
+
let contents;
|
|
1335
|
+
try {
|
|
1336
|
+
contents = await node_fs_1.promises.readFile(modelsPath, 'utf8');
|
|
1337
|
+
}
|
|
1338
|
+
catch (error) {
|
|
1339
|
+
if (error.code === 'ENOENT') {
|
|
1340
|
+
return { version: 1, providers: [] };
|
|
1341
|
+
}
|
|
1342
|
+
throw new ModelStoreError(`Unable to read ${modelsPath}: ${error.message}`);
|
|
1343
|
+
}
|
|
1344
|
+
let parsed;
|
|
1345
|
+
try {
|
|
1346
|
+
parsed = JSON.parse(contents);
|
|
1347
|
+
}
|
|
1348
|
+
catch (error) {
|
|
1349
|
+
throw new ModelStoreError(`Invalid JSON in ${modelsPath}: ${error.message}`);
|
|
1350
|
+
}
|
|
1351
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed.providers)) {
|
|
1352
|
+
throw new ModelStoreError(`${modelsPath} must contain an object with a "providers" array.`);
|
|
1353
|
+
}
|
|
1354
|
+
if (parsed.$schema !== undefined && typeof parsed.$schema !== 'string') {
|
|
1355
|
+
throw new ModelStoreError(`${modelsPath}: "$schema" must be a string.`);
|
|
1356
|
+
}
|
|
1357
|
+
const seen = new Set();
|
|
1358
|
+
const providers = parsed.providers.map((provider, index) => {
|
|
1359
|
+
const config = parseProvider(provider, index, modelsPath);
|
|
1360
|
+
if (seen.has(config.provider)) {
|
|
1361
|
+
throw new ModelStoreError(`${modelsPath}: duplicate provider name "${config.provider}".`);
|
|
1362
|
+
}
|
|
1363
|
+
seen.add(config.provider);
|
|
1364
|
+
return config;
|
|
1365
|
+
});
|
|
1366
|
+
return { ...(parsed.$schema === undefined ? {} : { $schema: parsed.$schema }), version: 1, providers };
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Writes `models.json`, creating the config folder when needed.
|
|
1370
|
+
*
|
|
1371
|
+
* Returns the file path that was written.
|
|
1372
|
+
*/
|
|
1373
|
+
async function saveModels(configDir, file) {
|
|
1374
|
+
const modelsPath = getModelsPath(configDir);
|
|
1375
|
+
const payload = {
|
|
1376
|
+
...(file.$schema === undefined ? {} : { $schema: file.$schema }),
|
|
1377
|
+
version: 1,
|
|
1378
|
+
providers: file.providers
|
|
1379
|
+
};
|
|
1380
|
+
try {
|
|
1381
|
+
await node_fs_1.promises.mkdir(configDir, { recursive: true });
|
|
1382
|
+
await node_fs_1.promises.writeFile(modelsPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
throw new ModelStoreError(`Unable to write ${modelsPath}: ${error.message}`);
|
|
1386
|
+
}
|
|
1387
|
+
return modelsPath;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Finds a provider by name.
|
|
1391
|
+
*/
|
|
1392
|
+
function findProvider(file, provider) {
|
|
1393
|
+
return file.providers.find((entry) => entry.provider === provider);
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Returns the provider, creating it when missing.
|
|
1397
|
+
*/
|
|
1398
|
+
function ensureProvider(file, provider, baseUrl) {
|
|
1399
|
+
const existing = findProvider(file, provider);
|
|
1400
|
+
if (existing !== undefined) {
|
|
1401
|
+
return existing;
|
|
1402
|
+
}
|
|
1403
|
+
if (!baseUrl.trim()) {
|
|
1404
|
+
throw new ModelStoreError('A non-empty baseUrl is required to create a provider.');
|
|
1405
|
+
}
|
|
1406
|
+
const created = { provider, baseUrl: baseUrl.trim(), timeoutMs: limit_1.DEFAULT_MODEL_TIMEOUT_MS, models: [] };
|
|
1407
|
+
file.providers.push(created);
|
|
1408
|
+
return created;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Removes a provider by name. Returns true when an entry was removed.
|
|
1412
|
+
*/
|
|
1413
|
+
function removeProvider(file, provider) {
|
|
1414
|
+
const index = file.providers.findIndex((entry) => entry.provider === provider);
|
|
1415
|
+
if (index === -1) {
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
file.providers.splice(index, 1);
|
|
1419
|
+
return true;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Finds a model within a provider.
|
|
1423
|
+
*/
|
|
1424
|
+
function findModel(provider, name) {
|
|
1425
|
+
return provider.models.find((model) => model.name === name);
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Adds a model or replaces the entry with the same name.
|
|
1429
|
+
*/
|
|
1430
|
+
function upsertModel(provider, model) {
|
|
1431
|
+
const index = provider.models.findIndex((candidate) => candidate.name === model.name);
|
|
1432
|
+
if (index === -1) {
|
|
1433
|
+
provider.models.push(model);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
provider.models[index] = model;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Removes a model by name. Returns true when an entry was removed.
|
|
1440
|
+
*/
|
|
1441
|
+
function removeModel(provider, name) {
|
|
1442
|
+
const index = provider.models.findIndex((candidate) => candidate.name === name);
|
|
1443
|
+
if (index === -1) {
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
provider.models.splice(index, 1);
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
/***/ },
|
|
1452
|
+
|
|
1453
|
+
/***/ 5978
|
|
1454
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
1455
|
+
|
|
1456
|
+
var __webpack_unused_export__;
|
|
1457
|
+
|
|
1458
|
+
__webpack_unused_export__ = ({ value: true });
|
|
1459
|
+
exports.x = void 0;
|
|
1460
|
+
exports.runRemove = runRemove;
|
|
1461
|
+
const credentials_1 = __webpack_require__(854);
|
|
1462
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
1463
|
+
const command_support_1 = __webpack_require__(2327);
|
|
1464
|
+
const model_store_1 = __webpack_require__(317);
|
|
1465
|
+
/**
|
|
1466
|
+
* Help text for `chatserver-models remove`.
|
|
1467
|
+
*/
|
|
1468
|
+
exports.x = `Usage: chatserver-models remove --provider <name> --name <name> [options]
|
|
1469
|
+
|
|
1470
|
+
Removes a model. When it was the provider's last model, the provider entry is removed
|
|
1471
|
+
too and its API key is deleted from the credential manager.
|
|
1472
|
+
|
|
1473
|
+
Options:
|
|
1474
|
+
--provider <name> Provider name (required)
|
|
1475
|
+
--name <name> Model name to remove (required)
|
|
1476
|
+
--config_dir <dir> Config folder
|
|
1477
|
+
-h, --help Show this help
|
|
1478
|
+
`;
|
|
1479
|
+
/**
|
|
1480
|
+
* Removes a model, and its provider once the provider has no models left.
|
|
1481
|
+
*/
|
|
1482
|
+
async function runRemove(argv) {
|
|
1483
|
+
const cli = (0, command_support_1.parseCommandOptions)(argv, {
|
|
1484
|
+
valueFlags: { '--provider': 'provider', '--name': 'name' },
|
|
1485
|
+
booleanFlags: {}
|
|
1486
|
+
});
|
|
1487
|
+
if ((0, cli_support_1.hasFlag)(cli, 'help')) {
|
|
1488
|
+
console.log(exports.x);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const configDir = (0, cli_support_1.resolveConfigDir)((0, cli_support_1.optionalValue)(cli, 'configDir'));
|
|
1492
|
+
const providerName = (0, cli_support_1.requireValue)(cli, 'provider', '--provider');
|
|
1493
|
+
const name = (0, cli_support_1.requireValue)(cli, 'name', '--name');
|
|
1494
|
+
const { file, provider } = await (0, command_support_1.requireProvider)(configDir, providerName);
|
|
1495
|
+
(0, command_support_1.requireModel)(provider, name);
|
|
1496
|
+
(0, model_store_1.removeModel)(provider, name);
|
|
1497
|
+
const providerRemoved = provider.models.length === 0;
|
|
1498
|
+
if (providerRemoved) {
|
|
1499
|
+
(0, model_store_1.removeProvider)(file, providerName);
|
|
1500
|
+
}
|
|
1501
|
+
const modelsPath = await (0, model_store_1.saveModels)(configDir, file);
|
|
1502
|
+
console.log(`Removed model "${name}" from provider "${providerName}" in ${modelsPath}.`);
|
|
1503
|
+
if (providerRemoved) {
|
|
1504
|
+
console.log(`Provider "${providerName}" had no models left and was removed.`);
|
|
1505
|
+
const credentials = (0, credentials_1.createCredentialStore)();
|
|
1506
|
+
await credentials.delete(providerName);
|
|
1507
|
+
console.log(`Removed API key from the ${credentials.name} (account: "${providerName}").`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
/***/ },
|
|
1513
|
+
|
|
1514
|
+
/***/ 102
|
|
1515
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
1516
|
+
|
|
1517
|
+
var __webpack_unused_export__;
|
|
1518
|
+
|
|
1519
|
+
__webpack_unused_export__ = ({ value: true });
|
|
1520
|
+
exports.ResponsesApiError = void 0;
|
|
1521
|
+
exports.resolveModelTimeoutMs = resolveModelTimeoutMs;
|
|
1522
|
+
__webpack_unused_export__ = responsesEndpoint;
|
|
1523
|
+
__webpack_unused_export__ = extractOutputText;
|
|
1524
|
+
__webpack_unused_export__ = extractCitations;
|
|
1525
|
+
exports.createResponse = createResponse;
|
|
1526
|
+
const limit_1 = __webpack_require__(9258);
|
|
1527
|
+
/** Validates durations against Node's timer range, including programmatic callers. */
|
|
1528
|
+
function resolveModelTimeoutMs(value = limit_1.DEFAULT_MODEL_TIMEOUT_MS) {
|
|
1529
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1 || value > limit_1.MAX_TIMER_MS) {
|
|
1530
|
+
throw new Error(`Model timeout must be an integer from 1 to ${limit_1.MAX_TIMER_MS} milliseconds.`);
|
|
1531
|
+
}
|
|
1532
|
+
return value;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Raised when a Responses API call fails.
|
|
1536
|
+
*/
|
|
1537
|
+
class ResponsesApiError extends Error {
|
|
1538
|
+
status;
|
|
1539
|
+
body;
|
|
1540
|
+
constructor(message, status, body) {
|
|
1541
|
+
super(message);
|
|
1542
|
+
this.name = 'ResponsesApiError';
|
|
1543
|
+
this.status = status;
|
|
1544
|
+
this.body = body;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
exports.ResponsesApiError = ResponsesApiError;
|
|
1548
|
+
/**
|
|
1549
|
+
* Returns true for plain JSON objects (not null, arrays, or primitives).
|
|
1550
|
+
*/
|
|
1551
|
+
function isRecord(value) {
|
|
1552
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Builds the endpoint used for Responses API calls.
|
|
1556
|
+
*/
|
|
1557
|
+
function responsesEndpoint(baseUrl) {
|
|
1558
|
+
return `${baseUrl.replace(/\/+$/, '')}/responses`;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Extracts assistant text from a Responses API payload.
|
|
1562
|
+
*
|
|
1563
|
+
* Handles the documented `output` array with `output_text` content parts, and falls
|
|
1564
|
+
* back to a top-level `output_text` string for compatible servers that use it.
|
|
1565
|
+
*/
|
|
1566
|
+
function extractOutputText(payload) {
|
|
1567
|
+
if (!isRecord(payload)) {
|
|
1568
|
+
return '';
|
|
1569
|
+
}
|
|
1570
|
+
if (typeof payload.output_text === 'string') {
|
|
1571
|
+
return payload.output_text.trim();
|
|
1572
|
+
}
|
|
1573
|
+
if (!Array.isArray(payload.output)) {
|
|
1574
|
+
return '';
|
|
1575
|
+
}
|
|
1576
|
+
const parts = [];
|
|
1577
|
+
for (const item of payload.output) {
|
|
1578
|
+
if (!isRecord(item) || !Array.isArray(item.content)) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
for (const content of item.content) {
|
|
1582
|
+
if (isRecord(content) && content.type === 'output_text' && typeof content.text === 'string') {
|
|
1583
|
+
parts.push(content.text);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return parts.join('\n').trim();
|
|
1588
|
+
}
|
|
1589
|
+
/** Retains URL citations even when generated text uses provider-specific markers. */
|
|
1590
|
+
function extractCitations(payload) {
|
|
1591
|
+
if (!isRecord(payload) || !Array.isArray(payload.output)) {
|
|
1592
|
+
return [];
|
|
1593
|
+
}
|
|
1594
|
+
const urls = new Set();
|
|
1595
|
+
for (const item of payload.output) {
|
|
1596
|
+
if (!isRecord(item) || item.type !== 'message' || !Array.isArray(item.content)) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
for (const content of item.content) {
|
|
1600
|
+
if (!isRecord(content) || content.type !== 'output_text' || !Array.isArray(content.annotations)) {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
for (const annotation of content.annotations) {
|
|
1604
|
+
if (!isRecord(annotation) || annotation.type !== 'url_citation' || typeof annotation.url !== 'string') {
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
try {
|
|
1608
|
+
const url = new URL(annotation.url);
|
|
1609
|
+
if ((url.protocol === 'http:' || url.protocol === 'https:') && !url.username && !url.password) {
|
|
1610
|
+
urls.add(url.href);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
catch {
|
|
1614
|
+
// Ignore malformed citation URLs rather than rendering them as links.
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return [...urls].map((url) => ({ url }));
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Sends one prompt through the Responses API and returns the text it produced.
|
|
1623
|
+
*/
|
|
1624
|
+
async function createResponse(request) {
|
|
1625
|
+
const url = responsesEndpoint(request.baseUrl);
|
|
1626
|
+
const timeoutMs = resolveModelTimeoutMs(request.timeoutMs);
|
|
1627
|
+
const controller = new AbortController();
|
|
1628
|
+
const cancel = () => controller.abort();
|
|
1629
|
+
request.signal?.addEventListener('abort', cancel, { once: true });
|
|
1630
|
+
if (request.signal?.aborted) {
|
|
1631
|
+
controller.abort();
|
|
1632
|
+
}
|
|
1633
|
+
const timer = setTimeout(() => {
|
|
1634
|
+
controller.abort();
|
|
1635
|
+
}, timeoutMs);
|
|
1636
|
+
const body = {
|
|
1637
|
+
model: request.model,
|
|
1638
|
+
input: request.input
|
|
1639
|
+
};
|
|
1640
|
+
if (request.tools !== undefined && request.tools.length > 0) {
|
|
1641
|
+
body.tools = request.tools;
|
|
1642
|
+
body.tool_choice = request.toolChoice ?? 'auto';
|
|
1643
|
+
// Replay the complete output locally, including any encrypted reasoning.
|
|
1644
|
+
body.store = false;
|
|
1645
|
+
body.include = ['reasoning.encrypted_content'];
|
|
1646
|
+
if (request.maxToolCalls !== undefined) {
|
|
1647
|
+
body.max_tool_calls = request.maxToolCalls;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (request.reasoningEffort !== undefined) {
|
|
1651
|
+
body.reasoning = { effort: request.reasoningEffort };
|
|
1652
|
+
}
|
|
1653
|
+
if (request.instructions !== undefined) {
|
|
1654
|
+
body.instructions = request.instructions;
|
|
1655
|
+
}
|
|
1656
|
+
const headers = {
|
|
1657
|
+
'Content-Type': 'application/json',
|
|
1658
|
+
Authorization: `Bearer ${request.apiKey}`,
|
|
1659
|
+
...request.headers
|
|
1660
|
+
};
|
|
1661
|
+
try {
|
|
1662
|
+
const response = await fetch(url, {
|
|
1663
|
+
method: 'POST',
|
|
1664
|
+
headers,
|
|
1665
|
+
body: JSON.stringify(body),
|
|
1666
|
+
signal: controller.signal
|
|
1667
|
+
});
|
|
1668
|
+
const text = await response.text();
|
|
1669
|
+
if (!response.ok) {
|
|
1670
|
+
throw new ResponsesApiError(`Request to ${url} failed with HTTP ${response.status} ${response.statusText}.`, response.status, text);
|
|
1671
|
+
}
|
|
1672
|
+
let parsed;
|
|
1673
|
+
try {
|
|
1674
|
+
parsed = JSON.parse(text);
|
|
1675
|
+
}
|
|
1676
|
+
catch (error) {
|
|
1677
|
+
throw new ResponsesApiError(`Response from ${url} was not valid JSON: ${error.message}`, response.status, text);
|
|
1678
|
+
}
|
|
1679
|
+
if (isRecord(parsed) && ((parsed.error !== undefined && parsed.error !== null) || parsed.status === 'failed' || parsed.status === 'incomplete')) {
|
|
1680
|
+
throw new ResponsesApiError(`Response from ${url} did not complete successfully.`, response.status);
|
|
1681
|
+
}
|
|
1682
|
+
const result = { outputText: extractOutputText(parsed), citations: extractCitations(parsed), raw: parsed };
|
|
1683
|
+
if (isRecord(parsed) && typeof parsed.id === 'string') {
|
|
1684
|
+
result.id = parsed.id;
|
|
1685
|
+
}
|
|
1686
|
+
return result;
|
|
1687
|
+
}
|
|
1688
|
+
catch (error) {
|
|
1689
|
+
if (error instanceof ResponsesApiError) {
|
|
1690
|
+
throw error;
|
|
1691
|
+
}
|
|
1692
|
+
if (request.signal?.aborted) {
|
|
1693
|
+
throw new ResponsesApiError(`Request to ${url} was cancelled.`);
|
|
1694
|
+
}
|
|
1695
|
+
if (error.name === 'AbortError') {
|
|
1696
|
+
throw new ResponsesApiError(`Request to ${url} timed out after ${timeoutMs} ms.`);
|
|
1697
|
+
}
|
|
1698
|
+
throw new ResponsesApiError(`Request to ${url} failed: ${error.message}`);
|
|
1699
|
+
}
|
|
1700
|
+
finally {
|
|
1701
|
+
clearTimeout(timer);
|
|
1702
|
+
request.signal?.removeEventListener('abort', cancel);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
/***/ },
|
|
1708
|
+
|
|
1709
|
+
/***/ 7718
|
|
1710
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
1711
|
+
|
|
1712
|
+
var __webpack_unused_export__;
|
|
1713
|
+
|
|
1714
|
+
__webpack_unused_export__ = ({ value: true });
|
|
1715
|
+
exports.withModelRetries = withModelRetries;
|
|
1716
|
+
const limit_1 = __webpack_require__(9258);
|
|
1717
|
+
const promises_1 = __webpack_require__(8500);
|
|
1718
|
+
const responses_client_1 = __webpack_require__(102);
|
|
1719
|
+
/** One initial generation and at most three retries, with ten seconds between attempts. */
|
|
1720
|
+
async function withModelRetries(generate, signal) {
|
|
1721
|
+
for (let retries = 0;; retries += 1) {
|
|
1722
|
+
signal?.throwIfAborted();
|
|
1723
|
+
try {
|
|
1724
|
+
return await generate();
|
|
1725
|
+
}
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
const transient = error instanceof responses_client_1.ResponsesApiError &&
|
|
1728
|
+
(error.status === undefined || error.status === 408 || error.status === 429 || error.status >= 500);
|
|
1729
|
+
if (signal?.aborted || retries >= limit_1.MAX_MODEL_RETRIES || !transient) {
|
|
1730
|
+
throw error;
|
|
1731
|
+
}
|
|
1732
|
+
console.warn(`[model] generation failed; retry ${retries + 1}/${limit_1.MAX_MODEL_RETRIES} in ${limit_1.MODEL_RETRY_DELAY_MS / 1000} seconds.`);
|
|
1733
|
+
await (0, promises_1.setTimeout)(limit_1.MODEL_RETRY_DELAY_MS, undefined, { signal });
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
/***/ },
|
|
1740
|
+
|
|
1741
|
+
/***/ 2993
|
|
1742
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
1743
|
+
|
|
1744
|
+
var __webpack_unused_export__;
|
|
1745
|
+
|
|
1746
|
+
__webpack_unused_export__ = ({ value: true });
|
|
1747
|
+
exports.setupValue = setupValue;
|
|
1748
|
+
exports.setupBaseUrl = setupBaseUrl;
|
|
1749
|
+
__webpack_unused_export__ = requireSetupTerminal;
|
|
1750
|
+
exports.setupPrompt = setupPrompt;
|
|
1751
|
+
exports.setupChoice = setupChoice;
|
|
1752
|
+
exports.setupProvider = setupProvider;
|
|
1753
|
+
exports.setupInteractive = setupInteractive;
|
|
1754
|
+
exports.setupModelId = setupModelId;
|
|
1755
|
+
const limit_1 = __webpack_require__(9258);
|
|
1756
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
1757
|
+
/** Reads an optional nonempty, trimmed command-line value. */
|
|
1758
|
+
function setupValue(cli, key, flag) {
|
|
1759
|
+
const value = (0, cli_support_1.optionalValue)(cli, key)?.trim();
|
|
1760
|
+
if (value === '') {
|
|
1761
|
+
throw new cli_support_1.CliError(`${flag} must not be empty.`);
|
|
1762
|
+
}
|
|
1763
|
+
return value;
|
|
1764
|
+
}
|
|
1765
|
+
/** Validates a provider endpoint before prompting for or saving credentials. */
|
|
1766
|
+
function setupBaseUrl(value) {
|
|
1767
|
+
let url;
|
|
1768
|
+
try {
|
|
1769
|
+
url = new URL(value);
|
|
1770
|
+
}
|
|
1771
|
+
catch {
|
|
1772
|
+
throw new cli_support_1.CliError('Base URL must be an absolute http:// or https:// URL.');
|
|
1773
|
+
}
|
|
1774
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
1775
|
+
throw new cli_support_1.CliError('Base URL must use HTTP(S), without credentials, a query, or a fragment.');
|
|
1776
|
+
}
|
|
1777
|
+
if (/\/responses\/?$/.test(url.pathname)) {
|
|
1778
|
+
throw new cli_support_1.CliError('Use the API base URL, without the trailing /responses.');
|
|
1779
|
+
}
|
|
1780
|
+
return value.trim().replace(/\/+$/, '');
|
|
1781
|
+
}
|
|
1782
|
+
/** Requires a terminal for guided setup, keeping scripts from hanging on prompts. */
|
|
1783
|
+
function requireSetupTerminal() {
|
|
1784
|
+
if (!process.stdin.isTTY) {
|
|
1785
|
+
throw new cli_support_1.CliError('Guided setup requires a terminal. Supply the command options instead.');
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
/** Prompts with an optional current value and retries invalid input. */
|
|
1789
|
+
async function setupPrompt(label, fallback, validate = (value) => {
|
|
1790
|
+
if (!value) {
|
|
1791
|
+
throw new cli_support_1.CliError('A value is required.');
|
|
1792
|
+
}
|
|
1793
|
+
return value;
|
|
1794
|
+
}) {
|
|
1795
|
+
requireSetupTerminal();
|
|
1796
|
+
for (;;) {
|
|
1797
|
+
const value = (await (0, cli_support_1.promptLine)(`${label}${fallback === undefined ? '' : ` [${fallback}]`}: `)).trim() || fallback || '';
|
|
1798
|
+
try {
|
|
1799
|
+
return validate(value);
|
|
1800
|
+
}
|
|
1801
|
+
catch (error) {
|
|
1802
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
/** Selects a numbered item or, when allowed, accepts a new name/ID directly. */
|
|
1807
|
+
async function setupChoice(label, choices, allowCustom = false) {
|
|
1808
|
+
requireSetupTerminal();
|
|
1809
|
+
const shown = allowCustom ? choices.slice(0, limit_1.MAX_SETUP_CHOICES) : choices;
|
|
1810
|
+
shown.forEach((choice, index) => console.log(` ${index + 1}. ${choice.label}`));
|
|
1811
|
+
if (choices.length > shown.length) {
|
|
1812
|
+
console.log(`Showing ${shown.length} of ${choices.length}; enter an exact name/ID for another choice.`);
|
|
1813
|
+
}
|
|
1814
|
+
return setupPrompt(`${label}${allowCustom ? ' (number or name/ID)' : ' (number or name)'}`, allowCustom ? undefined : '1', (value) => {
|
|
1815
|
+
// Exact names win over numbered shortcuts (including numeric model IDs).
|
|
1816
|
+
const exact = choices.find((choice) => choice.value === value || choice.name === value);
|
|
1817
|
+
if (exact !== undefined) {
|
|
1818
|
+
return exact.value;
|
|
1819
|
+
}
|
|
1820
|
+
const index = /^[1-9]\d*$/.test(value) ? Number(value) - 1 : -1;
|
|
1821
|
+
if (shown[index] !== undefined) {
|
|
1822
|
+
return shown[index].value;
|
|
1823
|
+
}
|
|
1824
|
+
if (allowCustom && value) {
|
|
1825
|
+
return value;
|
|
1826
|
+
}
|
|
1827
|
+
throw new cli_support_1.CliError('Choose a listed number or name.');
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
/** Resolves a supplied provider or offers configured providers and a new-name entry. */
|
|
1831
|
+
async function setupProvider(file, cli, allowNew) {
|
|
1832
|
+
const supplied = setupValue(cli, 'provider', '--provider');
|
|
1833
|
+
if (supplied !== undefined) {
|
|
1834
|
+
return supplied;
|
|
1835
|
+
}
|
|
1836
|
+
if (!process.stdin.isTTY) {
|
|
1837
|
+
throw new cli_support_1.CliError('--provider is required without a terminal.');
|
|
1838
|
+
}
|
|
1839
|
+
if (!allowNew && file.providers.length === 0) {
|
|
1840
|
+
throw new cli_support_1.CliError('No providers configured. Run chatserver-models add first.');
|
|
1841
|
+
}
|
|
1842
|
+
return setupChoice(allowNew ? 'Choose a provider or enter a new provider name' : 'Provider', file.providers.map((provider) => ({ value: provider.provider, label: `${provider.provider} (${provider.baseUrl})` })), allowNew);
|
|
1843
|
+
}
|
|
1844
|
+
/** Enables the editor explicitly or when a terminal command has no edits yet. */
|
|
1845
|
+
function setupInteractive(cli, missing) {
|
|
1846
|
+
if ((0, cli_support_1.hasFlag)(cli, 'interactive')) {
|
|
1847
|
+
requireSetupTerminal();
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1850
|
+
return Boolean(process.stdin.isTTY) && missing;
|
|
1851
|
+
}
|
|
1852
|
+
/** Offers the provider's advertised models, falling back to a manually entered ID. */
|
|
1853
|
+
async function setupModelId(provider, apiKey) {
|
|
1854
|
+
requireSetupTerminal();
|
|
1855
|
+
console.log(`Looking up models at ${provider.baseUrl}...`);
|
|
1856
|
+
let ids;
|
|
1857
|
+
try {
|
|
1858
|
+
const response = await fetch(`${provider.baseUrl.replace(/\/+$/, '')}/models`, {
|
|
1859
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1860
|
+
signal: AbortSignal.timeout(Math.min(provider.timeoutMs ?? limit_1.DEFAULT_MODEL_TIMEOUT_MS, limit_1.MODEL_DISCOVERY_TIMEOUT_MS)),
|
|
1861
|
+
redirect: 'error'
|
|
1862
|
+
});
|
|
1863
|
+
if (!response.ok) {
|
|
1864
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1865
|
+
}
|
|
1866
|
+
const body = await response.json();
|
|
1867
|
+
if (typeof body !== 'object' || body === null || !('data' in body) || !Array.isArray(body.data)) {
|
|
1868
|
+
throw new Error('Unsupported model list');
|
|
1869
|
+
}
|
|
1870
|
+
ids = [...new Set(body.data.flatMap((model) => typeof model === 'object' && model !== null && 'id' in model && typeof model.id === 'string' && model.id.trim()
|
|
1871
|
+
? [model.id.trim()] : []))].sort();
|
|
1872
|
+
}
|
|
1873
|
+
catch {
|
|
1874
|
+
console.log('Could not list models from this provider. Enter the model ID manually.');
|
|
1875
|
+
return setupPrompt('Model ID');
|
|
1876
|
+
}
|
|
1877
|
+
if (ids.length === 0) {
|
|
1878
|
+
return setupPrompt('Model ID');
|
|
1879
|
+
}
|
|
1880
|
+
return setupChoice('Model', ids.map((id) => ({ value: id, label: id })), true);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
/***/ },
|
|
1885
|
+
|
|
1886
|
+
/***/ 5314
|
|
1887
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
1888
|
+
|
|
1889
|
+
var __webpack_unused_export__;
|
|
1890
|
+
|
|
1891
|
+
__webpack_unused_export__ = ({ value: true });
|
|
1892
|
+
exports.createSpeech = createSpeech;
|
|
1893
|
+
const limit_1 = __webpack_require__(9258);
|
|
1894
|
+
const responses_client_1 = __webpack_require__(102);
|
|
1895
|
+
/** Requests bounded WAV audio from an OpenAI-compatible audio/speech endpoint. */
|
|
1896
|
+
async function createSpeech(request) {
|
|
1897
|
+
request.signal?.throwIfAborted();
|
|
1898
|
+
if (!request.text.trim() || request.text.length > limit_1.MAX_TTS_INPUT_CHARS) {
|
|
1899
|
+
throw new Error(`Speech input must contain 1–${limit_1.MAX_TTS_INPUT_CHARS} characters.`);
|
|
1900
|
+
}
|
|
1901
|
+
const url = `${request.baseUrl.replace(/\/+$/, '')}/audio/speech`;
|
|
1902
|
+
const timeoutMs = (0, responses_client_1.resolveModelTimeoutMs)(request.timeoutMs);
|
|
1903
|
+
const controller = new AbortController();
|
|
1904
|
+
const cancel = () => controller.abort();
|
|
1905
|
+
request.signal?.addEventListener('abort', cancel, { once: true });
|
|
1906
|
+
const timer = setTimeout(cancel, timeoutMs);
|
|
1907
|
+
try {
|
|
1908
|
+
const response = await fetch(url, {
|
|
1909
|
+
method: 'POST',
|
|
1910
|
+
headers: { Authorization: `Bearer ${request.apiKey}`, 'Content-Type': 'application/json' },
|
|
1911
|
+
body: JSON.stringify({ ...request.options, model: request.model, input: request.text,
|
|
1912
|
+
response_format: 'wav', stream_format: 'audio' }),
|
|
1913
|
+
signal: controller.signal,
|
|
1914
|
+
redirect: 'error'
|
|
1915
|
+
});
|
|
1916
|
+
if (!response.ok) {
|
|
1917
|
+
await response.body?.cancel();
|
|
1918
|
+
throw new responses_client_1.ResponsesApiError(`Speech request to ${url} failed with HTTP ${response.status}.`, response.status);
|
|
1919
|
+
}
|
|
1920
|
+
if (Number(response.headers.get('content-length')) > limit_1.MAX_AUDIO_BYTES || response.body === null) {
|
|
1921
|
+
await response.body?.cancel();
|
|
1922
|
+
throw new responses_client_1.ResponsesApiError('Speech response is missing or exceeds the audio size limit.', response.status);
|
|
1923
|
+
}
|
|
1924
|
+
const reader = response.body.getReader();
|
|
1925
|
+
const chunks = [];
|
|
1926
|
+
let size = 0;
|
|
1927
|
+
try {
|
|
1928
|
+
while (true) {
|
|
1929
|
+
const chunk = await reader.read();
|
|
1930
|
+
if (chunk.done) {
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
size += chunk.value.byteLength;
|
|
1934
|
+
if (size > limit_1.MAX_AUDIO_BYTES) {
|
|
1935
|
+
await reader.cancel();
|
|
1936
|
+
throw new responses_client_1.ResponsesApiError('Speech response exceeds the audio size limit.', response.status);
|
|
1937
|
+
}
|
|
1938
|
+
chunks.push(chunk.value);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
finally {
|
|
1942
|
+
reader.releaseLock();
|
|
1943
|
+
}
|
|
1944
|
+
if (!size) {
|
|
1945
|
+
throw new responses_client_1.ResponsesApiError('Speech model returned empty audio.', response.status);
|
|
1946
|
+
}
|
|
1947
|
+
return { data: Buffer.concat(chunks), filename: 'reply.wav', mimeType: 'audio/wav' };
|
|
1948
|
+
}
|
|
1949
|
+
catch (error) {
|
|
1950
|
+
request.signal?.throwIfAborted();
|
|
1951
|
+
if (error instanceof responses_client_1.ResponsesApiError) {
|
|
1952
|
+
throw error;
|
|
1953
|
+
}
|
|
1954
|
+
if (controller.signal.aborted) {
|
|
1955
|
+
throw new responses_client_1.ResponsesApiError(`Speech request to ${url} timed out after ${timeoutMs} ms.`);
|
|
1956
|
+
}
|
|
1957
|
+
throw new responses_client_1.ResponsesApiError(`Speech request to ${url} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1958
|
+
}
|
|
1959
|
+
finally {
|
|
1960
|
+
clearTimeout(timer);
|
|
1961
|
+
request.signal?.removeEventListener('abort', cancel);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
|
|
1966
|
+
/***/ },
|
|
1967
|
+
|
|
1968
|
+
/***/ 794
|
|
1969
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
1973
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
1974
|
+
};
|
|
1975
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
1976
|
+
exports.TEST_STT_HELP = void 0;
|
|
1977
|
+
exports.runTestStt = runTestStt;
|
|
1978
|
+
const node_fs_1 = __webpack_require__(3024);
|
|
1979
|
+
const node_path_1 = __importDefault(__webpack_require__(6760));
|
|
1980
|
+
const limit_1 = __webpack_require__(9258);
|
|
1981
|
+
const audio_1 = __webpack_require__(796);
|
|
1982
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
1983
|
+
const command_support_1 = __webpack_require__(2327);
|
|
1984
|
+
const audio_conversion_1 = __webpack_require__(1515);
|
|
1985
|
+
const transcriptions_client_1 = __webpack_require__(6763);
|
|
1986
|
+
const retry_1 = __webpack_require__(7718);
|
|
1987
|
+
exports.TEST_STT_HELP = `Usage: chatserver-models test-stt --provider <name> --name <name> --file <audio> [options]
|
|
1988
|
+
|
|
1989
|
+
Transcribes a local audio file through the configured provider's audio/transcriptions endpoint.
|
|
1990
|
+
|
|
1991
|
+
Options:
|
|
1992
|
+
--provider <name> Provider name from models.json (required)
|
|
1993
|
+
--name <name> Model CLI alias from models.json (required)
|
|
1994
|
+
--file <path> Audio input; --audio is an alias (required)
|
|
1995
|
+
--audio-conversion-cmd <cmd> Optional WAV conversion command using {input} and {output}
|
|
1996
|
+
--timeout <ms> Request timeout (default: provider setting or ${limit_1.DEFAULT_MODEL_TIMEOUT_MS})
|
|
1997
|
+
--json Print { "text": "..." } instead of plain transcript text
|
|
1998
|
+
--config_dir <dir> Config folder
|
|
1999
|
+
-h, --help Show this help
|
|
2000
|
+
|
|
2001
|
+
Supported input: mp3, mpeg, mpga, wav, ogg, flac, m4a, mp4, webm; maximum ${limit_1.MAX_AUDIO_BYTES / (1024 * 1024)} MiB.
|
|
2002
|
+
Use --audio-conversion-cmd for WAV-only engines such as speech.cpp.
|
|
2003
|
+
Paths resolve from the current directory. Original audio is never modified.
|
|
2004
|
+
The API key is read from CHATSERVER_MODEL_API_KEY or the OS credential manager.
|
|
2005
|
+
`;
|
|
2006
|
+
/** Reads a bounded regular file before attempting credentials or model requests. */
|
|
2007
|
+
async function readAudio(file) {
|
|
2008
|
+
const input = await node_fs_1.promises.open(file, 'r');
|
|
2009
|
+
try {
|
|
2010
|
+
const stat = await input.stat();
|
|
2011
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > limit_1.MAX_AUDIO_BYTES) {
|
|
2012
|
+
throw new cli_support_1.CliError(`Audio input must be a nonempty regular file no larger than ${limit_1.MAX_AUDIO_BYTES / (1024 * 1024)} MiB.`);
|
|
2013
|
+
}
|
|
2014
|
+
const data = await input.readFile();
|
|
2015
|
+
const audio = (0, audio_1.audioFile)(data.toString('base64'), node_path_1.default.extname(file).slice(1), 0);
|
|
2016
|
+
return { ...audio, filename: node_path_1.default.basename(file) };
|
|
2017
|
+
}
|
|
2018
|
+
finally {
|
|
2019
|
+
await input.close();
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
/** Exercises the same transcription and optional conversion used by incoming voice messages. */
|
|
2023
|
+
async function runTestStt(argv) {
|
|
2024
|
+
const cli = (0, command_support_1.parseCommandOptions)(argv, {
|
|
2025
|
+
valueFlags: { '--provider': 'provider', '--name': 'name', '--file': 'file', '--audio': 'file',
|
|
2026
|
+
'--timeout': 'timeout', '--audio-conversion-cmd': 'conversion' },
|
|
2027
|
+
booleanFlags: { '--json': 'json' }
|
|
2028
|
+
});
|
|
2029
|
+
if ((0, cli_support_1.hasFlag)(cli, 'help')) {
|
|
2030
|
+
console.log(exports.TEST_STT_HELP);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
if (cli.positionals.length) {
|
|
2034
|
+
throw new cli_support_1.CliError('Unexpected arguments. Pass the audio path with --file.');
|
|
2035
|
+
}
|
|
2036
|
+
const file = node_path_1.default.resolve((0, cli_support_1.requireValue)(cli, 'file', '--file'));
|
|
2037
|
+
const conversion = (0, cli_support_1.optionalValue)(cli, 'conversion');
|
|
2038
|
+
if (conversion !== undefined && (!conversion.trim() || !conversion.includes('{input}') || !conversion.includes('{output}'))) {
|
|
2039
|
+
throw new cli_support_1.CliError('--audio-conversion-cmd must contain {input} and {output}.');
|
|
2040
|
+
}
|
|
2041
|
+
let audio = await readAudio(file);
|
|
2042
|
+
const { provider, model, apiKey, timeoutMs } = await (0, command_support_1.loadTestModel)(cli);
|
|
2043
|
+
if (conversion !== undefined) {
|
|
2044
|
+
audio = await (0, audio_conversion_1.convertAudio)(audio, conversion);
|
|
2045
|
+
}
|
|
2046
|
+
console.error(`Testing STT ${model.name} (${model.id}) via ${provider.provider}...`);
|
|
2047
|
+
const text = await (0, retry_1.withModelRetries)(() => (0, transcriptions_client_1.createTranscription)({
|
|
2048
|
+
baseUrl: provider.baseUrl, model: model.id, apiKey, timeoutMs, audio
|
|
2049
|
+
}));
|
|
2050
|
+
console.log((0, cli_support_1.hasFlag)(cli, 'json') ? JSON.stringify({ text }) : text || '(no speech recognized)');
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
|
|
2054
|
+
/***/ },
|
|
2055
|
+
|
|
2056
|
+
/***/ 7642
|
|
2057
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
2061
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
2062
|
+
};
|
|
2063
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
2064
|
+
exports.TEST_TTS_HELP = void 0;
|
|
2065
|
+
exports.runTestTts = runTestTts;
|
|
2066
|
+
const node_fs_1 = __webpack_require__(3024);
|
|
2067
|
+
const node_path_1 = __importDefault(__webpack_require__(6760));
|
|
2068
|
+
const limit_1 = __webpack_require__(9258);
|
|
2069
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
2070
|
+
const command_support_1 = __webpack_require__(2327);
|
|
2071
|
+
const speech_client_1 = __webpack_require__(5314);
|
|
2072
|
+
const voice_synthesizer_1 = __webpack_require__(7393);
|
|
2073
|
+
const retry_1 = __webpack_require__(7718);
|
|
2074
|
+
const DEFAULT_SPEECH_TEST_TEXT = 'Hello! This is a speech synthesis test.';
|
|
2075
|
+
exports.TEST_TTS_HELP = `Usage: chatserver-models test-tts --provider <name> --name <name> [options]
|
|
2076
|
+
|
|
2077
|
+
Synthesizes local test audio through audio/speech and saves WAV with WeChat's random padding.
|
|
2078
|
+
This command does not send a WeChat message.
|
|
2079
|
+
|
|
2080
|
+
Options:
|
|
2081
|
+
--provider <name> Provider name from models.json (required)
|
|
2082
|
+
--name <name> Model CLI alias from models.json (required)
|
|
2083
|
+
--text <text> Literal text to speak; --prompt is an alias (default: "${DEFAULT_SPEECH_TEST_TEXT}")
|
|
2084
|
+
--voice <name> Provider voice name; overrides voice in --options
|
|
2085
|
+
--options <json> Provider speech options as a JSON object, such as {"speed":1}
|
|
2086
|
+
--output <path> WAV output file (default: tts-test.wav)
|
|
2087
|
+
--force Overwrite an existing output file
|
|
2088
|
+
--timeout <ms> Request timeout (default: provider setting or ${limit_1.DEFAULT_MODEL_TIMEOUT_MS})
|
|
2089
|
+
--config_dir <dir> Config folder
|
|
2090
|
+
-h, --help Show this help
|
|
2091
|
+
|
|
2092
|
+
Options use the same format as tts.options in config.json. ChatServer controls
|
|
2093
|
+
model, input, response_format=wav, and stream_format=audio. The provider must return
|
|
2094
|
+
PCM/float WAV. Independent 0.5–2 second silences are added before and after speech.
|
|
2095
|
+
Paths resolve from the current directory; the output directory must already exist.
|
|
2096
|
+
The API key is read from CHATSERVER_MODEL_API_KEY or the OS credential manager.
|
|
2097
|
+
`;
|
|
2098
|
+
/** Validates operator-supplied provider options before making a model request. */
|
|
2099
|
+
function speechOptions(encoded) {
|
|
2100
|
+
let value = {};
|
|
2101
|
+
if (encoded !== undefined) {
|
|
2102
|
+
try {
|
|
2103
|
+
value = JSON.parse(encoded);
|
|
2104
|
+
}
|
|
2105
|
+
catch {
|
|
2106
|
+
throw new cli_support_1.CliError('--options must contain a valid JSON object.');
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
2110
|
+
throw new cli_support_1.CliError('--options must contain a JSON object.');
|
|
2111
|
+
}
|
|
2112
|
+
for (const key of ['model', 'input', 'response_format', 'stream_format']) {
|
|
2113
|
+
if (Object.hasOwn(value, key)) {
|
|
2114
|
+
throw new cli_support_1.CliError(`--options.${key} is controlled by ChatServer.`);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return value;
|
|
2118
|
+
}
|
|
2119
|
+
/** Catches invalid destinations before synthesis; exclusive writes also guard creation races. */
|
|
2120
|
+
async function checkOutput(output, force) {
|
|
2121
|
+
if (!(await node_fs_1.promises.stat(node_path_1.default.dirname(output))).isDirectory()) {
|
|
2122
|
+
throw new cli_support_1.CliError('The output directory must already exist.');
|
|
2123
|
+
}
|
|
2124
|
+
try {
|
|
2125
|
+
const stat = await node_fs_1.promises.lstat(output);
|
|
2126
|
+
if (!stat.isFile()) {
|
|
2127
|
+
throw new cli_support_1.CliError('The output path must be a regular file.');
|
|
2128
|
+
}
|
|
2129
|
+
if (!force) {
|
|
2130
|
+
throw new cli_support_1.CliError(`Output file already exists: ${output}. Use --force to overwrite it.`);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
catch (error) {
|
|
2134
|
+
if (error.code !== 'ENOENT') {
|
|
2135
|
+
throw error;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
/** Exercises real speech synthesis and the same padding used for WeChat voice replies. */
|
|
2140
|
+
async function runTestTts(argv) {
|
|
2141
|
+
const cli = (0, command_support_1.parseCommandOptions)(argv, {
|
|
2142
|
+
valueFlags: { '--provider': 'provider', '--name': 'name', '--text': 'text', '--prompt': 'text', '--voice': 'voice',
|
|
2143
|
+
'--options': 'options', '--output': 'output', '--timeout': 'timeout' },
|
|
2144
|
+
booleanFlags: { '--force': 'force' }
|
|
2145
|
+
});
|
|
2146
|
+
if ((0, cli_support_1.hasFlag)(cli, 'help')) {
|
|
2147
|
+
console.log(exports.TEST_TTS_HELP);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
if (cli.positionals.length) {
|
|
2151
|
+
throw new cli_support_1.CliError('Unexpected arguments. Pass literal speech text with --text.');
|
|
2152
|
+
}
|
|
2153
|
+
const text = (0, cli_support_1.optionalValue)(cli, 'text') ?? DEFAULT_SPEECH_TEST_TEXT;
|
|
2154
|
+
if (!text.trim() || text.length > limit_1.MAX_TTS_INPUT_CHARS) {
|
|
2155
|
+
throw new cli_support_1.CliError(`--text must contain 1–${limit_1.MAX_TTS_INPUT_CHARS} characters.`);
|
|
2156
|
+
}
|
|
2157
|
+
const options = speechOptions((0, cli_support_1.optionalValue)(cli, 'options'));
|
|
2158
|
+
const voice = (0, cli_support_1.optionalValue)(cli, 'voice');
|
|
2159
|
+
if (voice !== undefined) {
|
|
2160
|
+
if (!voice.trim()) {
|
|
2161
|
+
throw new cli_support_1.CliError('--voice must be nonempty.');
|
|
2162
|
+
}
|
|
2163
|
+
options.voice = voice.trim();
|
|
2164
|
+
}
|
|
2165
|
+
const outputOption = (0, cli_support_1.optionalValue)(cli, 'output') ?? 'tts-test.wav';
|
|
2166
|
+
if (!outputOption.trim() || node_path_1.default.extname(outputOption).toLowerCase() !== '.wav') {
|
|
2167
|
+
throw new cli_support_1.CliError('--output must be a WAV file path ending in .wav.');
|
|
2168
|
+
}
|
|
2169
|
+
const output = node_path_1.default.resolve(outputOption);
|
|
2170
|
+
const force = (0, cli_support_1.hasFlag)(cli, 'force');
|
|
2171
|
+
await checkOutput(output, force);
|
|
2172
|
+
const { provider, model, apiKey, timeoutMs } = await (0, command_support_1.loadTestModel)(cli);
|
|
2173
|
+
console.error(`Testing TTS ${model.name} (${model.id}) via ${provider.provider}...`);
|
|
2174
|
+
const audio = (0, voice_synthesizer_1.padVoiceAudio)(await (0, retry_1.withModelRetries)(() => (0, speech_client_1.createSpeech)({
|
|
2175
|
+
baseUrl: provider.baseUrl, model: model.id, apiKey, timeoutMs, text, options
|
|
2176
|
+
})));
|
|
2177
|
+
await node_fs_1.promises.writeFile(output, audio.data, { flag: force ? 'w' : 'wx', mode: 0o600 });
|
|
2178
|
+
console.log(`Saved speech to ${output} (${audio.data.byteLength} bytes, including random padding).`);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
|
|
2182
|
+
/***/ },
|
|
2183
|
+
|
|
2184
|
+
/***/ 9154
|
|
2185
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
2186
|
+
|
|
2187
|
+
var __webpack_unused_export__;
|
|
2188
|
+
|
|
2189
|
+
__webpack_unused_export__ = ({ value: true });
|
|
2190
|
+
exports.l = void 0;
|
|
2191
|
+
exports.runTest = runTest;
|
|
2192
|
+
const limit_1 = __webpack_require__(9258);
|
|
2193
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
2194
|
+
const command_support_1 = __webpack_require__(2327);
|
|
2195
|
+
const responses_client_1 = __webpack_require__(102);
|
|
2196
|
+
const retry_1 = __webpack_require__(7718);
|
|
2197
|
+
/**
|
|
2198
|
+
* Help text for `chatserver-models test`.
|
|
2199
|
+
*/
|
|
2200
|
+
exports.l = `Usage: chatserver-models test --provider <name> --name <name> [options]
|
|
2201
|
+
|
|
2202
|
+
Sends one prompt to a configured model and prints the reply.
|
|
2203
|
+
|
|
2204
|
+
Options:
|
|
2205
|
+
--provider <name> Provider name from models.json (required)
|
|
2206
|
+
--name <name> Model name from models.json (required)
|
|
2207
|
+
--prompt <text> Prompt to send (default: "${command_support_1.DEFAULT_PROMPT}")
|
|
2208
|
+
--timeout <ms> Override provider timeout for this request (default: provider setting or ${limit_1.DEFAULT_MODEL_TIMEOUT_MS})
|
|
2209
|
+
--json Print the raw API response instead of just the text
|
|
2210
|
+
--config_dir <dir> Config folder
|
|
2211
|
+
-h, --help Show this help
|
|
2212
|
+
|
|
2213
|
+
The API key is read from CHATSERVER_MODEL_API_KEY or the OS credential manager
|
|
2214
|
+
(account: the provider name).
|
|
2215
|
+
`;
|
|
2216
|
+
/**
|
|
2217
|
+
* Sends one prompt to a configured model.
|
|
2218
|
+
*/
|
|
2219
|
+
async function runTest(argv) {
|
|
2220
|
+
const cli = (0, command_support_1.parseCommandOptions)(argv, {
|
|
2221
|
+
valueFlags: {
|
|
2222
|
+
'--provider': 'provider',
|
|
2223
|
+
'--name': 'name',
|
|
2224
|
+
'--prompt': 'prompt',
|
|
2225
|
+
'--timeout': 'timeout'
|
|
2226
|
+
},
|
|
2227
|
+
booleanFlags: { '--json': 'json' }
|
|
2228
|
+
});
|
|
2229
|
+
if ((0, cli_support_1.hasFlag)(cli, 'help')) {
|
|
2230
|
+
console.log(exports.l);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const { provider, model, apiKey, timeoutMs } = await (0, command_support_1.loadTestModel)(cli);
|
|
2234
|
+
const prompt = (0, cli_support_1.optionalValue)(cli, 'prompt') ?? command_support_1.DEFAULT_PROMPT;
|
|
2235
|
+
console.log(`Testing ${model.name} (${model.id}) via ${provider.provider} at ${provider.baseUrl}...`);
|
|
2236
|
+
const result = await (0, retry_1.withModelRetries)(() => (0, responses_client_1.createResponse)({
|
|
2237
|
+
baseUrl: provider.baseUrl,
|
|
2238
|
+
model: model.id,
|
|
2239
|
+
apiKey,
|
|
2240
|
+
input: prompt,
|
|
2241
|
+
timeoutMs
|
|
2242
|
+
}));
|
|
2243
|
+
if ((0, cli_support_1.hasFlag)(cli, 'json')) {
|
|
2244
|
+
console.log(JSON.stringify(result.raw, null, 2));
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
console.log('');
|
|
2248
|
+
console.log(result.outputText === '' ? '(no output text in response)' : result.outputText);
|
|
2249
|
+
if (result.id !== undefined) {
|
|
2250
|
+
console.log('');
|
|
2251
|
+
console.log(`Response id: ${result.id}`);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
|
|
2256
|
+
/***/ },
|
|
2257
|
+
|
|
2258
|
+
/***/ 6763
|
|
2259
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
2260
|
+
|
|
2261
|
+
var __webpack_unused_export__;
|
|
2262
|
+
|
|
2263
|
+
__webpack_unused_export__ = ({ value: true });
|
|
2264
|
+
exports.createTranscription = createTranscription;
|
|
2265
|
+
const limit_1 = __webpack_require__(9258);
|
|
2266
|
+
const responses_client_1 = __webpack_require__(102);
|
|
2267
|
+
/** Calls an OpenAI-compatible audio/transcriptions endpoint using native multipart upload. */
|
|
2268
|
+
async function createTranscription(request) {
|
|
2269
|
+
request.signal?.throwIfAborted();
|
|
2270
|
+
if (request.audio.data.byteLength === 0 || request.audio.data.byteLength > limit_1.MAX_AUDIO_BYTES) {
|
|
2271
|
+
throw new Error(`Transcription audio must be nonempty and no larger than ${limit_1.MAX_AUDIO_BYTES / (1024 * 1024)} MiB.`);
|
|
2272
|
+
}
|
|
2273
|
+
const url = `${request.baseUrl.replace(/\/+$/, '')}/audio/transcriptions`;
|
|
2274
|
+
const timeoutMs = (0, responses_client_1.resolveModelTimeoutMs)(request.timeoutMs);
|
|
2275
|
+
const controller = new AbortController();
|
|
2276
|
+
const cancel = () => controller.abort();
|
|
2277
|
+
request.signal?.addEventListener('abort', cancel, { once: true });
|
|
2278
|
+
const timer = setTimeout(cancel, timeoutMs);
|
|
2279
|
+
try {
|
|
2280
|
+
const form = new FormData();
|
|
2281
|
+
form.set('model', request.model);
|
|
2282
|
+
form.set('response_format', 'json');
|
|
2283
|
+
form.set('file', new Blob([new Uint8Array(request.audio.data)], { type: request.audio.mimeType }), request.audio.filename);
|
|
2284
|
+
const response = await fetch(url, {
|
|
2285
|
+
method: 'POST',
|
|
2286
|
+
headers: { Authorization: `Bearer ${request.apiKey}` },
|
|
2287
|
+
body: form,
|
|
2288
|
+
signal: controller.signal,
|
|
2289
|
+
redirect: 'error'
|
|
2290
|
+
});
|
|
2291
|
+
if (!response.ok) {
|
|
2292
|
+
throw new responses_client_1.ResponsesApiError(`Transcription request to ${url} failed with HTTP ${response.status}.`, response.status);
|
|
2293
|
+
}
|
|
2294
|
+
let result;
|
|
2295
|
+
try {
|
|
2296
|
+
result = await response.json();
|
|
2297
|
+
}
|
|
2298
|
+
catch (error) {
|
|
2299
|
+
if (controller.signal.aborted) {
|
|
2300
|
+
throw error;
|
|
2301
|
+
}
|
|
2302
|
+
throw new responses_client_1.ResponsesApiError('Transcription model returned invalid JSON.', response.status);
|
|
2303
|
+
}
|
|
2304
|
+
if (typeof result !== 'object' || result === null || Array.isArray(result) ||
|
|
2305
|
+
!('text' in result) || typeof result.text !== 'string' || result.text.length > limit_1.MAX_TRANSCRIPT_CHARS) {
|
|
2306
|
+
throw new responses_client_1.ResponsesApiError(`Transcription model must return a text string of at most ${limit_1.MAX_TRANSCRIPT_CHARS} characters.`, response.status);
|
|
2307
|
+
}
|
|
2308
|
+
return result.text.trim();
|
|
2309
|
+
}
|
|
2310
|
+
catch (error) {
|
|
2311
|
+
request.signal?.throwIfAborted();
|
|
2312
|
+
if (controller.signal.aborted) {
|
|
2313
|
+
throw new responses_client_1.ResponsesApiError(`Transcription request to ${url} timed out after ${timeoutMs} ms.`);
|
|
2314
|
+
}
|
|
2315
|
+
if (error instanceof responses_client_1.ResponsesApiError) {
|
|
2316
|
+
throw error;
|
|
2317
|
+
}
|
|
2318
|
+
throw new responses_client_1.ResponsesApiError(`Transcription request to ${url} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2319
|
+
}
|
|
2320
|
+
finally {
|
|
2321
|
+
clearTimeout(timer);
|
|
2322
|
+
request.signal?.removeEventListener('abort', cancel);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
|
|
2327
|
+
/***/ },
|
|
2328
|
+
|
|
2329
|
+
/***/ 4439
|
|
2330
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
2331
|
+
|
|
2332
|
+
var __webpack_unused_export__;
|
|
2333
|
+
|
|
2334
|
+
__webpack_unused_export__ = ({ value: true });
|
|
2335
|
+
exports.C = void 0;
|
|
2336
|
+
exports.runUpdate = runUpdate;
|
|
2337
|
+
const limit_1 = __webpack_require__(9258);
|
|
2338
|
+
const credentials_1 = __webpack_require__(854);
|
|
2339
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
2340
|
+
const command_support_1 = __webpack_require__(2327);
|
|
2341
|
+
const model_store_1 = __webpack_require__(317);
|
|
2342
|
+
const setup_1 = __webpack_require__(2993);
|
|
2343
|
+
/**
|
|
2344
|
+
* Help text for `chatserver-models update`.
|
|
2345
|
+
*/
|
|
2346
|
+
exports.C = `Usage: chatserver-models update [options]
|
|
2347
|
+
|
|
2348
|
+
Updates a provider or one of its models. Only the options you pass are changed.
|
|
2349
|
+
Run without edits for a guided editor. Press Enter to keep current values.
|
|
2350
|
+
|
|
2351
|
+
Options:
|
|
2352
|
+
--provider <name> Provider name (prompted in a terminal if omitted)
|
|
2353
|
+
--base-url <url> New OpenAI-compatible base URL for the provider
|
|
2354
|
+
--timeout <ms> New model timeout for all models from the provider
|
|
2355
|
+
--api-key <key> Replace the provider's stored API key
|
|
2356
|
+
--name <name> Model name to update
|
|
2357
|
+
--model <id> New model identifier for --name
|
|
2358
|
+
--new-name <name> Rename the model given by --name
|
|
2359
|
+
--config_dir <dir> Config folder
|
|
2360
|
+
-i, --interactive Open the guided editor even when edits are supplied
|
|
2361
|
+
-h, --help Show this help
|
|
2362
|
+
`;
|
|
2363
|
+
/**
|
|
2364
|
+
* Updates provider or model details, and optionally the provider's API key.
|
|
2365
|
+
*/
|
|
2366
|
+
async function runUpdate(argv) {
|
|
2367
|
+
const cli = (0, command_support_1.parseCommandOptions)(argv, {
|
|
2368
|
+
valueFlags: {
|
|
2369
|
+
'--provider': 'provider',
|
|
2370
|
+
'--base-url': 'baseUrl',
|
|
2371
|
+
'--timeout': 'timeout',
|
|
2372
|
+
'--api-key': 'apiKey',
|
|
2373
|
+
'--name': 'name',
|
|
2374
|
+
'--model': 'model',
|
|
2375
|
+
'--new-name': 'newName'
|
|
2376
|
+
},
|
|
2377
|
+
booleanFlags: { '--interactive': 'interactive', '-i': 'interactive' }
|
|
2378
|
+
});
|
|
2379
|
+
if ((0, cli_support_1.hasFlag)(cli, 'help')) {
|
|
2380
|
+
console.log(exports.C);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const configDir = (0, cli_support_1.resolveConfigDir)((0, cli_support_1.optionalValue)(cli, 'configDir'));
|
|
2384
|
+
let newBaseUrl = (0, setup_1.setupValue)(cli, 'baseUrl', '--base-url');
|
|
2385
|
+
if (newBaseUrl !== undefined) {
|
|
2386
|
+
newBaseUrl = (0, setup_1.setupBaseUrl)(newBaseUrl);
|
|
2387
|
+
}
|
|
2388
|
+
const timeoutValue = (0, cli_support_1.optionalValue)(cli, 'timeout');
|
|
2389
|
+
let timeoutMs = timeoutValue === undefined ? undefined : (0, command_support_1.modelTimeoutOption)(timeoutValue);
|
|
2390
|
+
let apiKey = (0, setup_1.setupValue)(cli, 'apiKey', '--api-key');
|
|
2391
|
+
let modelName = (0, setup_1.setupValue)(cli, 'name', '--name');
|
|
2392
|
+
let newModelId = (0, setup_1.setupValue)(cli, 'model', '--model');
|
|
2393
|
+
let newName = (0, setup_1.setupValue)(cli, 'newName', '--new-name');
|
|
2394
|
+
const hasEdits = newBaseUrl !== undefined || timeoutMs !== undefined || apiKey !== undefined ||
|
|
2395
|
+
newModelId !== undefined || newName !== undefined;
|
|
2396
|
+
const guided = (0, setup_1.setupInteractive)(cli, !hasEdits);
|
|
2397
|
+
const providerName = await (0, setup_1.setupProvider)(await (0, model_store_1.loadModels)(configDir), cli, false);
|
|
2398
|
+
const { file, provider } = await (0, command_support_1.requireProvider)(configDir, providerName);
|
|
2399
|
+
if (!guided && !hasEdits) {
|
|
2400
|
+
throw new cli_support_1.CliError('Nothing to update. Pass --base-url, --timeout, --api-key, or --name with --model/--new-name.');
|
|
2401
|
+
}
|
|
2402
|
+
let editProvider = false;
|
|
2403
|
+
if (guided && modelName === undefined && newModelId === undefined && newName === undefined) {
|
|
2404
|
+
const target = await (0, setup_1.setupChoice)('Update', [
|
|
2405
|
+
{ value: 'provider', label: 'Provider settings (URL, timeout, API key)' },
|
|
2406
|
+
...provider.models.map((model) => ({ value: `model:${model.name}`, name: model.name, label: `${model.name} (${model.id})` }))
|
|
2407
|
+
]);
|
|
2408
|
+
editProvider = target === 'provider';
|
|
2409
|
+
if (!editProvider) {
|
|
2410
|
+
modelName = target.slice('model:'.length);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
if (modelName === undefined && (newModelId !== undefined || newName !== undefined)) {
|
|
2414
|
+
if (!process.stdin.isTTY) {
|
|
2415
|
+
throw new cli_support_1.CliError('--name is required to update or rename a model.');
|
|
2416
|
+
}
|
|
2417
|
+
if (provider.models.length === 0) {
|
|
2418
|
+
throw new cli_support_1.CliError('This provider has no models. Run chatserver-models add first.');
|
|
2419
|
+
}
|
|
2420
|
+
modelName = await (0, setup_1.setupChoice)('Model', provider.models.map((model) => ({ value: model.name, label: `${model.name} (${model.id})` })));
|
|
2421
|
+
}
|
|
2422
|
+
const model = modelName === undefined ? undefined : (0, command_support_1.requireModel)(provider, modelName);
|
|
2423
|
+
if (guided && model !== undefined) {
|
|
2424
|
+
newModelId ??= await (0, setup_1.setupPrompt)('Model ID', model.id);
|
|
2425
|
+
newName ??= await (0, setup_1.setupPrompt)('Model name', model.name);
|
|
2426
|
+
}
|
|
2427
|
+
if (editProvider) {
|
|
2428
|
+
newBaseUrl ??= await (0, setup_1.setupPrompt)('API base URL', provider.baseUrl, setup_1.setupBaseUrl);
|
|
2429
|
+
const currentTimeout = provider.timeoutMs ?? limit_1.DEFAULT_MODEL_TIMEOUT_MS;
|
|
2430
|
+
timeoutMs ??= Number(await (0, setup_1.setupPrompt)('Timeout (ms)', String(currentTimeout), (value) => String((0, command_support_1.modelTimeoutOption)(value))));
|
|
2431
|
+
if (apiKey === undefined) {
|
|
2432
|
+
apiKey = (await (0, cli_support_1.promptHidden)('New API key (Enter to keep current): ')).trim() || undefined;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
// Validate every edit before changing either the credential manager or models.json.
|
|
2436
|
+
if (newName !== undefined && newName !== model?.name && (0, model_store_1.findModel)(provider, newName) !== undefined) {
|
|
2437
|
+
throw new cli_support_1.CliError(`Model "${newName}" already exists for provider "${provider.provider}".`);
|
|
2438
|
+
}
|
|
2439
|
+
const changed = newBaseUrl !== undefined && newBaseUrl !== provider.baseUrl ||
|
|
2440
|
+
timeoutMs !== undefined && timeoutMs !== provider.timeoutMs || apiKey !== undefined ||
|
|
2441
|
+
model !== undefined && (newModelId !== undefined && newModelId !== model.id || newName !== undefined && newName !== model.name);
|
|
2442
|
+
if (!changed) {
|
|
2443
|
+
console.log('No changes.');
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (newBaseUrl !== undefined) {
|
|
2447
|
+
provider.baseUrl = newBaseUrl;
|
|
2448
|
+
}
|
|
2449
|
+
if (timeoutMs !== undefined) {
|
|
2450
|
+
provider.timeoutMs = timeoutMs;
|
|
2451
|
+
}
|
|
2452
|
+
if (apiKey !== undefined) {
|
|
2453
|
+
const credentials = (0, credentials_1.createCredentialStore)();
|
|
2454
|
+
await credentials.set(provider.provider, apiKey);
|
|
2455
|
+
console.log(`API key updated in the ${credentials.name} (account: "${provider.provider}").`);
|
|
2456
|
+
}
|
|
2457
|
+
const previousId = model?.id;
|
|
2458
|
+
if (model !== undefined) {
|
|
2459
|
+
if (newModelId !== undefined) {
|
|
2460
|
+
model.id = newModelId;
|
|
2461
|
+
}
|
|
2462
|
+
if (newName !== undefined) {
|
|
2463
|
+
model.name = newName;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
const modelsPath = await (0, model_store_1.saveModels)(configDir, file);
|
|
2467
|
+
console.log(`Updated provider "${providerName}" in ${modelsPath}.`);
|
|
2468
|
+
if (model !== undefined) {
|
|
2469
|
+
console.log(`Config reference: ${providerName}/${model.id}`);
|
|
2470
|
+
if (previousId !== model.id) {
|
|
2471
|
+
console.log(`Update config.json references from ${providerName}/${previousId} to ${providerName}/${model.id}.`);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
/***/ },
|
|
2478
|
+
|
|
2479
|
+
/***/ 7393
|
|
2480
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
2481
|
+
|
|
2482
|
+
var __webpack_unused_export__;
|
|
2483
|
+
|
|
2484
|
+
__webpack_unused_export__ = ({ value: true });
|
|
2485
|
+
__webpack_unused_export__ = void 0;
|
|
2486
|
+
exports.padVoiceAudio = padVoiceAudio;
|
|
2487
|
+
const node_crypto_1 = __webpack_require__(7598);
|
|
2488
|
+
const limit_1 = __webpack_require__(9258);
|
|
2489
|
+
/** Adds actual silence to PCM/float WAV samples, independently at either end. */
|
|
2490
|
+
function padVoiceAudio(audio) {
|
|
2491
|
+
const wav = Buffer.from(audio.data);
|
|
2492
|
+
const invalid = () => new Error('TTS must return a valid, uncompressed PCM or float WAV file for voice padding.');
|
|
2493
|
+
if (wav.length < 44 || wav.length > limit_1.MAX_AUDIO_BYTES || wav.toString('ascii', 0, 4) !== 'RIFF' ||
|
|
2494
|
+
wav.toString('ascii', 8, 12) !== 'WAVE') {
|
|
2495
|
+
throw invalid();
|
|
2496
|
+
}
|
|
2497
|
+
let format;
|
|
2498
|
+
let samples;
|
|
2499
|
+
for (let offset = 12; offset + 8 <= wav.length;) {
|
|
2500
|
+
const id = wav.toString('ascii', offset, offset + 4);
|
|
2501
|
+
const declared = wav.readUInt32LE(offset + 4);
|
|
2502
|
+
const start = offset + 8;
|
|
2503
|
+
// Streaming WAV encoders can leave RIFF/data sizes as large placeholders.
|
|
2504
|
+
const size = id === 'data' && declared >= 0x7fff0000 ? wav.length - start : declared;
|
|
2505
|
+
if (start + size > wav.length) {
|
|
2506
|
+
throw invalid();
|
|
2507
|
+
}
|
|
2508
|
+
if (id === 'fmt ') {
|
|
2509
|
+
if (format !== undefined) {
|
|
2510
|
+
throw invalid();
|
|
2511
|
+
}
|
|
2512
|
+
format = wav.subarray(start, start + size);
|
|
2513
|
+
}
|
|
2514
|
+
else if (id === 'data') {
|
|
2515
|
+
if (samples !== undefined) {
|
|
2516
|
+
throw invalid();
|
|
2517
|
+
}
|
|
2518
|
+
samples = wav.subarray(start, start + size);
|
|
2519
|
+
}
|
|
2520
|
+
offset = start + size + size % 2;
|
|
2521
|
+
}
|
|
2522
|
+
if (format === undefined || format.length < 16 || samples === undefined || !samples.length) {
|
|
2523
|
+
throw invalid();
|
|
2524
|
+
}
|
|
2525
|
+
let encoding = format.readUInt16LE(0);
|
|
2526
|
+
if (encoding === 0xfffe && format.length >= 40 &&
|
|
2527
|
+
format.subarray(28, 40).equals(Buffer.from('00001000800000aa00389b71', 'hex'))) {
|
|
2528
|
+
encoding = format.readUInt32LE(24);
|
|
2529
|
+
}
|
|
2530
|
+
const channels = format.readUInt16LE(2);
|
|
2531
|
+
const sampleRate = format.readUInt32LE(4);
|
|
2532
|
+
const frameBytes = format.readUInt16LE(12);
|
|
2533
|
+
const bits = format.readUInt16LE(14);
|
|
2534
|
+
if (!channels || !sampleRate || !frameBytes || frameBytes !== channels * bits / 8 || samples.length % frameBytes !== 0 ||
|
|
2535
|
+
!(encoding === 1 && [8, 16, 24, 32].includes(bits) || encoding === 3 && [32, 64].includes(bits))) {
|
|
2536
|
+
throw invalid();
|
|
2537
|
+
}
|
|
2538
|
+
const leadingMs = (0, node_crypto_1.randomInt)(limit_1.MIN_VOICE_PADDING_MS, limit_1.MAX_VOICE_PADDING_MS + 1);
|
|
2539
|
+
const trailingMs = (0, node_crypto_1.randomInt)(limit_1.MIN_VOICE_PADDING_MS, limit_1.MAX_VOICE_PADDING_MS + 1);
|
|
2540
|
+
const leadingBytes = Math.round(sampleRate * leadingMs / 1000) * frameBytes;
|
|
2541
|
+
const trailingBytes = Math.round(sampleRate * trailingMs / 1000) * frameBytes;
|
|
2542
|
+
const dataSize = leadingBytes + samples.length + trailingBytes;
|
|
2543
|
+
const formatSize = format.length + format.length % 2;
|
|
2544
|
+
const totalSize = 12 + 8 + formatSize + 8 + dataSize + dataSize % 2;
|
|
2545
|
+
if (totalSize > limit_1.MAX_AUDIO_BYTES) {
|
|
2546
|
+
throw new Error('Padded speech exceeds the audio size limit.');
|
|
2547
|
+
}
|
|
2548
|
+
const output = Buffer.alloc(totalSize);
|
|
2549
|
+
output.write('RIFF', 0);
|
|
2550
|
+
output.writeUInt32LE(totalSize - 8, 4);
|
|
2551
|
+
output.write('WAVEfmt ', 8);
|
|
2552
|
+
output.writeUInt32LE(format.length, 16);
|
|
2553
|
+
format.copy(output, 20);
|
|
2554
|
+
const dataOffset = 20 + formatSize;
|
|
2555
|
+
output.write('data', dataOffset);
|
|
2556
|
+
output.writeUInt32LE(dataSize, dataOffset + 4);
|
|
2557
|
+
// Unsigned 8-bit PCM uses midpoint 128 for silence; other supported samples use zero.
|
|
2558
|
+
output.fill(encoding === 1 && bits === 8 ? 128 : 0, dataOffset + 8, dataOffset + 8 + dataSize);
|
|
2559
|
+
samples.copy(output, dataOffset + 8 + leadingBytes);
|
|
2560
|
+
return { data: output, filename: 'reply.wav', mimeType: 'audio/wav' };
|
|
2561
|
+
}
|
|
2562
|
+
/** Synthesizes the bounded public reply text and pads it before any send attempt. */
|
|
2563
|
+
class VoiceSynthesizer {
|
|
2564
|
+
model;
|
|
2565
|
+
options;
|
|
2566
|
+
constructor(model, options = {}) {
|
|
2567
|
+
this.model = model;
|
|
2568
|
+
this.options = options;
|
|
2569
|
+
}
|
|
2570
|
+
async synthesize(text, signal) {
|
|
2571
|
+
const audio = await this.model.synthesize(text, this.options, signal);
|
|
2572
|
+
signal?.throwIfAborted();
|
|
2573
|
+
return padVoiceAudio(audio);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
__webpack_unused_export__ = VoiceSynthesizer;
|
|
2577
|
+
|
|
2578
|
+
|
|
2579
|
+
/***/ },
|
|
2580
|
+
|
|
2581
|
+
/***/ 1421
|
|
2582
|
+
(module) {
|
|
2583
|
+
|
|
2584
|
+
module.exports = require("node:child_process");
|
|
2585
|
+
|
|
2586
|
+
/***/ },
|
|
2587
|
+
|
|
2588
|
+
/***/ 7598
|
|
2589
|
+
(module) {
|
|
2590
|
+
|
|
2591
|
+
module.exports = require("node:crypto");
|
|
2592
|
+
|
|
2593
|
+
/***/ },
|
|
2594
|
+
|
|
2595
|
+
/***/ 3024
|
|
2596
|
+
(module) {
|
|
2597
|
+
|
|
2598
|
+
module.exports = require("node:fs");
|
|
2599
|
+
|
|
2600
|
+
/***/ },
|
|
2601
|
+
|
|
2602
|
+
/***/ 8161
|
|
2603
|
+
(module) {
|
|
2604
|
+
|
|
2605
|
+
module.exports = require("node:os");
|
|
2606
|
+
|
|
2607
|
+
/***/ },
|
|
2608
|
+
|
|
2609
|
+
/***/ 6760
|
|
2610
|
+
(module) {
|
|
2611
|
+
|
|
2612
|
+
module.exports = require("node:path");
|
|
2613
|
+
|
|
2614
|
+
/***/ },
|
|
2615
|
+
|
|
2616
|
+
/***/ 6848
|
|
2617
|
+
(module) {
|
|
2618
|
+
|
|
2619
|
+
module.exports = require("node:readline/promises");
|
|
2620
|
+
|
|
2621
|
+
/***/ },
|
|
2622
|
+
|
|
2623
|
+
/***/ 8500
|
|
2624
|
+
(module) {
|
|
2625
|
+
|
|
2626
|
+
module.exports = require("node:timers/promises");
|
|
2627
|
+
|
|
2628
|
+
/***/ }
|
|
2629
|
+
|
|
2630
|
+
/******/ });
|
|
2631
|
+
/************************************************************************/
|
|
2632
|
+
/******/ // The module cache
|
|
2633
|
+
/******/ const __webpack_module_cache__ = {};
|
|
2634
|
+
/******/
|
|
2635
|
+
/******/ // The require function
|
|
2636
|
+
/******/ function __webpack_require__(moduleId) {
|
|
2637
|
+
/******/ // Check if module is in cache
|
|
2638
|
+
/******/ const cachedModule = __webpack_module_cache__[moduleId];
|
|
2639
|
+
/******/ if (cachedModule !== undefined) {
|
|
2640
|
+
/******/ return cachedModule.exports;
|
|
2641
|
+
/******/ }
|
|
2642
|
+
/******/ // Create a new module (and put it into the cache)
|
|
2643
|
+
/******/ const module = __webpack_module_cache__[moduleId] = {
|
|
2644
|
+
/******/ // no module.id needed
|
|
2645
|
+
/******/ // no module.loaded needed
|
|
2646
|
+
/******/ exports: {}
|
|
2647
|
+
/******/ };
|
|
2648
|
+
/******/
|
|
2649
|
+
/******/ // Execute the module function
|
|
2650
|
+
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
2651
|
+
/******/
|
|
2652
|
+
/******/ // Return the exports of the module
|
|
2653
|
+
/******/ return module.exports;
|
|
2654
|
+
/******/ }
|
|
2655
|
+
/******/
|
|
2656
|
+
/************************************************************************/
|
|
2657
|
+
let __webpack_exports__ = {};
|
|
2658
|
+
// This entry needs to be wrapped in an IIFE because it uses a non-standard name for the exports (exports).
|
|
2659
|
+
(() => {
|
|
2660
|
+
let exports = __webpack_exports__;
|
|
2661
|
+
var __webpack_unused_export__;
|
|
2662
|
+
|
|
2663
|
+
__webpack_unused_export__ = ({ value: true });
|
|
2664
|
+
const cli_support_1 = __webpack_require__(8256);
|
|
2665
|
+
const add_1 = __webpack_require__(6115);
|
|
2666
|
+
const list_1 = __webpack_require__(9598);
|
|
2667
|
+
const remove_1 = __webpack_require__(5978);
|
|
2668
|
+
const test_1 = __webpack_require__(9154);
|
|
2669
|
+
const test_stt_1 = __webpack_require__(794);
|
|
2670
|
+
const test_tts_1 = __webpack_require__(7642);
|
|
2671
|
+
const update_1 = __webpack_require__(4439);
|
|
2672
|
+
const COMMANDS = ['add', 'test', 'test-stt', 'test-tts', 'list', 'update', 'remove'];
|
|
2673
|
+
/**
|
|
2674
|
+
* General help text.
|
|
2675
|
+
*/
|
|
2676
|
+
const HELP_TEXT = `Usage: chatserver-models <command> [options]
|
|
2677
|
+
|
|
2678
|
+
Manage OpenAI-compatible chat, speech-to-text, and text-to-speech models. Provider details live in
|
|
2679
|
+
<config_dir>/models.json; each provider's API key lives in the OS credential manager
|
|
2680
|
+
under the provider name.
|
|
2681
|
+
|
|
2682
|
+
Commands:
|
|
2683
|
+
add Add a model (guided setup when options are omitted)
|
|
2684
|
+
test Send a prompt to a configured model
|
|
2685
|
+
test-stt Transcribe a local audio file with a configured model
|
|
2686
|
+
test-tts Synthesize test speech and save a padded WAV file
|
|
2687
|
+
list List configured providers and models
|
|
2688
|
+
update Edit provider/model settings (guided editor without edits)
|
|
2689
|
+
remove Remove a model (and its provider once empty)
|
|
2690
|
+
|
|
2691
|
+
Common options:
|
|
2692
|
+
--config_dir <dir> Config folder (default: CHATSERVER_CONFIG_DIR or the per-user folder)
|
|
2693
|
+
-h, --help Show help for a command
|
|
2694
|
+
|
|
2695
|
+
Run "chatserver-models <command> --help" for command-specific options.
|
|
2696
|
+
Run "chatserver-models add" or "chatserver-models update" for guided terminal setup.
|
|
2697
|
+
`;
|
|
2698
|
+
async function main() {
|
|
2699
|
+
const argv = process.argv.slice(2);
|
|
2700
|
+
const command = argv[0];
|
|
2701
|
+
if (command === undefined || command === '-h' || command === '--help' || command === 'help') {
|
|
2702
|
+
console.log(HELP_TEXT);
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
const rest = argv.slice(1);
|
|
2706
|
+
switch (command) {
|
|
2707
|
+
case 'add':
|
|
2708
|
+
await (0, add_1.runAdd)(rest);
|
|
2709
|
+
return;
|
|
2710
|
+
case 'test':
|
|
2711
|
+
await (0, test_1.runTest)(rest);
|
|
2712
|
+
return;
|
|
2713
|
+
case 'test-stt':
|
|
2714
|
+
await (0, test_stt_1.runTestStt)(rest);
|
|
2715
|
+
return;
|
|
2716
|
+
case 'test-tts':
|
|
2717
|
+
await (0, test_tts_1.runTestTts)(rest);
|
|
2718
|
+
return;
|
|
2719
|
+
case 'list':
|
|
2720
|
+
await (0, list_1.runList)(rest);
|
|
2721
|
+
return;
|
|
2722
|
+
case 'update':
|
|
2723
|
+
await (0, update_1.runUpdate)(rest);
|
|
2724
|
+
return;
|
|
2725
|
+
case 'remove':
|
|
2726
|
+
await (0, remove_1.runRemove)(rest);
|
|
2727
|
+
return;
|
|
2728
|
+
default:
|
|
2729
|
+
throw new cli_support_1.CliError(`Unknown command: ${command}. Expected one of: ${COMMANDS.join(', ')}.`);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
main().catch((error) => {
|
|
2733
|
+
(0, cli_support_1.reportError)(error);
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
})();
|
|
2737
|
+
|
|
2738
|
+
/******/ })()
|
|
2739
|
+
;
|
|
2740
|
+
//# sourceMappingURL=chatserver-models.js.map
|