@imenam/unified-llm-client 2.1.2 → 2.2.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/dist/adapters/qwen/QwenLogAdapter.d.ts +5 -0
- package/dist/adapters/qwen/QwenLogAdapter.d.ts.map +1 -1
- package/dist/adapters/qwen/QwenLogAdapter.js +18 -9
- package/dist/adapters/qwen/QwenPromptAdapter.d.ts.map +1 -1
- package/dist/adapters/qwen/QwenPromptAdapter.js +136 -79
- package/dist/core/Models.d.ts +1 -0
- package/dist/core/Models.d.ts.map +1 -1
- package/dist/core/Models.js +12 -0
- package/dist/examples/qwen-omni-audio-example.d.ts +2 -0
- package/dist/examples/qwen-omni-audio-example.d.ts.map +1 -0
- package/dist/examples/qwen-omni-audio-example.js +324 -0
- package/package.json +2 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { UnifiedLog } from "../../common/types";
|
|
2
2
|
import { LogAdapter, LogData } from "../../core/LLMAdapter";
|
|
3
3
|
export declare class QwenLogAdapter implements LogAdapter {
|
|
4
|
+
/**
|
|
5
|
+
* Remplace les données base64 des images et de l'audio par un marqueur avant
|
|
6
|
+
* journalisation. Un message dont `content` est une chaîne est renvoyé tel
|
|
7
|
+
* quel. Les messages reçus ne sont pas modifiés.
|
|
8
|
+
*/
|
|
4
9
|
removeImages(messages: any[]): any[];
|
|
5
10
|
toUnifiedLog(data: LogData): UnifiedLog;
|
|
6
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QwenLogAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/qwen/QwenLogAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAY,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAE5D,qBAAa,cAAe,YAAW,UAAU;IAC/C,YAAY,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE;
|
|
1
|
+
{"version":3,"file":"QwenLogAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/qwen/QwenLogAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAY,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAE5D,qBAAa,cAAe,YAAW,UAAU;IAC/C;;;;OAIG;IACH,YAAY,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE;IAyBpC,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,UAAU;CAgCxC"}
|
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QwenLogAdapter = void 0;
|
|
4
4
|
class QwenLogAdapter {
|
|
5
|
+
/**
|
|
6
|
+
* Remplace les données base64 des images et de l'audio par un marqueur avant
|
|
7
|
+
* journalisation. Un message dont `content` est une chaîne est renvoyé tel
|
|
8
|
+
* quel. Les messages reçus ne sont pas modifiés.
|
|
9
|
+
*/
|
|
5
10
|
removeImages(messages) {
|
|
6
|
-
|
|
7
|
-
message
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
return messages.map((message) => {
|
|
12
|
+
if (!Array.isArray(message === null || message === void 0 ? void 0 : message.content))
|
|
13
|
+
return message;
|
|
14
|
+
return Object.assign(Object.assign({}, message), { content: message.content.map((content) => {
|
|
15
|
+
var _a, _b;
|
|
16
|
+
if ((content === null || content === void 0 ? void 0 : content.type) === 'image_url' && typeof ((_a = content.image_url) === null || _a === void 0 ? void 0 : _a.url) === 'string') {
|
|
17
|
+
return Object.assign(Object.assign({}, content), { image_url: Object.assign(Object.assign({}, content.image_url), { url: content.image_url.url.split(',')[0] + "removed" }) });
|
|
18
|
+
}
|
|
19
|
+
if ((content === null || content === void 0 ? void 0 : content.type) === 'input_audio' && typeof ((_b = content.input_audio) === null || _b === void 0 ? void 0 : _b.data) === 'string') {
|
|
20
|
+
return Object.assign(Object.assign({}, content), { input_audio: Object.assign(Object.assign({}, content.input_audio), { data: "removed" }) });
|
|
21
|
+
}
|
|
22
|
+
return content;
|
|
23
|
+
}) });
|
|
14
24
|
});
|
|
15
|
-
return formattedMessages;
|
|
16
25
|
}
|
|
17
26
|
toUnifiedLog(data) {
|
|
18
27
|
var _a, _b, _c, _d, _e;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QwenPromptAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/qwen/QwenPromptAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,kBAAkB,EAAqB,MAAM,uBAAuB,CAAC;AACtI,OAAO,EAAE,WAAW,EAAY,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"QwenPromptAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/qwen/QwenPromptAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,kBAAkB,EAAqB,MAAM,uBAAuB,CAAC;AACtI,OAAO,EAAE,WAAW,EAAY,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAoFzF,qBAAa,iBAAkB,YAAW,aAAa;IAIzC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAa;gBAEA,IAAI,EAAE,oBAAoB;IAMhD,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,GAAG;IAiGzC,cAAc,CAClB,QAAQ,EAAE,GAAG,EACb,KAAK,GAAE,cAAc,EAAO,EAC5B,UAAU,GAAE,MAAM,GAAG,MAAM,GAAG,MAAe,EAC7C,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EAChC,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC;IAmClB,kBAAkB,CAAC,CAAC,EACxB,QAAQ,EACR,KAAK,EACL,UAAU,EACV,KAAK,EACL,OAAO,EACP,eAAe,EACf,MAAM,GACP,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC;YA4K9B,OAAO;CAgBtB"}
|
|
@@ -49,6 +49,75 @@ exports.QwenPromptAdapter = void 0;
|
|
|
49
49
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
50
50
|
const fs = __importStar(require("fs"));
|
|
51
51
|
const path = __importStar(require("path"));
|
|
52
|
+
const IMAGE_MIME_TYPES = {
|
|
53
|
+
".jpg": "image/jpeg",
|
|
54
|
+
".jpeg": "image/jpeg",
|
|
55
|
+
".png": "image/png",
|
|
56
|
+
".gif": "image/gif",
|
|
57
|
+
".webp": "image/webp",
|
|
58
|
+
".pdf": "application/pdf",
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Extensions audio acceptées par les modèles Qwen-Omni, associées à la valeur
|
|
62
|
+
* attendue dans `input_audio.format`.
|
|
63
|
+
*/
|
|
64
|
+
const AUDIO_FORMATS = {
|
|
65
|
+
".mp3": "mp3",
|
|
66
|
+
".wav": "wav",
|
|
67
|
+
".aac": "aac",
|
|
68
|
+
".amr": "amr",
|
|
69
|
+
".3gp": "3gp",
|
|
70
|
+
".3gpp": "3gpp",
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Extensions audio refusées par Qwen : un fichier de ce type lève une erreur
|
|
74
|
+
* au lieu d'être envoyé comme une image.
|
|
75
|
+
*/
|
|
76
|
+
const UNSUPPORTED_AUDIO_EXTENSIONS = [".m4a", ".ogg", ".oga", ".opus", ".flac", ".wma", ".webm"];
|
|
77
|
+
/** Taille maximale d'un audio envoyé en base64, mesurée après encodage. */
|
|
78
|
+
const MAX_INLINE_AUDIO_BYTES = 10 * 1024 * 1024;
|
|
79
|
+
function base64Length(byteLength) {
|
|
80
|
+
return Math.ceil(byteLength / 3) * 4;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Regroupe par `index` les fragments de tool calls reçus en streaming : le
|
|
84
|
+
* premier fragment porte l'identifiant et le nom, les suivants complètent
|
|
85
|
+
* `arguments`.
|
|
86
|
+
*/
|
|
87
|
+
function accumulateToolCalls(deltaToolCalls, parts) {
|
|
88
|
+
deltaToolCalls.forEach((toolCall, position) => {
|
|
89
|
+
var _a, _b, _c;
|
|
90
|
+
const index = typeof toolCall.index === "number" ? toolCall.index : position;
|
|
91
|
+
const current = (_a = parts.get(index)) !== null && _a !== void 0 ? _a : { name: "", arguments: "" };
|
|
92
|
+
if (toolCall.id)
|
|
93
|
+
current.id = toolCall.id;
|
|
94
|
+
if ((_b = toolCall.function) === null || _b === void 0 ? void 0 : _b.name)
|
|
95
|
+
current.name = toolCall.function.name;
|
|
96
|
+
if (typeof ((_c = toolCall.function) === null || _c === void 0 ? void 0 : _c.arguments) === "string") {
|
|
97
|
+
current.arguments += toolCall.function.arguments;
|
|
98
|
+
}
|
|
99
|
+
parts.set(index, current);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function finalizeToolCalls(parts) {
|
|
103
|
+
return [...parts.entries()]
|
|
104
|
+
.sort(([a], [b]) => a - b)
|
|
105
|
+
.map(([, part]) => {
|
|
106
|
+
let parsed = {};
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(part.arguments || "{}");
|
|
109
|
+
}
|
|
110
|
+
catch (_a) {
|
|
111
|
+
// Arguments non parsables : le texte brut est renvoyé sous `_raw`.
|
|
112
|
+
parsed = { _raw: part.arguments };
|
|
113
|
+
}
|
|
114
|
+
return { id: part.id, name: part.name, arguments: parsed };
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** Indique si le modèle est un modèle Omni, qui requiert le paramètre `modalities`. */
|
|
118
|
+
function isOmniModel(model) {
|
|
119
|
+
return !!model && /omni/i.test(model);
|
|
120
|
+
}
|
|
52
121
|
class QwenPromptAdapter {
|
|
53
122
|
constructor(opts) {
|
|
54
123
|
var _a, _b;
|
|
@@ -59,7 +128,7 @@ class QwenPromptAdapter {
|
|
|
59
128
|
this.endpoint += "/chat/completions";
|
|
60
129
|
}
|
|
61
130
|
formatMessage(message) {
|
|
62
|
-
var _a;
|
|
131
|
+
var _a, _b;
|
|
63
132
|
// Assistant message with tool calls → OpenAI-compatible format
|
|
64
133
|
if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
|
|
65
134
|
return {
|
|
@@ -78,55 +147,57 @@ class QwenPromptAdapter {
|
|
|
78
147
|
}),
|
|
79
148
|
};
|
|
80
149
|
}
|
|
81
|
-
// Si le message contient des attachedFile, les convertir au format Qwen VL
|
|
150
|
+
// Si le message contient des attachedFile, les convertir au format Qwen VL / Omni
|
|
82
151
|
if (message.attachedFile) {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
content.push({
|
|
87
|
-
type: "text",
|
|
88
|
-
text: message.content
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
// Ajouter les fichiers attachés
|
|
152
|
+
const textPart = message.content && typeof message.content === 'string'
|
|
153
|
+
? { type: "text", text: message.content }
|
|
154
|
+
: null;
|
|
92
155
|
const files = Array.isArray(message.attachedFile) ? message.attachedFile : [message.attachedFile];
|
|
156
|
+
const fileParts = [];
|
|
93
157
|
for (const file of files) {
|
|
94
|
-
if (typeof file
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
mimeType = 'image/png';
|
|
108
|
-
}
|
|
109
|
-
else if (ext === '.gif') {
|
|
110
|
-
mimeType = 'image/gif';
|
|
111
|
-
}
|
|
112
|
-
else if (ext === '.webp') {
|
|
113
|
-
mimeType = 'image/webp';
|
|
114
|
-
}
|
|
115
|
-
else if (ext === '.pdf') {
|
|
116
|
-
mimeType = 'application/pdf';
|
|
117
|
-
}
|
|
118
|
-
content.push({
|
|
119
|
-
type: "image_url",
|
|
120
|
-
image_url: {
|
|
121
|
-
url: `data:${mimeType};base64,${base64Data}`
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
console.warn(`Failed to read attached file ${file}:`, error);
|
|
158
|
+
if (typeof file !== 'string')
|
|
159
|
+
continue;
|
|
160
|
+
const filePath = path.resolve(file);
|
|
161
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
162
|
+
const audioFormat = AUDIO_FORMATS[ext];
|
|
163
|
+
// Les erreurs de lecture et de taille d'un audio sont propagées.
|
|
164
|
+
if (UNSUPPORTED_AUDIO_EXTENSIONS.includes(ext)) {
|
|
165
|
+
throw new Error(`Audio format ${ext} is not supported by Qwen. Transcode ${file} to one of: ${Object.keys(AUDIO_FORMATS).join(", ")}.`);
|
|
166
|
+
}
|
|
167
|
+
if (audioFormat) {
|
|
168
|
+
const encodedSize = base64Length(fs.statSync(filePath).size);
|
|
169
|
+
if (encodedSize > MAX_INLINE_AUDIO_BYTES) {
|
|
170
|
+
throw new Error(`Audio file ${file} is ${(encodedSize / 1024 / 1024).toFixed(1)} MB once Base64-encoded, above the ${MAX_INLINE_AUDIO_BYTES / 1024 / 1024} MB limit of the Qwen API. Split it or lower the bitrate.`);
|
|
127
171
|
}
|
|
172
|
+
fileParts.push({
|
|
173
|
+
type: "input_audio",
|
|
174
|
+
input_audio: {
|
|
175
|
+
// DashScope attend le préfixe `data:;base64,`.
|
|
176
|
+
data: `data:;base64,${fs.readFileSync(filePath).toString('base64')}`,
|
|
177
|
+
format: audioFormat,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const base64Data = fs.readFileSync(filePath).toString('base64');
|
|
184
|
+
const mimeType = (_b = IMAGE_MIME_TYPES[ext]) !== null && _b !== void 0 ? _b : 'application/octet-stream';
|
|
185
|
+
fileParts.push({
|
|
186
|
+
type: "image_url",
|
|
187
|
+
image_url: {
|
|
188
|
+
url: `data:${mimeType};base64,${base64Data}`
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.warn(`Failed to read attached file ${file}:`, error);
|
|
128
194
|
}
|
|
129
195
|
}
|
|
196
|
+
// L'audio précède le texte ; les images le suivent.
|
|
197
|
+
const hasAudio = fileParts.some(part => part.type === "input_audio");
|
|
198
|
+
const content = hasAudio
|
|
199
|
+
? [...fileParts, ...(textPart ? [textPart] : [])]
|
|
200
|
+
: [...(textPart ? [textPart] : []), ...fileParts];
|
|
130
201
|
return {
|
|
131
202
|
role: message.role,
|
|
132
203
|
content: content
|
|
@@ -138,11 +209,8 @@ class QwenPromptAdapter {
|
|
|
138
209
|
sendToProvider(messages_1) {
|
|
139
210
|
return __awaiter(this, arguments, void 0, function* (messages, tools = [], toolChoice = "auto", attachedFile, model, config) {
|
|
140
211
|
var _a;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
messages,
|
|
144
|
-
stream: false,
|
|
145
|
-
};
|
|
212
|
+
const resolvedModel = model || this.opts.model;
|
|
213
|
+
const body = Object.assign({ model: resolvedModel, messages, stream: false }, (isOmniModel(resolvedModel) ? { modalities: ["text"] } : {}));
|
|
146
214
|
if (tools.length) {
|
|
147
215
|
body.tools = tools.map((fn) => ({ type: "function", function: fn }));
|
|
148
216
|
body.tool_choice =
|
|
@@ -171,11 +239,10 @@ class QwenPromptAdapter {
|
|
|
171
239
|
}
|
|
172
240
|
streamFromProvider(_a) {
|
|
173
241
|
return __awaiter(this, arguments, void 0, function* ({ messages, tools, toolChoice, model, onChunk, onFinalResponse, config, }) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
};
|
|
242
|
+
const resolvedModel = model || this.opts.model;
|
|
243
|
+
const body = Object.assign({ model: resolvedModel, messages, stream: true,
|
|
244
|
+
// Demande le décompte des tokens dans le dernier chunk du stream.
|
|
245
|
+
stream_options: { include_usage: true } }, (isOmniModel(resolvedModel) ? { modalities: ["text"] } : {}));
|
|
179
246
|
if (tools === null || tools === void 0 ? void 0 : tools.length) {
|
|
180
247
|
body.tools = tools.map((fn) => ({ type: "function", function: fn }));
|
|
181
248
|
body.tool_choice =
|
|
@@ -231,10 +298,14 @@ class QwenPromptAdapter {
|
|
|
231
298
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
232
299
|
}
|
|
233
300
|
let aggregatedContent = "";
|
|
234
|
-
const
|
|
301
|
+
const toolCallParts = new Map();
|
|
235
302
|
let buffer = "";
|
|
303
|
+
// Usage, motif d'arrêt et dernier chunk, restitués dans la réponse finale.
|
|
304
|
+
let streamUsage = null;
|
|
305
|
+
let finishReason = null;
|
|
306
|
+
let lastChunk = null;
|
|
236
307
|
response.body.on('data', (chunk) => {
|
|
237
|
-
var _a, _b;
|
|
308
|
+
var _a, _b, _c, _d;
|
|
238
309
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted)
|
|
239
310
|
return;
|
|
240
311
|
// Convertir le buffer en string
|
|
@@ -251,20 +322,17 @@ class QwenPromptAdapter {
|
|
|
251
322
|
try {
|
|
252
323
|
const parsed = JSON.parse(data);
|
|
253
324
|
const delta = (_b = (_a = parsed.choices) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.delta;
|
|
325
|
+
lastChunk = parsed;
|
|
326
|
+
if (parsed.usage)
|
|
327
|
+
streamUsage = parsed.usage;
|
|
328
|
+
if ((_d = (_c = parsed.choices) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.finish_reason)
|
|
329
|
+
finishReason = parsed.choices[0].finish_reason;
|
|
254
330
|
if (delta === null || delta === void 0 ? void 0 : delta.content) {
|
|
255
331
|
aggregatedContent += delta.content;
|
|
256
332
|
onChunk === null || onChunk === void 0 ? void 0 : onChunk({ contentChunk: delta.content, rawChunk: parsed });
|
|
257
333
|
}
|
|
258
334
|
if (delta === null || delta === void 0 ? void 0 : delta.tool_calls) {
|
|
259
|
-
|
|
260
|
-
if (toolCall.function) {
|
|
261
|
-
aggregatedToolCalls.push({
|
|
262
|
-
id: toolCall.id,
|
|
263
|
-
name: toolCall.function.name,
|
|
264
|
-
arguments: JSON.parse(toolCall.function.arguments || "{}"),
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
335
|
+
accumulateToolCalls(delta.tool_calls, toolCallParts);
|
|
268
336
|
}
|
|
269
337
|
}
|
|
270
338
|
catch (e) {
|
|
@@ -293,15 +361,7 @@ class QwenPromptAdapter {
|
|
|
293
361
|
onChunk === null || onChunk === void 0 ? void 0 : onChunk({ contentChunk: delta.content, rawChunk: parsed });
|
|
294
362
|
}
|
|
295
363
|
if (delta === null || delta === void 0 ? void 0 : delta.tool_calls) {
|
|
296
|
-
|
|
297
|
-
if (toolCall.function) {
|
|
298
|
-
aggregatedToolCalls.push({
|
|
299
|
-
id: toolCall.id,
|
|
300
|
-
name: toolCall.function.name,
|
|
301
|
-
arguments: JSON.parse(toolCall.function.arguments || "{}"),
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
364
|
+
accumulateToolCalls(delta.tool_calls, toolCallParts);
|
|
305
365
|
}
|
|
306
366
|
}
|
|
307
367
|
catch (e) {
|
|
@@ -311,11 +371,8 @@ class QwenPromptAdapter {
|
|
|
311
371
|
}
|
|
312
372
|
}
|
|
313
373
|
}
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
toolCalls: aggregatedToolCalls.length > 0 ? aggregatedToolCalls : undefined,
|
|
317
|
-
raw: null,
|
|
318
|
-
};
|
|
374
|
+
const aggregatedToolCalls = finalizeToolCalls(toolCallParts);
|
|
375
|
+
const finalResponse = Object.assign({ content: aggregatedContent, toolCalls: aggregatedToolCalls.length > 0 ? aggregatedToolCalls : undefined, raw: lastChunk ? Object.assign(Object.assign({}, lastChunk), { usage: streamUsage }) : null }, (finishReason ? { finishReason } : {}));
|
|
319
376
|
onFinalResponse === null || onFinalResponse === void 0 ? void 0 : onFinalResponse(finalResponse);
|
|
320
377
|
finish(() => resolve(finalResponse));
|
|
321
378
|
});
|
package/dist/core/Models.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Models.d.ts","sourceRoot":"","sources":["../../src/core/Models.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAExC,eAAO,MAAM,MAAM;sBAWZ,KAAK;oBAYL,KAAK;iBAYL,KAAK;WAWL,KAAK;eAWL,KAAK;mBAWL,KAAK;qBAWL,KAAK;kBAWL,KAAK;oBAWL,KAAK;oBAaL,KAAK;sBAWL,KAAK;uBAKL,KAAK;qBAKL,KAAK;CACX,CAAC"}
|
|
1
|
+
{"version":3,"file":"Models.d.ts","sourceRoot":"","sources":["../../src/core/Models.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAExC,eAAO,MAAM,MAAM;sBAWZ,KAAK;oBAYL,KAAK;iBAYL,KAAK;WAWL,KAAK;eAWL,KAAK;mBAWL,KAAK;qBAWL,KAAK;qBAaL,KAAK;kBAWL,KAAK;oBAWL,KAAK;oBAaL,KAAK;sBAWL,KAAK;uBAKL,KAAK;qBAKL,KAAK;CACX,CAAC"}
|
package/dist/core/Models.js
CHANGED
|
@@ -75,6 +75,18 @@ exports.MODELS = {
|
|
|
75
75
|
},
|
|
76
76
|
currency: "USD",
|
|
77
77
|
},
|
|
78
|
+
// Omnimodal (audio/vidéo/image/texte), tarifs du déploiement international (Singapour).
|
|
79
|
+
QWEN_OMNI_FLASH: {
|
|
80
|
+
provider: "qwen",
|
|
81
|
+
modelName: "qwen3.8-omni-flash",
|
|
82
|
+
costsPer1MTokens: {
|
|
83
|
+
prompt: 0.15,
|
|
84
|
+
prompt_cache: 0.016,
|
|
85
|
+
prompt_audio: 0.15,
|
|
86
|
+
output: 0.47,
|
|
87
|
+
},
|
|
88
|
+
currency: "USD",
|
|
89
|
+
},
|
|
78
90
|
GEMINI_3_PRO: {
|
|
79
91
|
provider: "gemini",
|
|
80
92
|
modelName: "gemini-3-pro-preview",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qwen-omni-audio-example.d.ts","sourceRoot":"","sources":["../../src/examples/qwen-omni-audio-example.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
const UnifiedLLMClient_1 = require("../core/UnifiedLLMClient");
|
|
46
|
+
const Models_1 = require("../core/Models");
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
/**
|
|
51
|
+
* Test d'intégration Qwen-Omni : transcription audio horodatée.
|
|
52
|
+
*
|
|
53
|
+
* Vérifie que la sortie est un JSON exploitable, aux timecodes croissants et
|
|
54
|
+
* compris dans la durée du fichier.
|
|
55
|
+
*
|
|
56
|
+
* ALIBABA_CLOUD_API_KEY=... npm run example:qwen-omni -- ./chemin/audio.mp3 [--tools]
|
|
57
|
+
* npm run example:qwen-omni -- ./reponse.txt [duree_en_secondes]
|
|
58
|
+
*/
|
|
59
|
+
const TRANSCRIPTION_PROMPT = `Transcris cet extrait audio intégralement, en français.
|
|
60
|
+
Réponds UNIQUEMENT avec un tableau JSON, sans texte autour et sans bloc de code.
|
|
61
|
+
Chaque élément représente une phrase et suit ce format exact :
|
|
62
|
+
{"timecode": "MM:SS", "speaker": "nom ou Inconnu", "text": "la phrase transcrite"}
|
|
63
|
+
Le timecode est l'instant de DÉBUT de la phrase, relatif au début de l'extrait.`;
|
|
64
|
+
/** Prompt du mode function calling : le format est défini par le schéma de l'outil. */
|
|
65
|
+
const TRANSCRIPTION_PROMPT_TOOLS = `Transcris cet extrait audio intégralement, en français,
|
|
66
|
+
puis transmets le résultat via l'outil submit_transcription.`;
|
|
67
|
+
const TRANSCRIPTION_TOOL = {
|
|
68
|
+
name: "submit_transcription",
|
|
69
|
+
description: "Enregistre la transcription horodatée d'un extrait audio.",
|
|
70
|
+
parameters: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
segments: {
|
|
74
|
+
type: "array",
|
|
75
|
+
description: "Une entrée par phrase, dans l'ordre chronologique.",
|
|
76
|
+
items: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
timecode: { type: "string", description: "Début de la phrase, au format MM:SS." },
|
|
80
|
+
speaker: { type: "string", description: "Nom du locuteur, ou 'Inconnu'." },
|
|
81
|
+
text: { type: "string", description: "La phrase transcrite." },
|
|
82
|
+
},
|
|
83
|
+
required: ["timecode", "speaker", "text"],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
required: ["segments"],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
/** Durée réelle du fichier, via ffprobe. Renvoie null si ffprobe est absent. */
|
|
91
|
+
function probeDurationSeconds(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
const out = (0, child_process_1.execFileSync)("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { encoding: "utf-8" });
|
|
94
|
+
const seconds = parseFloat(out.trim());
|
|
95
|
+
return Number.isFinite(seconds) ? seconds : null;
|
|
96
|
+
}
|
|
97
|
+
catch (_a) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** "MM:SS" ou "HH:MM:SS" → secondes. Renvoie NaN si le format est invalide. */
|
|
102
|
+
function timecodeToSeconds(timecode) {
|
|
103
|
+
if (typeof timecode !== "string")
|
|
104
|
+
return NaN;
|
|
105
|
+
const parts = timecode.trim().split(":").map(Number);
|
|
106
|
+
if (parts.some(n => !Number.isFinite(n)))
|
|
107
|
+
return NaN;
|
|
108
|
+
if (parts.length === 2)
|
|
109
|
+
return parts[0] * 60 + parts[1];
|
|
110
|
+
if (parts.length === 3)
|
|
111
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
112
|
+
return NaN;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Extrait les segments horodatés d'une réponse, qu'elle soit formée d'objets
|
|
116
|
+
* concaténés, d'un tableau ou d'un tableau enveloppé dans un objet,
|
|
117
|
+
* éventuellement dans un bloc de code.
|
|
118
|
+
*/
|
|
119
|
+
function extractJsonObjects(raw) {
|
|
120
|
+
const cleaned = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/```$/, "");
|
|
121
|
+
const objects = [];
|
|
122
|
+
let depth = 0;
|
|
123
|
+
let start = -1;
|
|
124
|
+
let inString = false;
|
|
125
|
+
let escaped = false;
|
|
126
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
127
|
+
const char = cleaned[i];
|
|
128
|
+
if (inString) {
|
|
129
|
+
if (escaped)
|
|
130
|
+
escaped = false;
|
|
131
|
+
else if (char === "\\")
|
|
132
|
+
escaped = true;
|
|
133
|
+
else if (char === '"')
|
|
134
|
+
inString = false;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (char === '"')
|
|
138
|
+
inString = true;
|
|
139
|
+
else if (char === "{") {
|
|
140
|
+
if (depth === 0)
|
|
141
|
+
start = i;
|
|
142
|
+
depth++;
|
|
143
|
+
}
|
|
144
|
+
else if (char === "}") {
|
|
145
|
+
depth--;
|
|
146
|
+
if (depth === 0 && start !== -1) {
|
|
147
|
+
try {
|
|
148
|
+
objects.push(JSON.parse(cleaned.slice(start, i + 1)));
|
|
149
|
+
}
|
|
150
|
+
catch (_a) {
|
|
151
|
+
// Objet non parsable : ignoré.
|
|
152
|
+
}
|
|
153
|
+
start = -1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!objects.length)
|
|
158
|
+
throw new Error("Aucun objet JSON trouvé dans la réponse.");
|
|
159
|
+
// Collecte tout objet portant un `timecode`, quelle que soit sa profondeur.
|
|
160
|
+
const segments = [];
|
|
161
|
+
const visit = (node) => {
|
|
162
|
+
if (Array.isArray(node))
|
|
163
|
+
return node.forEach(visit);
|
|
164
|
+
if (!node || typeof node !== "object")
|
|
165
|
+
return;
|
|
166
|
+
if ("timecode" in node)
|
|
167
|
+
return void segments.push(node);
|
|
168
|
+
Object.values(node).forEach(visit);
|
|
169
|
+
};
|
|
170
|
+
objects.forEach(visit);
|
|
171
|
+
if (!segments.length) {
|
|
172
|
+
throw new Error("Objets JSON trouvés, mais aucun ne porte de champ `timecode`.");
|
|
173
|
+
}
|
|
174
|
+
return segments;
|
|
175
|
+
}
|
|
176
|
+
function runTimecodeChecks(entries, durationSeconds) {
|
|
177
|
+
const checks = [];
|
|
178
|
+
checks.push({
|
|
179
|
+
label: "Transcription non vide",
|
|
180
|
+
ok: entries.length > 0,
|
|
181
|
+
detail: `${entries.length} segment(s)`,
|
|
182
|
+
});
|
|
183
|
+
const malformed = entries.filter(e => !e || typeof e.text !== "string" || !e.text.trim() || !Number.isFinite(timecodeToSeconds(e.timecode)));
|
|
184
|
+
checks.push({
|
|
185
|
+
label: "Tous les segments ont un timecode et un texte valides",
|
|
186
|
+
ok: malformed.length === 0,
|
|
187
|
+
detail: malformed.length ? `${malformed.length} segment(s) invalide(s)` : "aucun défaut",
|
|
188
|
+
});
|
|
189
|
+
const seconds = entries.map(e => timecodeToSeconds(e === null || e === void 0 ? void 0 : e.timecode)).filter(Number.isFinite);
|
|
190
|
+
const regressions = [];
|
|
191
|
+
for (let i = 1; i < seconds.length; i++) {
|
|
192
|
+
if (seconds[i] < seconds[i - 1]) {
|
|
193
|
+
regressions.push(`#${i}: ${entries[i].timecode} après ${entries[i - 1].timecode}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
checks.push({
|
|
197
|
+
label: "Timecodes strictement croissants",
|
|
198
|
+
ok: regressions.length === 0,
|
|
199
|
+
detail: regressions.length ? regressions.slice(0, 5).join(", ") : "ordre respecté",
|
|
200
|
+
});
|
|
201
|
+
const last = seconds.length ? seconds[seconds.length - 1] : 0;
|
|
202
|
+
if (durationSeconds !== null) {
|
|
203
|
+
checks.push({
|
|
204
|
+
label: "Dernier timecode dans la durée du fichier",
|
|
205
|
+
ok: last <= durationSeconds + 1,
|
|
206
|
+
detail: `${last.toFixed(0)}s vs ${durationSeconds.toFixed(0)}s de piste`,
|
|
207
|
+
});
|
|
208
|
+
const coverage = durationSeconds > 0 ? last / durationSeconds : 0;
|
|
209
|
+
checks.push({
|
|
210
|
+
label: "Couverture de la piste ≥ 80 %",
|
|
211
|
+
ok: coverage >= 0.8,
|
|
212
|
+
detail: `${(coverage * 100).toFixed(0)} % couverts`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
console.log("ℹ️ ffprobe introuvable : contrôles de durée ignorés.");
|
|
217
|
+
}
|
|
218
|
+
return checks;
|
|
219
|
+
}
|
|
220
|
+
function reportCost(raw) {
|
|
221
|
+
var _a, _b;
|
|
222
|
+
const usage = raw === null || raw === void 0 ? void 0 : raw.usage;
|
|
223
|
+
if (!usage)
|
|
224
|
+
return;
|
|
225
|
+
const { prompt, output } = Models_1.MODELS.QWEN_OMNI_FLASH.costsPer1MTokens;
|
|
226
|
+
const inputTokens = (_a = usage.prompt_tokens) !== null && _a !== void 0 ? _a : 0;
|
|
227
|
+
const outputTokens = (_b = usage.completion_tokens) !== null && _b !== void 0 ? _b : 0;
|
|
228
|
+
const cost = (inputTokens * prompt + outputTokens * output) / 1000000;
|
|
229
|
+
console.log(`\n💰 ${inputTokens} tokens entrée / ${outputTokens} sortie → ${cost.toFixed(5)} USD`);
|
|
230
|
+
}
|
|
231
|
+
/** Rejoue les contrôles sur une réponse déjà obtenue, sans rappeler l'API. */
|
|
232
|
+
function replaySavedResponse(rawPath, durationSeconds) {
|
|
233
|
+
const content = fs.readFileSync(rawPath, "utf-8");
|
|
234
|
+
console.log(`♻️ Rejeu hors ligne de ${path.basename(rawPath)} (${content.length} caractères)\n`);
|
|
235
|
+
const entries = extractJsonObjects(content);
|
|
236
|
+
const checks = runTimecodeChecks(entries, durationSeconds);
|
|
237
|
+
checks.forEach(c => console.log(` ${c.ok ? "✅" : "❌"} ${c.label} (${c.detail})`));
|
|
238
|
+
if (checks.some(c => !c.ok))
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
function main() {
|
|
242
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
243
|
+
var _a, _b, _c, _d, _e, _f;
|
|
244
|
+
const audioPath = process.argv[2];
|
|
245
|
+
if (!audioPath) {
|
|
246
|
+
console.error("Usage: npm run example:qwen-omni -- <chemin/audio.mp3>");
|
|
247
|
+
console.error(" npm run example:qwen-omni -- <reponse.txt> [duree_en_secondes]");
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
// Un fichier .txt est une réponse sauvegardée : les contrôles sont rejoués sans appel à l'API.
|
|
251
|
+
if (audioPath.toLowerCase().endsWith(".txt")) {
|
|
252
|
+
const duration = process.argv[3] ? Number(process.argv[3]) : null;
|
|
253
|
+
replaySavedResponse(audioPath, Number.isFinite(duration) ? duration : null);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!fs.existsSync(audioPath)) {
|
|
257
|
+
console.error(`Fichier introuvable : ${audioPath}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const sizeMb = fs.statSync(audioPath).size / 1024 / 1024;
|
|
261
|
+
const duration = probeDurationSeconds(audioPath);
|
|
262
|
+
console.log(`🎧 ${path.basename(audioPath)} — ${sizeMb.toFixed(1)} Mo` +
|
|
263
|
+
(duration !== null ? `, ${duration.toFixed(0)}s` : ""));
|
|
264
|
+
console.log(`🤖 ${Models_1.MODELS.QWEN_OMNI_FLASH.modelName}\n`);
|
|
265
|
+
const client = new UnifiedLLMClient_1.UnifiedLLMClient(Models_1.MODELS.QWEN_OMNI_FLASH);
|
|
266
|
+
const startedAt = Date.now();
|
|
267
|
+
const useTools = process.argv.includes("--tools");
|
|
268
|
+
console.log(useTools ? "🔧 Mode function calling\n" : "💬 Mode prompt JSON\n");
|
|
269
|
+
const result = yield client.chat(Object.assign(Object.assign({ message: [
|
|
270
|
+
{
|
|
271
|
+
role: "user",
|
|
272
|
+
content: useTools ? TRANSCRIPTION_PROMPT_TOOLS : TRANSCRIPTION_PROMPT,
|
|
273
|
+
attachedFile: audioPath,
|
|
274
|
+
},
|
|
275
|
+
], stream: true }, (useTools ? { tools: [TRANSCRIPTION_TOOL], toolChoice: TRANSCRIPTION_TOOL.name } : {})), { onChunk: chunk => {
|
|
276
|
+
if (chunk.contentChunk)
|
|
277
|
+
process.stdout.write(".");
|
|
278
|
+
} }));
|
|
279
|
+
const latencyMs = Date.now() - startedAt;
|
|
280
|
+
console.log(`\n\n⏱️ Réponse en ${(latencyMs / 1000).toFixed(1)}s`);
|
|
281
|
+
// En function calling, la transcription arrive dans les arguments de l'outil,
|
|
282
|
+
// pas dans le contenu du message.
|
|
283
|
+
const toolArguments = (_b = (_a = result.toolCalls) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.arguments;
|
|
284
|
+
const content = useTools && toolArguments
|
|
285
|
+
? JSON.stringify(toolArguments, null, 2)
|
|
286
|
+
: typeof result.content === "string"
|
|
287
|
+
? result.content
|
|
288
|
+
: JSON.stringify(result.content);
|
|
289
|
+
if (useTools) {
|
|
290
|
+
console.log(`🔧 tool_calls: ${(_d = (_c = result.toolCalls) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0}` +
|
|
291
|
+
(((_e = result.toolCalls) === null || _e === void 0 ? void 0 : _e[0]) ? ` → ${result.toolCalls[0].name}` : ""));
|
|
292
|
+
}
|
|
293
|
+
// La réponse brute est sauvegardée à côté du fichier audio.
|
|
294
|
+
const rawPath = path.join(path.dirname(audioPath), `${path.basename(audioPath)}.qwen-raw.txt`);
|
|
295
|
+
fs.writeFileSync(rawPath, content, "utf-8");
|
|
296
|
+
console.log(`📄 Réponse brute : ${rawPath}`);
|
|
297
|
+
console.log(`🏁 finish_reason: ${(_f = result.finishReason) !== null && _f !== void 0 ? _f : "?"} | ${content.length} caractères reçus`);
|
|
298
|
+
reportCost(result.raw);
|
|
299
|
+
let entries;
|
|
300
|
+
try {
|
|
301
|
+
entries = extractJsonObjects(content);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
console.error(`\n❌ Sortie non structurée : ${error instanceof Error ? error.message : error}`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
console.log("\n📋 Premiers segments :");
|
|
308
|
+
entries.slice(0, 5).forEach(e => { var _a; return console.log(` [${e.timecode}] ${(_a = e.speaker) !== null && _a !== void 0 ? _a : "?"} — ${e.text}`); });
|
|
309
|
+
const checks = runTimecodeChecks(entries, duration);
|
|
310
|
+
console.log("\n🔍 Contrôles :");
|
|
311
|
+
checks.forEach(c => console.log(` ${c.ok ? "✅" : "❌"} ${c.label} (${c.detail})`));
|
|
312
|
+
reportCost(result.raw);
|
|
313
|
+
const failed = checks.filter(c => !c.ok);
|
|
314
|
+
if (failed.length) {
|
|
315
|
+
console.error(`\n❌ ${failed.length} contrôle(s) en échec.`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
console.log("\n✅ Tous les contrôles passent.");
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
main().catch(error => {
|
|
322
|
+
console.error("\n💥 Échec :", error instanceof Error ? error.message : error);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/unified-llm-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A unified interface for multiple LLM providers (OpenAI, Gemini, Anthropic, Qwen) with tool-calling, streaming, and audio support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"example": "node dist/examples/example.js",
|
|
20
20
|
"example:audio": "node dist/examples/audio-example.js",
|
|
21
21
|
"example:qwen": "node dist/examples/qwen-vl-example.js",
|
|
22
|
+
"example:qwen-omni": "node dist/examples/qwen-omni-audio-example.js",
|
|
22
23
|
"example:anthropic": "node dist/examples/anthropic-example.js",
|
|
23
24
|
"example:abort": "node dist/examples/chat-abort-example.js",
|
|
24
25
|
"example:proxy": "node dist/examples/proxy-example.js",
|