@zhivex-ai/qwen 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -44
- package/dist/index.d.ts +22 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +761 -301
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { toJSONSchema } from "zod";
|
|
2
|
-
import { CallbackRealtimeSession, ConfigurationError, decodeBase64WithLimit, ProviderHTTPError, readBodyWithLimit, readErrorBodyWithLimit, readJsonWithLimit, resolveAudioResponseLimits, UnsupportedFeatureError, createProviderAdapter, hostedTool, openWebSocketConnection, isCallableToolDefinition, normalizeFinishReason, providerDataPart, streamSSE, withRetry, withTimeoutSignal } from "@zhivex-ai/core";
|
|
2
|
+
import { CallbackRealtimeSession, ConfigurationError, decodeBase64WithLimit, ProviderHTTPError, readBodyWithLimit, readErrorBodyWithLimit, readJsonWithLimit, resolveAudioResponseLimits, UnsupportedFeatureError, createProviderAdapter, hostedTool, openWebSocketConnection, isCallableToolDefinition, normalizeFinishReason, providerDataPart, streamSSE, toToolSet, withRetry, withTimeoutSignal } from "@zhivex-ai/core";
|
|
3
3
|
const capabilities = {
|
|
4
4
|
streaming: true,
|
|
5
5
|
tools: true,
|
|
6
6
|
structuredOutput: true,
|
|
7
7
|
jsonMode: true,
|
|
8
8
|
toolChoice: true,
|
|
9
|
-
parallelToolCalls:
|
|
9
|
+
parallelToolCalls: false,
|
|
10
10
|
vision: true,
|
|
11
11
|
files: false,
|
|
12
12
|
audioInput: false,
|
|
@@ -57,6 +57,24 @@ const qwenTaskBaseURLFrom = (baseURL) => {
|
|
|
57
57
|
const url = new URL(baseURL);
|
|
58
58
|
return `${url.protocol}//${url.host}/api/v1`;
|
|
59
59
|
};
|
|
60
|
+
const QWEN_REGION_HOSTS = {
|
|
61
|
+
singapore: "ap-southeast-1.maas.aliyuncs.com",
|
|
62
|
+
beijing: "cn-beijing.maas.aliyuncs.com",
|
|
63
|
+
"hong-kong": "cn-hongkong.maas.aliyuncs.com",
|
|
64
|
+
tokyo: "ap-northeast-1.maas.aliyuncs.com",
|
|
65
|
+
frankfurt: "eu-central-1.maas.aliyuncs.com",
|
|
66
|
+
virginia: "dashscope-us.aliyuncs.com"
|
|
67
|
+
};
|
|
68
|
+
const qwenWorkspaceURLs = (workspaceId, region) => {
|
|
69
|
+
const origin = region === "virginia"
|
|
70
|
+
? `https://${QWEN_REGION_HOSTS[region]}`
|
|
71
|
+
: `https://${workspaceId}.${QWEN_REGION_HOSTS[region]}`;
|
|
72
|
+
return {
|
|
73
|
+
baseURL: `${origin}/compatible-mode/v1`,
|
|
74
|
+
taskBaseURL: `${origin}/api/v1`,
|
|
75
|
+
realtimeURL: `${origin.replace(/^https:/, "wss:")}/api-ws/v1/realtime`
|
|
76
|
+
};
|
|
77
|
+
};
|
|
60
78
|
const toUint8Array = async (data) => {
|
|
61
79
|
if (data instanceof Uint8Array) {
|
|
62
80
|
return data;
|
|
@@ -70,6 +88,24 @@ const toUint8Array = async (data) => {
|
|
|
70
88
|
return Uint8Array.from(Buffer.from(String(data), "base64"));
|
|
71
89
|
};
|
|
72
90
|
const toBase64 = async (data) => typeof data === "string" ? data : Buffer.from(await toUint8Array(data)).toString("base64");
|
|
91
|
+
const toDataURL = (data, mediaType) => {
|
|
92
|
+
if (typeof data === "string" && (/^data:/i.test(data) || /^https?:\/\//i.test(data))) {
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
const encoded = typeof data === "string"
|
|
96
|
+
? data
|
|
97
|
+
: Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data)).toString("base64");
|
|
98
|
+
return `data:${mediaType};base64,${encoded}`;
|
|
99
|
+
};
|
|
100
|
+
const mediaValue = (input) => {
|
|
101
|
+
if (input.uri) {
|
|
102
|
+
return input.uri;
|
|
103
|
+
}
|
|
104
|
+
if (input.data === undefined) {
|
|
105
|
+
throw new ConfigurationError("Qwen media input requires either uri or data.");
|
|
106
|
+
}
|
|
107
|
+
return toDataURL(input.data, input.mediaType);
|
|
108
|
+
};
|
|
73
109
|
const createFile = async (data, mediaType, filename) => {
|
|
74
110
|
const bytes = await toUint8Array(data);
|
|
75
111
|
return new File([bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)], filename, { type: mediaType });
|
|
@@ -83,6 +119,50 @@ const appendQuery = (url, query) => {
|
|
|
83
119
|
}
|
|
84
120
|
return parsed.toString();
|
|
85
121
|
};
|
|
122
|
+
const encodeQwenResourceId = (value, resource) => {
|
|
123
|
+
if (!value || value === "." || value === ".." || /[\\/?#\s]/.test(value)) {
|
|
124
|
+
throw new ConfigurationError(`Qwen ${resource} ID must be a non-empty opaque identifier without path separators.`);
|
|
125
|
+
}
|
|
126
|
+
return encodeURIComponent(value);
|
|
127
|
+
};
|
|
128
|
+
const isQwenOmniLanguageModel = (modelId) => /^qwen3(?:\.5)?-omni-(?:plus|flash)(?:$|-)/i.test(modelId);
|
|
129
|
+
const isOfficialQwenSpeechAudioURL = (url) => !url.username &&
|
|
130
|
+
!url.password &&
|
|
131
|
+
/(?:^|\.)oss(?:-[a-z0-9-]+)?\.aliyuncs\.com$/i.test(url.hostname);
|
|
132
|
+
const parseQwenSpeechAudioURL = async (value, validator) => {
|
|
133
|
+
let url;
|
|
134
|
+
try {
|
|
135
|
+
url = new URL(value);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new ProviderHTTPError("Qwen speech response contained an invalid audio download URL.", 502);
|
|
139
|
+
}
|
|
140
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
141
|
+
throw new ProviderHTTPError("Qwen speech audio downloads require an HTTP(S) URL.", 502);
|
|
142
|
+
}
|
|
143
|
+
const allowed = validator ? await validator(url) : isOfficialQwenSpeechAudioURL(url);
|
|
144
|
+
if (!allowed) {
|
|
145
|
+
throw new ProviderHTTPError("Qwen speech response contained an audio download URL rejected by the configured safety policy.", 502);
|
|
146
|
+
}
|
|
147
|
+
return url;
|
|
148
|
+
};
|
|
149
|
+
const downloadQwenSpeechAudio = async (initialURL, fetcher, signal, validator, maxRedirects) => {
|
|
150
|
+
let url = await parseQwenSpeechAudioURL(initialURL, validator);
|
|
151
|
+
for (let redirects = 0;; redirects += 1) {
|
|
152
|
+
const response = await fetcher(url, { method: "GET", redirect: "manual", signal });
|
|
153
|
+
if (![301, 302, 303, 307, 308].includes(response.status)) {
|
|
154
|
+
return response;
|
|
155
|
+
}
|
|
156
|
+
if (redirects >= maxRedirects) {
|
|
157
|
+
throw new ProviderHTTPError(`Qwen speech audio download exceeded ${maxRedirects} redirects.`, 502);
|
|
158
|
+
}
|
|
159
|
+
const location = response.headers.get("location");
|
|
160
|
+
if (!location) {
|
|
161
|
+
throw new ProviderHTTPError("Qwen speech audio redirect did not include a Location header.", 502);
|
|
162
|
+
}
|
|
163
|
+
url = await parseQwenSpeechAudioURL(new URL(location, url).toString(), validator);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
86
166
|
const providerHeaders = (apiKey, providerOptions) => {
|
|
87
167
|
const headers = jsonHeaders(apiKey);
|
|
88
168
|
const sessionCache = providerOptions?.["x-dashscope-session-cache"];
|
|
@@ -96,26 +176,69 @@ const stripHeaderOptions = (providerOptions) => {
|
|
|
96
176
|
delete next["x-dashscope-session-cache"];
|
|
97
177
|
return next;
|
|
98
178
|
};
|
|
179
|
+
const stripResponsesRequestOptions = (providerOptions) => {
|
|
180
|
+
const next = stripHeaderOptions(providerOptions);
|
|
181
|
+
for (const key of [
|
|
182
|
+
"model",
|
|
183
|
+
"input",
|
|
184
|
+
"stream",
|
|
185
|
+
"tools",
|
|
186
|
+
"tool_choice",
|
|
187
|
+
"temperature",
|
|
188
|
+
"reasoning",
|
|
189
|
+
"previous_response_id",
|
|
190
|
+
"max_output_tokens",
|
|
191
|
+
"response_format"
|
|
192
|
+
]) {
|
|
193
|
+
delete next[key];
|
|
194
|
+
}
|
|
195
|
+
return next;
|
|
196
|
+
};
|
|
99
197
|
const modelFamily = (modelId) => modelId.toLowerCase();
|
|
100
|
-
const supportsQwenReasoning = (modelId) =>
|
|
198
|
+
const supportsQwenReasoning = (modelId) => !isQwenOmniLanguageModel(modelId) &&
|
|
199
|
+
supportsQwenTools(modelId) &&
|
|
200
|
+
/^(qwen-(plus|turbo|max|flash)|qwq|qwen3|qwen3\.)/i.test(modelId);
|
|
101
201
|
const supportsQwenVision = (modelId) => {
|
|
102
202
|
const model = modelFamily(modelId);
|
|
103
203
|
return (model.includes("vl") ||
|
|
104
204
|
model.includes("omni") ||
|
|
205
|
+
model.includes("ocr") ||
|
|
105
206
|
model.includes("vision") ||
|
|
106
|
-
/^qwen3\.7-plus(?:$|-)/.test(model)
|
|
207
|
+
/^qwen3\.7-plus(?:$|-)/.test(model) ||
|
|
208
|
+
/^qwen3\.6-flash(?:$|-)/.test(model));
|
|
209
|
+
};
|
|
210
|
+
const supportsQwenTools = (modelId) => !/(embedding|rerank|asr|tts|image|realtime|ocr|translation|character|long-context)/.test(modelFamily(modelId));
|
|
211
|
+
const supportsQwenFiles = (modelId) => /^qwen3\.5-ocr(?:$|-)/.test(modelFamily(modelId));
|
|
212
|
+
const supportsQwenAudioInput = (modelId) => /(omni|audio|asr)/.test(modelFamily(modelId));
|
|
213
|
+
const qwenLanguageCapabilities = (modelId) => {
|
|
214
|
+
const tools = supportsQwenTools(modelId);
|
|
215
|
+
const omni = isQwenOmniLanguageModel(modelId);
|
|
216
|
+
const reasoning = supportsQwenReasoning(modelId);
|
|
217
|
+
return {
|
|
218
|
+
...capabilities,
|
|
219
|
+
vision: supportsQwenVision(modelId),
|
|
220
|
+
tools,
|
|
221
|
+
structuredOutput: tools && !omni,
|
|
222
|
+
jsonMode: tools && !omni,
|
|
223
|
+
toolChoice: tools,
|
|
224
|
+
parallelToolCalls: false,
|
|
225
|
+
webSearch: tools && !omni,
|
|
226
|
+
files: supportsQwenFiles(modelId),
|
|
227
|
+
audioInput: supportsQwenAudioInput(modelId),
|
|
228
|
+
reasoning,
|
|
229
|
+
reasoningEfforts: reasoning ? ["none", "minimal", "low", "medium", "high"] : undefined,
|
|
230
|
+
agentCapabilities: {
|
|
231
|
+
...capabilities.agentCapabilities,
|
|
232
|
+
supportTier: tools && !omni ? "tier-b" : "tier-c",
|
|
233
|
+
toolChoiceNone: tools,
|
|
234
|
+
hostedWebSearch: tools && !omni,
|
|
235
|
+
hostedFileSearch: tools && !omni,
|
|
236
|
+
remoteMcp: tools && !omni,
|
|
237
|
+
codeExecution: tools && !omni,
|
|
238
|
+
webExtraction: tools && !omni
|
|
239
|
+
}
|
|
240
|
+
};
|
|
107
241
|
};
|
|
108
|
-
const supportsQwenTools = (modelId) => !modelFamily(modelId).includes("embedding");
|
|
109
|
-
const qwenLanguageCapabilities = (modelId) => ({
|
|
110
|
-
...capabilities,
|
|
111
|
-
vision: supportsQwenVision(modelId),
|
|
112
|
-
tools: supportsQwenTools(modelId),
|
|
113
|
-
structuredOutput: supportsQwenTools(modelId),
|
|
114
|
-
jsonMode: supportsQwenTools(modelId),
|
|
115
|
-
toolChoice: supportsQwenTools(modelId),
|
|
116
|
-
parallelToolCalls: supportsQwenTools(modelId),
|
|
117
|
-
reasoning: supportsQwenReasoning(modelId)
|
|
118
|
-
});
|
|
119
242
|
const transcriptionCapabilities = {
|
|
120
243
|
...embeddingCapabilities,
|
|
121
244
|
audioInput: true
|
|
@@ -176,23 +299,25 @@ const parseJson = async (response, options = {}) => {
|
|
|
176
299
|
: response.json();
|
|
177
300
|
};
|
|
178
301
|
const mapContentParts = (message) => {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
302
|
+
const hasMedia = message.parts.some((part) => part.type === "image" || part.type === "audio");
|
|
303
|
+
if (!hasMedia) {
|
|
304
|
+
return message.parts
|
|
305
|
+
.filter((part) => part.type === "text")
|
|
306
|
+
.map((part) => part.text)
|
|
307
|
+
.join("");
|
|
183
308
|
}
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
type: "text",
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
309
|
+
return message.parts.flatMap((part) => {
|
|
310
|
+
if (part.type === "text") {
|
|
311
|
+
return [{ type: "text", text: part.text }];
|
|
312
|
+
}
|
|
313
|
+
if (part.type === "image") {
|
|
314
|
+
return [{ type: "image_url", image_url: { url: part.image } }];
|
|
315
|
+
}
|
|
316
|
+
if (part.type === "audio") {
|
|
317
|
+
return [{ type: "input_audio", input_audio: { data: toDataURL(part.data, part.mediaType) } }];
|
|
318
|
+
}
|
|
319
|
+
return [];
|
|
320
|
+
});
|
|
196
321
|
};
|
|
197
322
|
const mapMessages = (messages) => messages.map((message) => {
|
|
198
323
|
if (message.role === "tool") {
|
|
@@ -264,7 +389,7 @@ const mapResponsesTools = (tools) => tools
|
|
|
264
389
|
};
|
|
265
390
|
})
|
|
266
391
|
: undefined;
|
|
267
|
-
const
|
|
392
|
+
const mapChatToolChoice = (toolChoice) => {
|
|
268
393
|
if (!toolChoice) {
|
|
269
394
|
return undefined;
|
|
270
395
|
}
|
|
@@ -278,21 +403,48 @@ const mapToolChoice = (toolChoice) => {
|
|
|
278
403
|
}
|
|
279
404
|
};
|
|
280
405
|
};
|
|
281
|
-
const
|
|
282
|
-
if (!
|
|
406
|
+
const mapResponsesToolChoice = (toolChoice, tools) => {
|
|
407
|
+
if (!toolChoice) {
|
|
283
408
|
return undefined;
|
|
284
409
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
name: input.structuredOutput.name ?? "response",
|
|
289
|
-
strict: true,
|
|
290
|
-
schema: toJSONSchema(input.structuredOutput.schema)
|
|
410
|
+
if (typeof toolChoice === "string") {
|
|
411
|
+
if (toolChoice === "required" && Object.keys(tools ?? {}).length !== 1) {
|
|
412
|
+
throw new UnsupportedFeatureError('Qwen Responses supports toolChoice "required" only when exactly one tool is provided.');
|
|
291
413
|
}
|
|
414
|
+
return toolChoice;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
type: "allowed_tools",
|
|
418
|
+
mode: "required",
|
|
419
|
+
tools: [{ type: "function", name: toolChoice.toolName }]
|
|
292
420
|
};
|
|
293
421
|
};
|
|
422
|
+
const mapStructuredOutput = (input) => {
|
|
423
|
+
if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
return { type: "json_object" };
|
|
427
|
+
};
|
|
428
|
+
const withStructuredOutputMessages = (input) => {
|
|
429
|
+
if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
|
|
430
|
+
return input.messages;
|
|
431
|
+
}
|
|
432
|
+
const schema = JSON.stringify(toJSONSchema(input.structuredOutput.schema));
|
|
433
|
+
return [
|
|
434
|
+
{
|
|
435
|
+
role: "system",
|
|
436
|
+
parts: [
|
|
437
|
+
{
|
|
438
|
+
type: "text",
|
|
439
|
+
text: `Return only valid JSON matching this JSON Schema: ${schema}`
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
...input.messages
|
|
444
|
+
];
|
|
445
|
+
};
|
|
294
446
|
const hasPreservedReasoning = (messages) => messages.some((message) => message.role === "assistant" && reasoningContentFromMessage(message));
|
|
295
|
-
const
|
|
447
|
+
const mapChatReasoning = (input) => {
|
|
296
448
|
if (!input.reasoning) {
|
|
297
449
|
return undefined;
|
|
298
450
|
}
|
|
@@ -304,6 +456,63 @@ const mapReasoning = (input) => {
|
|
|
304
456
|
...(hasPreservedReasoning(input.messages) ? { preserve_thinking: true } : {})
|
|
305
457
|
};
|
|
306
458
|
};
|
|
459
|
+
const mapResponsesReasoning = (input) => {
|
|
460
|
+
const effort = input.reasoning?.effort;
|
|
461
|
+
if (!effort) {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
reasoning: {
|
|
466
|
+
effort: effort === "low" ? "minimal" : effort
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
const mapResponsesUsage = (usage) => usage
|
|
471
|
+
? {
|
|
472
|
+
inputTokens: usage.input_tokens,
|
|
473
|
+
cachedInputTokens: usage.input_tokens_details?.cached_tokens,
|
|
474
|
+
outputTokens: usage.output_tokens,
|
|
475
|
+
reasoningTokens: usage.output_tokens_details?.reasoning_tokens,
|
|
476
|
+
totalTokens: usage.total_tokens
|
|
477
|
+
}
|
|
478
|
+
: undefined;
|
|
479
|
+
const mapChatUsage = (usage) => usage
|
|
480
|
+
? {
|
|
481
|
+
inputTokens: usage.prompt_tokens,
|
|
482
|
+
cachedInputTokens: usage.prompt_tokens_details?.cached_tokens ?? usage.input_tokens_details?.cached_tokens,
|
|
483
|
+
outputTokens: usage.completion_tokens,
|
|
484
|
+
reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? usage.output_tokens_details?.reasoning_tokens,
|
|
485
|
+
totalTokens: usage.total_tokens
|
|
486
|
+
}
|
|
487
|
+
: undefined;
|
|
488
|
+
const hasHostedTools = (tools) => Object.values(tools ?? {}).some((tool) => !isCallableToolDefinition(tool));
|
|
489
|
+
const hasMessagePart = (messages, type) => messages.some((message) => message.parts.some((part) => part.type === type));
|
|
490
|
+
const resolveApiMode = (modelId, requestedMode, input) => {
|
|
491
|
+
const needsChat = input.maxTokens !== undefined ||
|
|
492
|
+
input.reasoning?.budgetTokens !== undefined ||
|
|
493
|
+
input.structuredOutput?.mode === "native" ||
|
|
494
|
+
hasMessagePart(input.messages, "audio");
|
|
495
|
+
const needsResponses = hasHostedTools(input.tools) || hasMessagePart(input.messages, "file");
|
|
496
|
+
if (needsChat && needsResponses) {
|
|
497
|
+
throw new UnsupportedFeatureError("Qwen cannot combine Responses-only hosted/file inputs with Chat-only maxTokens, audio, reasoning budgets, or structured output.");
|
|
498
|
+
}
|
|
499
|
+
if (isQwenOmniLanguageModel(modelId)) {
|
|
500
|
+
if (requestedMode === "responses" || needsResponses) {
|
|
501
|
+
throw new UnsupportedFeatureError("Qwen Omni uses streaming Chat Completions and does not support Responses hosted tools or file inputs.");
|
|
502
|
+
}
|
|
503
|
+
return "chat";
|
|
504
|
+
}
|
|
505
|
+
if (requestedMode === "responses" && needsChat) {
|
|
506
|
+
throw new UnsupportedFeatureError('Qwen Responses does not process maxTokens, audio input, reasoning budgets, or native structured output; use apiMode "chat" or "auto".');
|
|
507
|
+
}
|
|
508
|
+
if (requestedMode === "chat" && needsResponses) {
|
|
509
|
+
throw new UnsupportedFeatureError('Qwen Chat Completions does not support Responses hosted tools or OCR file inputs.');
|
|
510
|
+
}
|
|
511
|
+
if (requestedMode === "responses" || requestedMode === "chat") {
|
|
512
|
+
return requestedMode;
|
|
513
|
+
}
|
|
514
|
+
return needsChat ? "chat" : "responses";
|
|
515
|
+
};
|
|
307
516
|
const parseAssistantMessage = (message) => ({
|
|
308
517
|
role: "assistant",
|
|
309
518
|
parts: [
|
|
@@ -366,6 +575,16 @@ const toResponsesInput = (messages) => {
|
|
|
366
575
|
case "image":
|
|
367
576
|
content.push({ type: "input_image", image_url: part.image });
|
|
368
577
|
break;
|
|
578
|
+
case "file":
|
|
579
|
+
if (!/^https?:\/\//i.test(part.data)) {
|
|
580
|
+
throw new UnsupportedFeatureError("Qwen Responses file input requires a public HTTP(S) file URL; DashScope Files IDs are reserved for batch jobs.");
|
|
581
|
+
}
|
|
582
|
+
content.push({
|
|
583
|
+
type: "input_file",
|
|
584
|
+
file_url: part.data,
|
|
585
|
+
...(part.filename ? { filename: part.filename } : {})
|
|
586
|
+
});
|
|
587
|
+
break;
|
|
369
588
|
case "tool-call":
|
|
370
589
|
if (message.role === "assistant") {
|
|
371
590
|
input.push({
|
|
@@ -376,6 +595,15 @@ const toResponsesInput = (messages) => {
|
|
|
376
595
|
});
|
|
377
596
|
}
|
|
378
597
|
break;
|
|
598
|
+
case "provider-data":
|
|
599
|
+
if (message.role === "assistant" &&
|
|
600
|
+
part.provider === "qwen" &&
|
|
601
|
+
part.data &&
|
|
602
|
+
typeof part.data === "object" &&
|
|
603
|
+
typeof part.data.type === "string") {
|
|
604
|
+
input.push(part.data);
|
|
605
|
+
}
|
|
606
|
+
break;
|
|
379
607
|
}
|
|
380
608
|
}
|
|
381
609
|
if (content.length) {
|
|
@@ -553,17 +781,18 @@ const streamResponses = async function* (response) {
|
|
|
553
781
|
}
|
|
554
782
|
if (type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
|
|
555
783
|
const responseData = json.response ?? {};
|
|
784
|
+
if (typeof responseData.id === "string") {
|
|
785
|
+
yield {
|
|
786
|
+
type: "provider-data",
|
|
787
|
+
provider: "qwen",
|
|
788
|
+
data: { responseId: responseData.id }
|
|
789
|
+
};
|
|
790
|
+
}
|
|
556
791
|
yield {
|
|
557
792
|
type: "finish",
|
|
558
793
|
finishReason: normalizeResponsesFinishReason(responseData.status, sawToolCalls),
|
|
559
794
|
providerFinishReason: responseData.status,
|
|
560
|
-
usage: responseData.usage
|
|
561
|
-
? {
|
|
562
|
-
inputTokens: responseData.usage.input_tokens,
|
|
563
|
-
outputTokens: responseData.usage.output_tokens,
|
|
564
|
-
totalTokens: responseData.usage.total_tokens
|
|
565
|
-
}
|
|
566
|
-
: undefined
|
|
795
|
+
usage: mapResponsesUsage(responseData.usage)
|
|
567
796
|
};
|
|
568
797
|
}
|
|
569
798
|
}
|
|
@@ -583,14 +812,17 @@ class QwenLanguageModel {
|
|
|
583
812
|
this.capabilities = qwenLanguageCapabilities(modelId);
|
|
584
813
|
}
|
|
585
814
|
async generate(input) {
|
|
815
|
+
if (isQwenOmniLanguageModel(this.modelId)) {
|
|
816
|
+
throw new UnsupportedFeatureError(`Qwen Omni model "${this.modelId}" is streaming-only. Use streamText() instead of generateText().`);
|
|
817
|
+
}
|
|
586
818
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
587
819
|
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
588
|
-
const apiMode = providerOptions.apiMode ?? "
|
|
820
|
+
const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input);
|
|
589
821
|
delete providerOptions.apiMode;
|
|
590
822
|
try {
|
|
591
823
|
if (apiMode === "responses") {
|
|
592
824
|
const previousResponse = getProviderResponseId(input.messages);
|
|
593
|
-
const responseProviderOptions =
|
|
825
|
+
const responseProviderOptions = stripResponsesRequestOptions(providerOptions);
|
|
594
826
|
const messages = previousResponse && previousResponse.index < input.messages.length - 1
|
|
595
827
|
? input.messages.slice(previousResponse.index + 1)
|
|
596
828
|
: input.messages;
|
|
@@ -603,12 +835,10 @@ class QwenLanguageModel {
|
|
|
603
835
|
model: this.modelId,
|
|
604
836
|
...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
|
|
605
837
|
...(messages.length ? { input: toResponsesInput(messages) } : {}),
|
|
606
|
-
...(input.structuredOutput?.mode === "native" ? { response_format: mapStructuredOutput(input) } : {}),
|
|
607
838
|
tools: mapResponsesTools(input.tools),
|
|
608
|
-
tool_choice:
|
|
839
|
+
tool_choice: mapResponsesToolChoice(input.toolChoice, input.tools),
|
|
609
840
|
temperature: input.temperature,
|
|
610
|
-
|
|
611
|
-
...mapReasoning(input),
|
|
841
|
+
...mapResponsesReasoning(input),
|
|
612
842
|
stream: false
|
|
613
843
|
})
|
|
614
844
|
}), input);
|
|
@@ -623,29 +853,25 @@ class QwenLanguageModel {
|
|
|
623
853
|
.join(""),
|
|
624
854
|
finishReason: normalizeResponsesFinishReason(json.status, hasToolCalls),
|
|
625
855
|
providerFinishReason: json.status,
|
|
626
|
-
usage:
|
|
627
|
-
inputTokens: json.usage?.input_tokens,
|
|
628
|
-
outputTokens: json.usage?.output_tokens,
|
|
629
|
-
totalTokens: json.usage?.total_tokens
|
|
630
|
-
},
|
|
856
|
+
usage: mapResponsesUsage(json.usage),
|
|
631
857
|
rawResponse: json
|
|
632
858
|
};
|
|
633
859
|
}
|
|
634
860
|
const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
|
|
635
861
|
method: "POST",
|
|
636
|
-
headers:
|
|
862
|
+
headers: providerHeaders(this.apiKey, providerOptions),
|
|
637
863
|
signal,
|
|
638
864
|
body: JSON.stringify({
|
|
639
|
-
...providerOptions,
|
|
865
|
+
...stripHeaderOptions(providerOptions),
|
|
640
866
|
model: this.modelId,
|
|
641
|
-
messages: mapMessages(input
|
|
867
|
+
messages: mapMessages(withStructuredOutputMessages(input)),
|
|
642
868
|
tools: mapChatTools(input.tools),
|
|
643
|
-
tool_choice:
|
|
869
|
+
tool_choice: mapChatToolChoice(input.toolChoice),
|
|
644
870
|
response_format: mapStructuredOutput(input),
|
|
645
871
|
temperature: input.temperature,
|
|
646
872
|
max_tokens: input.maxTokens,
|
|
647
873
|
stream: false,
|
|
648
|
-
...
|
|
874
|
+
...mapChatReasoning(input)
|
|
649
875
|
})
|
|
650
876
|
}), input);
|
|
651
877
|
const json = await parseJson(response);
|
|
@@ -660,11 +886,7 @@ class QwenLanguageModel {
|
|
|
660
886
|
.join(""),
|
|
661
887
|
finishReason: normalizeFinishReason(choice?.finish_reason),
|
|
662
888
|
providerFinishReason: choice?.finish_reason,
|
|
663
|
-
usage:
|
|
664
|
-
inputTokens: json.usage?.prompt_tokens,
|
|
665
|
-
outputTokens: json.usage?.completion_tokens,
|
|
666
|
-
totalTokens: json.usage?.total_tokens
|
|
667
|
-
},
|
|
889
|
+
usage: mapChatUsage(json.usage),
|
|
668
890
|
rawResponse: json
|
|
669
891
|
};
|
|
670
892
|
}
|
|
@@ -675,10 +897,14 @@ class QwenLanguageModel {
|
|
|
675
897
|
async stream(input) {
|
|
676
898
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
677
899
|
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
678
|
-
const apiMode = providerOptions.apiMode ?? "
|
|
900
|
+
const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input);
|
|
679
901
|
delete providerOptions.apiMode;
|
|
680
902
|
if (apiMode === "responses") {
|
|
681
|
-
const
|
|
903
|
+
const previousResponse = getProviderResponseId(input.messages);
|
|
904
|
+
const responseProviderOptions = stripResponsesRequestOptions(providerOptions);
|
|
905
|
+
const messages = previousResponse && previousResponse.index < input.messages.length - 1
|
|
906
|
+
? input.messages.slice(previousResponse.index + 1)
|
|
907
|
+
: input.messages;
|
|
682
908
|
const response = await withRetry(() => this.fetcher(`${this.baseURL}/responses`, {
|
|
683
909
|
method: "POST",
|
|
684
910
|
headers: providerHeaders(this.apiKey, providerOptions),
|
|
@@ -686,13 +912,12 @@ class QwenLanguageModel {
|
|
|
686
912
|
body: JSON.stringify({
|
|
687
913
|
...responseProviderOptions,
|
|
688
914
|
model: this.modelId,
|
|
689
|
-
|
|
690
|
-
...(
|
|
915
|
+
...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
|
|
916
|
+
...(messages.length ? { input: toResponsesInput(messages) } : {}),
|
|
691
917
|
tools: mapResponsesTools(input.tools),
|
|
692
|
-
tool_choice:
|
|
918
|
+
tool_choice: mapResponsesToolChoice(input.toolChoice, input.tools),
|
|
693
919
|
temperature: input.temperature,
|
|
694
|
-
|
|
695
|
-
...mapReasoning(input),
|
|
920
|
+
...mapResponsesReasoning(input),
|
|
696
921
|
stream: true
|
|
697
922
|
})
|
|
698
923
|
}), input);
|
|
@@ -707,20 +932,21 @@ class QwenLanguageModel {
|
|
|
707
932
|
}
|
|
708
933
|
const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
|
|
709
934
|
method: "POST",
|
|
710
|
-
headers:
|
|
935
|
+
headers: providerHeaders(this.apiKey, providerOptions),
|
|
711
936
|
signal,
|
|
712
937
|
body: JSON.stringify({
|
|
713
|
-
...providerOptions,
|
|
938
|
+
...stripHeaderOptions(providerOptions),
|
|
714
939
|
model: this.modelId,
|
|
715
|
-
|
|
940
|
+
modalities: isQwenOmniLanguageModel(this.modelId) ? providerOptions.modalities ?? ["text"] : undefined,
|
|
941
|
+
messages: mapMessages(withStructuredOutputMessages(input)),
|
|
716
942
|
tools: mapChatTools(input.tools),
|
|
717
|
-
tool_choice:
|
|
943
|
+
tool_choice: mapChatToolChoice(input.toolChoice),
|
|
718
944
|
response_format: mapStructuredOutput(input),
|
|
719
945
|
temperature: input.temperature,
|
|
720
946
|
max_tokens: input.maxTokens,
|
|
721
947
|
stream: true,
|
|
722
948
|
stream_options: { include_usage: true },
|
|
723
|
-
...
|
|
949
|
+
...mapChatReasoning(input)
|
|
724
950
|
})
|
|
725
951
|
}), input);
|
|
726
952
|
return (async function* () {
|
|
@@ -747,37 +973,40 @@ class QwenLanguageModel {
|
|
|
747
973
|
yield { type: "text-delta", textDelta: delta.content };
|
|
748
974
|
}
|
|
749
975
|
for (const toolCall of delta?.tool_calls ?? []) {
|
|
750
|
-
const
|
|
751
|
-
const existing = toolBuffers.get(
|
|
976
|
+
const index = Number(toolCall.index ?? 0);
|
|
977
|
+
const existing = toolBuffers.get(index) ?? {
|
|
978
|
+
id: toolCall.id ?? `${index}`,
|
|
752
979
|
name: toolCall.function?.name ?? "",
|
|
753
|
-
args: ""
|
|
980
|
+
args: "",
|
|
981
|
+
emitted: false
|
|
754
982
|
};
|
|
983
|
+
existing.id = toolCall.id ?? existing.id;
|
|
755
984
|
existing.name ||= toolCall.function?.name ?? "";
|
|
756
985
|
existing.args += toolCall.function?.arguments ?? "";
|
|
757
|
-
toolBuffers.set(
|
|
758
|
-
if (choice?.finish_reason === "tool_calls") {
|
|
759
|
-
yield {
|
|
760
|
-
type: "tool-call",
|
|
761
|
-
toolCall: {
|
|
762
|
-
id,
|
|
763
|
-
name: existing.name,
|
|
764
|
-
input: JSON.parse(existing.args || "{}")
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
}
|
|
986
|
+
toolBuffers.set(index, existing);
|
|
768
987
|
}
|
|
769
988
|
if (choice?.finish_reason) {
|
|
989
|
+
if (choice.finish_reason === "tool_calls") {
|
|
990
|
+
for (const toolCall of toolBuffers.values()) {
|
|
991
|
+
if (toolCall.emitted) {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
toolCall.emitted = true;
|
|
995
|
+
yield {
|
|
996
|
+
type: "tool-call",
|
|
997
|
+
toolCall: {
|
|
998
|
+
id: toolCall.id,
|
|
999
|
+
name: toolCall.name,
|
|
1000
|
+
input: JSON.parse(toolCall.args || "{}")
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
770
1005
|
yield {
|
|
771
1006
|
type: "finish",
|
|
772
1007
|
finishReason: normalizeFinishReason(choice.finish_reason),
|
|
773
1008
|
providerFinishReason: choice.finish_reason,
|
|
774
|
-
usage: json.usage
|
|
775
|
-
? {
|
|
776
|
-
inputTokens: json.usage.prompt_tokens,
|
|
777
|
-
outputTokens: json.usage.completion_tokens,
|
|
778
|
-
totalTokens: json.usage.total_tokens
|
|
779
|
-
}
|
|
780
|
-
: undefined
|
|
1009
|
+
usage: mapChatUsage(json.usage)
|
|
781
1010
|
};
|
|
782
1011
|
}
|
|
783
1012
|
}
|
|
@@ -834,6 +1063,67 @@ class QwenEmbeddingModel {
|
|
|
834
1063
|
}
|
|
835
1064
|
}
|
|
836
1065
|
}
|
|
1066
|
+
const toMultimodalEmbeddingContent = (value) => {
|
|
1067
|
+
if (typeof value === "string") {
|
|
1068
|
+
return { text: value };
|
|
1069
|
+
}
|
|
1070
|
+
if (value.mediaType.startsWith("image/")) {
|
|
1071
|
+
return { image: mediaValue(value) };
|
|
1072
|
+
}
|
|
1073
|
+
if (value.mediaType.startsWith("video/")) {
|
|
1074
|
+
if (!value.uri) {
|
|
1075
|
+
throw new UnsupportedFeatureError("Qwen multimodal embedding requires a public URL for video input.");
|
|
1076
|
+
}
|
|
1077
|
+
return { video: value.uri };
|
|
1078
|
+
}
|
|
1079
|
+
throw new UnsupportedFeatureError(`Qwen multimodal embedding supports text, image, and video inputs, not "${value.mediaType}".`);
|
|
1080
|
+
};
|
|
1081
|
+
class QwenMultimodalEmbeddingModelImpl {
|
|
1082
|
+
modelId;
|
|
1083
|
+
apiKey;
|
|
1084
|
+
taskBaseURL;
|
|
1085
|
+
fetcher;
|
|
1086
|
+
provider = "qwen";
|
|
1087
|
+
capabilities = {
|
|
1088
|
+
...embeddingCapabilities,
|
|
1089
|
+
vision: true
|
|
1090
|
+
};
|
|
1091
|
+
constructor(modelId, apiKey, taskBaseURL, fetcher) {
|
|
1092
|
+
this.modelId = modelId;
|
|
1093
|
+
this.apiKey = apiKey;
|
|
1094
|
+
this.taskBaseURL = taskBaseURL;
|
|
1095
|
+
this.fetcher = fetcher;
|
|
1096
|
+
}
|
|
1097
|
+
async embed(input) {
|
|
1098
|
+
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1099
|
+
try {
|
|
1100
|
+
const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/embeddings/multimodal-embedding/multimodal-embedding`, {
|
|
1101
|
+
method: "POST",
|
|
1102
|
+
headers: jsonHeaders(this.apiKey),
|
|
1103
|
+
signal,
|
|
1104
|
+
body: JSON.stringify({
|
|
1105
|
+
model: this.modelId,
|
|
1106
|
+
input: { contents: input.values.map(toMultimodalEmbeddingContent) },
|
|
1107
|
+
...(input.providerOptions && Object.keys(input.providerOptions).length
|
|
1108
|
+
? { parameters: input.providerOptions }
|
|
1109
|
+
: {})
|
|
1110
|
+
})
|
|
1111
|
+
}), input);
|
|
1112
|
+
const json = await parseJson(response);
|
|
1113
|
+
return {
|
|
1114
|
+
embeddings: (json.output?.embeddings ?? []).map((entry) => entry.embedding),
|
|
1115
|
+
usage: {
|
|
1116
|
+
inputTokens: json.usage?.input_tokens,
|
|
1117
|
+
totalTokens: json.usage?.total_tokens
|
|
1118
|
+
},
|
|
1119
|
+
rawResponse: json
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
finally {
|
|
1123
|
+
cleanup();
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
837
1127
|
const normalizeUploadedFile = (json) => ({
|
|
838
1128
|
name: json.id ?? json.name ?? json.file_id ?? "",
|
|
839
1129
|
uri: json.url ?? json.uri,
|
|
@@ -844,14 +1134,6 @@ const normalizeUploadedFile = (json) => ({
|
|
|
844
1134
|
rawResponse: json,
|
|
845
1135
|
providerMetadata: json
|
|
846
1136
|
});
|
|
847
|
-
const normalizeFileSearchStore = (json) => ({
|
|
848
|
-
name: json.id ?? json.name ?? "",
|
|
849
|
-
displayName: json.name ?? json.display_name ?? json.displayName,
|
|
850
|
-
createTime: json.created_at ? String(json.created_at) : json.createTime,
|
|
851
|
-
updateTime: json.updated_at ? String(json.updated_at) : json.updateTime,
|
|
852
|
-
rawResponse: json,
|
|
853
|
-
providerMetadata: json
|
|
854
|
-
});
|
|
855
1137
|
const normalizeBatchJob = (json) => ({
|
|
856
1138
|
name: json.id ?? json.name ?? "",
|
|
857
1139
|
model: json.model,
|
|
@@ -863,10 +1145,13 @@ const normalizeBatchJob = (json) => ({
|
|
|
863
1145
|
providerMetadata: json
|
|
864
1146
|
});
|
|
865
1147
|
const normalizeOperation = (json) => ({
|
|
866
|
-
name: json.task_id ?? json.id ?? json.name ?? "",
|
|
1148
|
+
name: json.output?.task_id ?? json.task_id ?? json.id ?? json.name ?? "",
|
|
867
1149
|
done: json.output?.task_status ? ["SUCCEEDED", "FAILED", "CANCELED"].includes(json.output.task_status) : json.done,
|
|
868
1150
|
response: json.output ?? json.response,
|
|
869
|
-
error: json.error
|
|
1151
|
+
error: json.error ??
|
|
1152
|
+
(json.output?.task_status === "FAILED"
|
|
1153
|
+
? { message: json.output?.message ?? "Qwen task failed.", code: json.output?.code }
|
|
1154
|
+
: undefined),
|
|
870
1155
|
metadata: json.usage ?? json.metadata,
|
|
871
1156
|
rawResponse: json
|
|
872
1157
|
});
|
|
@@ -900,7 +1185,8 @@ class QwenFilesClient {
|
|
|
900
1185
|
async get(input) {
|
|
901
1186
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
902
1187
|
try {
|
|
903
|
-
const
|
|
1188
|
+
const fileId = encodeQwenResourceId(input.name, "file");
|
|
1189
|
+
const response = await withRetry(() => this.fetcher(`${this.baseURL}/files/${fileId}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
904
1190
|
return normalizeUploadedFile(await parseJson(response));
|
|
905
1191
|
}
|
|
906
1192
|
finally {
|
|
@@ -925,88 +1211,8 @@ class QwenFilesClient {
|
|
|
925
1211
|
async delete(input) {
|
|
926
1212
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
927
1213
|
try {
|
|
928
|
-
const
|
|
929
|
-
const
|
|
930
|
-
return { name: input.name, rawResponse: json };
|
|
931
|
-
}
|
|
932
|
-
finally {
|
|
933
|
-
cleanup();
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
class QwenFileSearchStoresClient {
|
|
938
|
-
apiKey;
|
|
939
|
-
baseURL;
|
|
940
|
-
fetcher;
|
|
941
|
-
constructor(apiKey, baseURL, fetcher) {
|
|
942
|
-
this.apiKey = apiKey;
|
|
943
|
-
this.baseURL = baseURL;
|
|
944
|
-
this.fetcher = fetcher;
|
|
945
|
-
}
|
|
946
|
-
async create(input = {}) {
|
|
947
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
948
|
-
try {
|
|
949
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores`, {
|
|
950
|
-
method: "POST",
|
|
951
|
-
headers: jsonHeaders(this.apiKey),
|
|
952
|
-
signal,
|
|
953
|
-
body: JSON.stringify({ ...input.providerOptions, name: input.displayName })
|
|
954
|
-
}), input);
|
|
955
|
-
return normalizeFileSearchStore(await parseJson(response));
|
|
956
|
-
}
|
|
957
|
-
finally {
|
|
958
|
-
cleanup();
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
async upload(input) {
|
|
962
|
-
const files = new QwenFilesClient(this.apiKey, this.baseURL, this.fetcher);
|
|
963
|
-
const file = await files.upload(input);
|
|
964
|
-
return this.importFile({ ...input, fileName: file.name });
|
|
965
|
-
}
|
|
966
|
-
async importFile(input) {
|
|
967
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
968
|
-
try {
|
|
969
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores/${input.storeName}/files`, {
|
|
970
|
-
method: "POST",
|
|
971
|
-
headers: jsonHeaders(this.apiKey),
|
|
972
|
-
signal,
|
|
973
|
-
body: JSON.stringify({ ...input.providerOptions, file_id: input.fileName })
|
|
974
|
-
}), input);
|
|
975
|
-
return normalizeOperation(await parseJson(response));
|
|
976
|
-
}
|
|
977
|
-
finally {
|
|
978
|
-
cleanup();
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
async get(input) {
|
|
982
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
983
|
-
try {
|
|
984
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores/${input.name}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
985
|
-
return normalizeFileSearchStore(await parseJson(response));
|
|
986
|
-
}
|
|
987
|
-
finally {
|
|
988
|
-
cleanup();
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
async list(input = {}) {
|
|
992
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
993
|
-
try {
|
|
994
|
-
const response = await withRetry(() => this.fetcher(appendQuery(`${this.baseURL}/file_search_stores`, { limit: input.pageSize, after: input.pageToken }), { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
995
|
-
const json = await parseJson(response);
|
|
996
|
-
return {
|
|
997
|
-
stores: (json.data ?? json.file_search_stores ?? json.fileSearchStores ?? []).map(normalizeFileSearchStore),
|
|
998
|
-
nextPageToken: json.next ?? json.nextPageToken,
|
|
999
|
-
rawResponse: json
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
finally {
|
|
1003
|
-
cleanup();
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
async delete(input) {
|
|
1007
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1008
|
-
try {
|
|
1009
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores/${input.name}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
1214
|
+
const fileId = encodeQwenResourceId(input.name, "file");
|
|
1215
|
+
const response = await withRetry(() => this.fetcher(`${this.baseURL}/files/${fileId}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
1010
1216
|
const json = await parseJson(response);
|
|
1011
1217
|
return { name: input.name, rawResponse: json };
|
|
1012
1218
|
}
|
|
@@ -1025,6 +1231,9 @@ class QwenBatchesClient {
|
|
|
1025
1231
|
this.fetcher = fetcher;
|
|
1026
1232
|
}
|
|
1027
1233
|
async create(input) {
|
|
1234
|
+
if (!input.fileName) {
|
|
1235
|
+
throw new ConfigurationError("Qwen batch creation requires a JSONL fileName uploaded with purpose \"batch\".");
|
|
1236
|
+
}
|
|
1028
1237
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1029
1238
|
try {
|
|
1030
1239
|
const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches`, {
|
|
@@ -1036,9 +1245,7 @@ class QwenBatchesClient {
|
|
|
1036
1245
|
input_file_id: input.fileName,
|
|
1037
1246
|
endpoint: input.providerOptions?.endpoint ?? "/v1/chat/completions",
|
|
1038
1247
|
completion_window: input.providerOptions?.completion_window ?? "24h",
|
|
1039
|
-
metadata: input.displayName ? { displayName: input.displayName } : undefined
|
|
1040
|
-
model: input.modelId,
|
|
1041
|
-
requests: input.requests
|
|
1248
|
+
metadata: input.displayName ? { displayName: input.displayName } : undefined
|
|
1042
1249
|
})
|
|
1043
1250
|
}), input);
|
|
1044
1251
|
return normalizeBatchJob(await parseJson(response));
|
|
@@ -1050,7 +1257,8 @@ class QwenBatchesClient {
|
|
|
1050
1257
|
async get(input) {
|
|
1051
1258
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1052
1259
|
try {
|
|
1053
|
-
const
|
|
1260
|
+
const batchId = encodeQwenResourceId(input.name, "batch");
|
|
1261
|
+
const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${batchId}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
1054
1262
|
return normalizeBatchJob(await parseJson(response));
|
|
1055
1263
|
}
|
|
1056
1264
|
finally {
|
|
@@ -1071,7 +1279,8 @@ class QwenBatchesClient {
|
|
|
1071
1279
|
async cancel(input) {
|
|
1072
1280
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1073
1281
|
try {
|
|
1074
|
-
const
|
|
1282
|
+
const batchId = encodeQwenResourceId(input.name, "batch");
|
|
1283
|
+
const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${batchId}/cancel`, { method: "POST", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
1075
1284
|
return normalizeBatchJob(await parseJson(response));
|
|
1076
1285
|
}
|
|
1077
1286
|
finally {
|
|
@@ -1081,7 +1290,8 @@ class QwenBatchesClient {
|
|
|
1081
1290
|
async delete(input) {
|
|
1082
1291
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1083
1292
|
try {
|
|
1084
|
-
const
|
|
1293
|
+
const batchId = encodeQwenResourceId(input.name, "batch");
|
|
1294
|
+
const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${batchId}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
1085
1295
|
const json = await parseJson(response);
|
|
1086
1296
|
return { name: input.name, rawResponse: json };
|
|
1087
1297
|
}
|
|
@@ -1106,29 +1316,59 @@ class QwenTranscriptionModel {
|
|
|
1106
1316
|
this.responseLimits = responseLimits;
|
|
1107
1317
|
}
|
|
1108
1318
|
async transcribe(input) {
|
|
1319
|
+
if (input.prompt) {
|
|
1320
|
+
throw new UnsupportedFeatureError("Qwen3-ASR-Flash does not expose the common transcription prompt field; use providerOptions for documented asr_options.");
|
|
1321
|
+
}
|
|
1109
1322
|
const { signal, cleanup, abort } = withTimeoutSignal(input);
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
form.set("file", await createFile(input.audio.data, input.audio.mediaType, input.audio.filename ?? "audio"));
|
|
1119
|
-
if (input.prompt)
|
|
1120
|
-
form.set("prompt", input.prompt);
|
|
1121
|
-
if (input.language)
|
|
1122
|
-
form.set("language", input.language);
|
|
1323
|
+
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
1324
|
+
const nestedOptions = providerOptions.asr_options && typeof providerOptions.asr_options === "object"
|
|
1325
|
+
? providerOptions.asr_options
|
|
1326
|
+
: {};
|
|
1327
|
+
delete providerOptions.asr_options;
|
|
1328
|
+
delete providerOptions.model;
|
|
1329
|
+
delete providerOptions.messages;
|
|
1330
|
+
delete providerOptions.stream;
|
|
1123
1331
|
try {
|
|
1124
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/
|
|
1332
|
+
const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
|
|
1333
|
+
method: "POST",
|
|
1334
|
+
headers: jsonHeaders(this.apiKey),
|
|
1335
|
+
signal,
|
|
1336
|
+
body: JSON.stringify({
|
|
1337
|
+
model: this.modelId,
|
|
1338
|
+
messages: [
|
|
1339
|
+
{
|
|
1340
|
+
role: "user",
|
|
1341
|
+
content: [
|
|
1342
|
+
{
|
|
1343
|
+
type: "input_audio",
|
|
1344
|
+
input_audio: {
|
|
1345
|
+
data: toDataURL(input.audio.data, input.audio.mediaType)
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
]
|
|
1349
|
+
}
|
|
1350
|
+
],
|
|
1351
|
+
stream: false,
|
|
1352
|
+
asr_options: {
|
|
1353
|
+
...providerOptions,
|
|
1354
|
+
...nestedOptions,
|
|
1355
|
+
...(input.language ? { language: input.language } : {})
|
|
1356
|
+
}
|
|
1357
|
+
})
|
|
1358
|
+
}), input);
|
|
1125
1359
|
const json = await parseJson(response, {
|
|
1126
1360
|
maxBytes: this.responseLimits.transcriptionBytes,
|
|
1127
1361
|
errorBodyBytes: this.responseLimits.errorBodyBytes,
|
|
1128
|
-
endpoint: "
|
|
1362
|
+
endpoint: "chat/completions",
|
|
1129
1363
|
abort
|
|
1130
1364
|
});
|
|
1131
|
-
|
|
1365
|
+
const content = json.choices?.[0]?.message?.content;
|
|
1366
|
+
const text = Array.isArray(content)
|
|
1367
|
+
? content.map((part) => part?.text ?? "").join("")
|
|
1368
|
+
: typeof content === "string"
|
|
1369
|
+
? content
|
|
1370
|
+
: "";
|
|
1371
|
+
return { text, rawResponse: json };
|
|
1132
1372
|
}
|
|
1133
1373
|
finally {
|
|
1134
1374
|
cleanup();
|
|
@@ -1138,73 +1378,106 @@ class QwenTranscriptionModel {
|
|
|
1138
1378
|
class QwenSpeechModel {
|
|
1139
1379
|
modelId;
|
|
1140
1380
|
apiKey;
|
|
1141
|
-
|
|
1381
|
+
taskBaseURL;
|
|
1142
1382
|
fetcher;
|
|
1143
1383
|
responseLimits;
|
|
1384
|
+
speechAudioURLValidator;
|
|
1385
|
+
speechAudioMaxRedirects;
|
|
1144
1386
|
provider = "qwen";
|
|
1145
1387
|
capabilities = speechCapabilities;
|
|
1146
|
-
constructor(modelId, apiKey,
|
|
1388
|
+
constructor(modelId, apiKey, taskBaseURL, fetcher, responseLimits = resolveAudioResponseLimits(), speechAudioURLValidator, speechAudioMaxRedirects = 3) {
|
|
1147
1389
|
this.modelId = modelId;
|
|
1148
1390
|
this.apiKey = apiKey;
|
|
1149
|
-
this.
|
|
1391
|
+
this.taskBaseURL = taskBaseURL;
|
|
1150
1392
|
this.fetcher = fetcher;
|
|
1151
1393
|
this.responseLimits = responseLimits;
|
|
1394
|
+
this.speechAudioURLValidator = speechAudioURLValidator;
|
|
1395
|
+
this.speechAudioMaxRedirects = speechAudioMaxRedirects;
|
|
1152
1396
|
}
|
|
1153
1397
|
async generateSpeech(input) {
|
|
1154
1398
|
const { signal, cleanup, abort } = withTimeoutSignal(input);
|
|
1399
|
+
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
1400
|
+
const nestedInput = providerOptions.input && typeof providerOptions.input === "object"
|
|
1401
|
+
? providerOptions.input
|
|
1402
|
+
: {};
|
|
1403
|
+
const parameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
|
|
1404
|
+
? providerOptions.parameters
|
|
1405
|
+
: undefined;
|
|
1406
|
+
delete providerOptions.input;
|
|
1407
|
+
delete providerOptions.parameters;
|
|
1408
|
+
delete providerOptions.model;
|
|
1155
1409
|
try {
|
|
1156
|
-
const response = await withRetry(() => this.fetcher(`${this.
|
|
1410
|
+
const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/multimodal-generation/generation`, {
|
|
1157
1411
|
method: "POST",
|
|
1158
1412
|
headers: jsonHeaders(this.apiKey),
|
|
1159
1413
|
signal,
|
|
1160
|
-
body: JSON.stringify({
|
|
1414
|
+
body: JSON.stringify({
|
|
1415
|
+
model: this.modelId,
|
|
1416
|
+
input: {
|
|
1417
|
+
...providerOptions,
|
|
1418
|
+
...nestedInput,
|
|
1419
|
+
text: input.input,
|
|
1420
|
+
voice: input.voice ?? "Cherry"
|
|
1421
|
+
},
|
|
1422
|
+
...(parameters ? { parameters } : {})
|
|
1423
|
+
})
|
|
1161
1424
|
}), input);
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1425
|
+
const json = await parseJson(response, {
|
|
1426
|
+
maxBytes: this.responseLimits.speechJsonBytes,
|
|
1427
|
+
errorBodyBytes: this.responseLimits.errorBodyBytes,
|
|
1428
|
+
endpoint: "multimodal-generation/generation",
|
|
1429
|
+
abort
|
|
1430
|
+
});
|
|
1431
|
+
const audio = json.audio?.data ?? json.output?.audio?.data;
|
|
1432
|
+
const audioURL = json.audio?.url ?? json.output?.audio?.url;
|
|
1433
|
+
const rawResponse = {
|
|
1434
|
+
...json,
|
|
1435
|
+
...(json.audio && typeof json.audio === "object"
|
|
1436
|
+
? { audio: { ...json.audio, data: undefined, data_omitted: audio ? true : undefined } }
|
|
1437
|
+
: {}),
|
|
1438
|
+
...(json.output && typeof json.output === "object"
|
|
1439
|
+
? {
|
|
1440
|
+
output: {
|
|
1441
|
+
...json.output,
|
|
1442
|
+
...(json.output.audio && typeof json.output.audio === "object"
|
|
1443
|
+
? { audio: { ...json.output.audio, data: undefined, data_omitted: audio ? true : undefined } }
|
|
1444
|
+
: {})
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
: {})
|
|
1448
|
+
};
|
|
1449
|
+
if (audio) {
|
|
1170
1450
|
const decodedAudio = decodeBase64WithLimit(String(audio), {
|
|
1171
1451
|
maxBytes: this.responseLimits.speechBytes,
|
|
1172
1452
|
provider: "qwen",
|
|
1173
|
-
endpoint: "
|
|
1453
|
+
endpoint: "multimodal-generation/generation",
|
|
1174
1454
|
abort
|
|
1175
1455
|
});
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
: {}),
|
|
1181
|
-
...(json.output && typeof json.output === "object"
|
|
1182
|
-
? {
|
|
1183
|
-
output: {
|
|
1184
|
-
...json.output,
|
|
1185
|
-
...(json.output.audio && typeof json.output.audio === "object"
|
|
1186
|
-
? { audio: { ...json.output.audio, data: undefined, data_omitted: true } }
|
|
1187
|
-
: {})
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
: {})
|
|
1456
|
+
return {
|
|
1457
|
+
audio: decodedAudio,
|
|
1458
|
+
mediaType: json.audio?.media_type ?? json.output?.audio?.media_type ?? "audio/pcm",
|
|
1459
|
+
rawResponse
|
|
1191
1460
|
};
|
|
1192
|
-
return { audio: decodedAudio, mediaType: json.audio?.media_type ?? json.output?.audio?.media_type ?? "audio/mpeg", rawResponse };
|
|
1193
1461
|
}
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1196
|
-
|
|
1462
|
+
if (typeof audioURL !== "string" || !/^https?:\/\//i.test(audioURL)) {
|
|
1463
|
+
throw new ProviderHTTPError("Qwen speech response did not contain audio data or a downloadable audio URL.", 502);
|
|
1464
|
+
}
|
|
1465
|
+
const audioResponse = await withRetry(() => downloadQwenSpeechAudio(audioURL, this.fetcher, signal, this.speechAudioURLValidator, this.speechAudioMaxRedirects), input);
|
|
1466
|
+
if (!audioResponse.ok) {
|
|
1467
|
+
const body = await readErrorBodyWithLimit(audioResponse, this.responseLimits.errorBodyBytes);
|
|
1468
|
+
throw new ProviderHTTPError(`Qwen audio download failed with status ${audioResponse.status}.`, audioResponse.status, {
|
|
1197
1469
|
responseBody: body
|
|
1198
1470
|
});
|
|
1199
1471
|
}
|
|
1200
1472
|
return {
|
|
1201
|
-
audio: await readBodyWithLimit(
|
|
1473
|
+
audio: await readBodyWithLimit(audioResponse, {
|
|
1202
1474
|
maxBytes: this.responseLimits.speechBytes,
|
|
1203
1475
|
provider: "qwen",
|
|
1204
|
-
endpoint: "audio
|
|
1476
|
+
endpoint: "speech-audio-download",
|
|
1205
1477
|
abort
|
|
1206
1478
|
}),
|
|
1207
|
-
mediaType:
|
|
1479
|
+
mediaType: audioResponse.headers.get("content-type") ?? json.output?.audio?.media_type ?? "audio/wav",
|
|
1480
|
+
rawResponse
|
|
1208
1481
|
};
|
|
1209
1482
|
}
|
|
1210
1483
|
finally {
|
|
@@ -1213,7 +1486,7 @@ class QwenSpeechModel {
|
|
|
1213
1486
|
}
|
|
1214
1487
|
}
|
|
1215
1488
|
const normalizeGeneratedMedia = (item, fallbackMimeType) => ({
|
|
1216
|
-
uri: item.url ?? item.uri,
|
|
1489
|
+
uri: item.url ?? item.uri ?? item.image ?? item.video_url,
|
|
1217
1490
|
data: item.b64_json || item.base64 ? Uint8Array.from(Buffer.from(item.b64_json ?? item.base64, "base64")) : undefined,
|
|
1218
1491
|
mediaType: item.mime_type ?? item.mediaType ?? fallbackMimeType,
|
|
1219
1492
|
text: item.revised_prompt ?? item.text,
|
|
@@ -1222,15 +1495,13 @@ const normalizeGeneratedMedia = (item, fallbackMimeType) => ({
|
|
|
1222
1495
|
class QwenImageGenerationModel {
|
|
1223
1496
|
modelId;
|
|
1224
1497
|
apiKey;
|
|
1225
|
-
baseURL;
|
|
1226
1498
|
taskBaseURL;
|
|
1227
1499
|
fetcher;
|
|
1228
1500
|
provider = "qwen";
|
|
1229
1501
|
capabilities = imageGenerationCapabilities;
|
|
1230
|
-
constructor(modelId, apiKey,
|
|
1502
|
+
constructor(modelId, apiKey, taskBaseURL, fetcher) {
|
|
1231
1503
|
this.modelId = modelId;
|
|
1232
1504
|
this.apiKey = apiKey;
|
|
1233
|
-
this.baseURL = baseURL;
|
|
1234
1505
|
this.taskBaseURL = taskBaseURL;
|
|
1235
1506
|
this.fetcher = fetcher;
|
|
1236
1507
|
}
|
|
@@ -1238,25 +1509,53 @@ class QwenImageGenerationModel {
|
|
|
1238
1509
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1239
1510
|
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
1240
1511
|
delete providerOptions.endpoint;
|
|
1241
|
-
const
|
|
1512
|
+
const nestedParameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
|
|
1513
|
+
? providerOptions.parameters
|
|
1514
|
+
: {};
|
|
1515
|
+
delete providerOptions.parameters;
|
|
1516
|
+
delete providerOptions.model;
|
|
1517
|
+
delete providerOptions.input;
|
|
1518
|
+
const endpoint = `${this.taskBaseURL}/services/aigc/multimodal-generation/generation`;
|
|
1519
|
+
const sizeFromAspectRatio = {
|
|
1520
|
+
"1:1": "2048*2048",
|
|
1521
|
+
"16:9": "1664*928",
|
|
1522
|
+
"9:16": "928*1664",
|
|
1523
|
+
"4:3": "1472*1104",
|
|
1524
|
+
"3:4": "1104*1472"
|
|
1525
|
+
};
|
|
1242
1526
|
try {
|
|
1243
1527
|
const response = await withRetry(() => this.fetcher(endpoint, {
|
|
1244
1528
|
method: "POST",
|
|
1245
1529
|
headers: jsonHeaders(this.apiKey),
|
|
1246
1530
|
signal,
|
|
1247
1531
|
body: JSON.stringify({
|
|
1248
|
-
...providerOptions,
|
|
1249
1532
|
model: this.modelId,
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1533
|
+
input: {
|
|
1534
|
+
messages: [
|
|
1535
|
+
{
|
|
1536
|
+
role: "user",
|
|
1537
|
+
content: [
|
|
1538
|
+
...(input.images ?? []).map((image) => ({ image: mediaValue(image) })),
|
|
1539
|
+
{ text: input.prompt }
|
|
1540
|
+
]
|
|
1541
|
+
}
|
|
1542
|
+
]
|
|
1543
|
+
},
|
|
1544
|
+
parameters: {
|
|
1545
|
+
...providerOptions,
|
|
1546
|
+
...nestedParameters,
|
|
1547
|
+
n: input.count,
|
|
1548
|
+
size: input.size ?? (input.aspectRatio ? sizeFromAspectRatio[input.aspectRatio] : undefined),
|
|
1549
|
+
negative_prompt: input.negativePrompt
|
|
1550
|
+
}
|
|
1256
1551
|
})
|
|
1257
1552
|
}), input);
|
|
1258
1553
|
const json = await parseJson(response);
|
|
1259
|
-
const data = json.
|
|
1554
|
+
const data = json.output?.choices?.flatMap((choice) => choice.message?.content ?? []) ??
|
|
1555
|
+
json.output?.results ??
|
|
1556
|
+
json.output?.images ??
|
|
1557
|
+
json.data ??
|
|
1558
|
+
[];
|
|
1260
1559
|
return {
|
|
1261
1560
|
images: data.map((item) => normalizeGeneratedMedia(item, input.outputMimeType ?? "image/png")),
|
|
1262
1561
|
text: json.output?.text,
|
|
@@ -1283,20 +1582,49 @@ class QwenVideoGenerationModel {
|
|
|
1283
1582
|
}
|
|
1284
1583
|
async generateVideo(input) {
|
|
1285
1584
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1585
|
+
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
1586
|
+
delete providerOptions.endpoint;
|
|
1587
|
+
const nestedInput = providerOptions.input && typeof providerOptions.input === "object"
|
|
1588
|
+
? providerOptions.input
|
|
1589
|
+
: {};
|
|
1590
|
+
const nestedParameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
|
|
1591
|
+
? providerOptions.parameters
|
|
1592
|
+
: {};
|
|
1593
|
+
delete providerOptions.input;
|
|
1594
|
+
delete providerOptions.parameters;
|
|
1595
|
+
delete providerOptions.model;
|
|
1596
|
+
const isWan27 = /^wan2\.7(?:$|-)/i.test(this.modelId);
|
|
1286
1597
|
try {
|
|
1287
|
-
const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/video-generation/
|
|
1598
|
+
const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/video-generation/video-synthesis`, {
|
|
1288
1599
|
method: "POST",
|
|
1289
1600
|
headers: { ...jsonHeaders(this.apiKey), "X-DashScope-Async": "enable" },
|
|
1290
1601
|
signal,
|
|
1291
1602
|
body: JSON.stringify({
|
|
1292
|
-
...input.providerOptions,
|
|
1293
1603
|
model: this.modelId,
|
|
1294
|
-
input: {
|
|
1295
|
-
|
|
1604
|
+
input: {
|
|
1605
|
+
...nestedInput,
|
|
1606
|
+
prompt: input.prompt,
|
|
1607
|
+
negative_prompt: input.negativePrompt,
|
|
1608
|
+
...(input.image
|
|
1609
|
+
? isWan27
|
|
1610
|
+
? { media: [{ type: "first_frame", url: mediaValue(input.image) }] }
|
|
1611
|
+
: { img_url: mediaValue(input.image) }
|
|
1612
|
+
: {})
|
|
1613
|
+
},
|
|
1614
|
+
parameters: {
|
|
1615
|
+
...providerOptions,
|
|
1616
|
+
...nestedParameters,
|
|
1617
|
+
ratio: input.aspectRatio,
|
|
1618
|
+
duration: input.durationSeconds,
|
|
1619
|
+
n: input.count,
|
|
1620
|
+
output_storage_uri: input.outputStorageUri
|
|
1621
|
+
}
|
|
1296
1622
|
})
|
|
1297
1623
|
}), input);
|
|
1298
1624
|
const json = await parseJson(response);
|
|
1299
|
-
const items = json.output?.results ??
|
|
1625
|
+
const items = json.output?.results ??
|
|
1626
|
+
json.output?.videos ??
|
|
1627
|
+
(json.output?.video_url ? [{ url: json.output.video_url }] : []);
|
|
1300
1628
|
return {
|
|
1301
1629
|
videos: items.map((item) => normalizeGeneratedMedia(item, "video/mp4")),
|
|
1302
1630
|
operationName: json.output?.task_id ?? json.task_id,
|
|
@@ -1312,22 +1640,56 @@ class QwenRerankModelImpl {
|
|
|
1312
1640
|
modelId;
|
|
1313
1641
|
apiKey;
|
|
1314
1642
|
baseURL;
|
|
1643
|
+
taskBaseURL;
|
|
1315
1644
|
fetcher;
|
|
1316
1645
|
provider = "qwen";
|
|
1317
|
-
constructor(modelId, apiKey, baseURL, fetcher) {
|
|
1646
|
+
constructor(modelId, apiKey, baseURL, taskBaseURL, fetcher) {
|
|
1318
1647
|
this.modelId = modelId;
|
|
1319
1648
|
this.apiKey = apiKey;
|
|
1320
1649
|
this.baseURL = baseURL;
|
|
1650
|
+
this.taskBaseURL = taskBaseURL;
|
|
1321
1651
|
this.fetcher = fetcher;
|
|
1322
1652
|
}
|
|
1323
1653
|
async rerank(input) {
|
|
1324
1654
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1655
|
+
const isCompatibleRerank = /^qwen3-rerank(?:$|-)/i.test(this.modelId);
|
|
1656
|
+
if (isCompatibleRerank &&
|
|
1657
|
+
(typeof input.query !== "string" || input.documents.some((document) => typeof document !== "string"))) {
|
|
1658
|
+
throw new UnsupportedFeatureError("qwen3-rerank accepts text queries and text documents only.");
|
|
1659
|
+
}
|
|
1660
|
+
const toRerankContent = (value) => {
|
|
1661
|
+
if (typeof value === "string") {
|
|
1662
|
+
return /^qwen3-vl-rerank(?:$|-)/i.test(this.modelId) ? { text: value } : value;
|
|
1663
|
+
}
|
|
1664
|
+
return toMultimodalEmbeddingContent(value);
|
|
1665
|
+
};
|
|
1325
1666
|
try {
|
|
1326
|
-
const response = await withRetry(() => this.fetcher(
|
|
1667
|
+
const response = await withRetry(() => this.fetcher(isCompatibleRerank
|
|
1668
|
+
? `${this.baseURL}/reranks`
|
|
1669
|
+
: `${this.taskBaseURL}/services/rerank/text-rerank/text-rerank`, {
|
|
1327
1670
|
method: "POST",
|
|
1328
1671
|
headers: jsonHeaders(this.apiKey),
|
|
1329
1672
|
signal,
|
|
1330
|
-
body: JSON.stringify(
|
|
1673
|
+
body: JSON.stringify(isCompatibleRerank
|
|
1674
|
+
? {
|
|
1675
|
+
...input.providerOptions,
|
|
1676
|
+
model: this.modelId,
|
|
1677
|
+
query: input.query,
|
|
1678
|
+
documents: input.documents,
|
|
1679
|
+
top_n: input.topN
|
|
1680
|
+
}
|
|
1681
|
+
: {
|
|
1682
|
+
model: this.modelId,
|
|
1683
|
+
input: {
|
|
1684
|
+
query: toRerankContent(input.query),
|
|
1685
|
+
documents: input.documents.map(toRerankContent)
|
|
1686
|
+
},
|
|
1687
|
+
parameters: {
|
|
1688
|
+
return_documents: true,
|
|
1689
|
+
...input.providerOptions,
|
|
1690
|
+
top_n: input.topN
|
|
1691
|
+
}
|
|
1692
|
+
})
|
|
1331
1693
|
}), input);
|
|
1332
1694
|
const json = await parseJson(response);
|
|
1333
1695
|
return {
|
|
@@ -1357,7 +1719,8 @@ class QwenTasksClientImpl {
|
|
|
1357
1719
|
async get(input) {
|
|
1358
1720
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1359
1721
|
try {
|
|
1360
|
-
const
|
|
1722
|
+
const taskId = encodeQwenResourceId(input.name, "task");
|
|
1723
|
+
const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/tasks/${taskId}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
1361
1724
|
return normalizeOperation(await parseJson(response));
|
|
1362
1725
|
}
|
|
1363
1726
|
finally {
|
|
@@ -1367,7 +1730,8 @@ class QwenTasksClientImpl {
|
|
|
1367
1730
|
async cancel(input) {
|
|
1368
1731
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1369
1732
|
try {
|
|
1370
|
-
const
|
|
1733
|
+
const taskId = encodeQwenResourceId(input.name, "task");
|
|
1734
|
+
const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/tasks/${taskId}/cancel`, { method: "POST", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
1371
1735
|
return normalizeOperation(await parseJson(response));
|
|
1372
1736
|
}
|
|
1373
1737
|
finally {
|
|
@@ -1377,23 +1741,103 @@ class QwenTasksClientImpl {
|
|
|
1377
1741
|
}
|
|
1378
1742
|
const parseRealtimeEvent = (payload) => {
|
|
1379
1743
|
const type = String(payload.type ?? "");
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
if (type
|
|
1384
|
-
return [{ type: "realtime-
|
|
1744
|
+
const metadata = payload;
|
|
1745
|
+
const itemId = typeof payload.item_id === "string" ? payload.item_id : undefined;
|
|
1746
|
+
const responseId = typeof payload.response_id === "string" ? payload.response_id : undefined;
|
|
1747
|
+
if (type === "response.text.delta" && typeof payload.delta === "string") {
|
|
1748
|
+
return [{ type: "realtime-text-delta", textDelta: payload.delta, itemId, responseId, providerMetadata: metadata }];
|
|
1749
|
+
}
|
|
1750
|
+
if (type === "response.audio.delta" && typeof payload.delta === "string") {
|
|
1751
|
+
return [{
|
|
1752
|
+
type: "realtime-audio-output",
|
|
1753
|
+
audio: Uint8Array.from(Buffer.from(payload.delta, "base64")),
|
|
1754
|
+
mediaType: "audio/pcm",
|
|
1755
|
+
sampleRateHz: 24_000,
|
|
1756
|
+
channels: 1,
|
|
1757
|
+
itemId,
|
|
1758
|
+
responseId,
|
|
1759
|
+
providerMetadata: metadata
|
|
1760
|
+
}];
|
|
1761
|
+
}
|
|
1762
|
+
if (type === "response.audio_transcript.delta" && typeof payload.delta === "string") {
|
|
1763
|
+
return [{ type: "realtime-transcript", text: payload.delta, role: "assistant", isFinal: false, itemId, responseId, providerMetadata: metadata }];
|
|
1764
|
+
}
|
|
1765
|
+
if (type === "response.audio_transcript.done" && typeof payload.transcript === "string") {
|
|
1766
|
+
return [{ type: "realtime-transcript", text: payload.transcript, role: "assistant", isFinal: true, itemId, responseId, providerMetadata: metadata }];
|
|
1767
|
+
}
|
|
1768
|
+
if (type === "conversation.item.input_audio_transcription.delta") {
|
|
1769
|
+
const text = `${typeof payload.text === "string" ? payload.text : ""}${typeof payload.stash === "string" ? payload.stash : ""}`;
|
|
1770
|
+
return [{ type: "realtime-transcript", text, role: "user", isFinal: false, itemId, providerMetadata: metadata }];
|
|
1771
|
+
}
|
|
1772
|
+
if (type === "conversation.item.input_audio_transcription.completed" && typeof payload.transcript === "string") {
|
|
1773
|
+
return [{ type: "realtime-transcript", text: payload.transcript, role: "user", isFinal: true, itemId, providerMetadata: metadata }];
|
|
1774
|
+
}
|
|
1775
|
+
if (type === "response.function_call_arguments.done") {
|
|
1776
|
+
return [{
|
|
1777
|
+
type: "realtime-tool-call",
|
|
1778
|
+
toolCall: {
|
|
1779
|
+
id: String(payload.call_id ?? payload.item_id ?? "qwen-tool-call"),
|
|
1780
|
+
name: String(payload.name ?? ""),
|
|
1781
|
+
input: JSON.parse(typeof payload.arguments === "string" ? payload.arguments : "{}")
|
|
1782
|
+
}
|
|
1783
|
+
}];
|
|
1385
1784
|
}
|
|
1386
|
-
if (type
|
|
1387
|
-
|
|
1785
|
+
if (type === "response.done") {
|
|
1786
|
+
const response = payload.response;
|
|
1787
|
+
return [{ type: "realtime-response-complete", reason: typeof response?.status === "string" ? response.status : undefined, providerMetadata: metadata }];
|
|
1388
1788
|
}
|
|
1389
|
-
if (type
|
|
1390
|
-
return [{ type: "realtime-
|
|
1789
|
+
if (type === "session.finished") {
|
|
1790
|
+
return [{ type: "realtime-end", reason: "finished", providerMetadata: metadata }];
|
|
1391
1791
|
}
|
|
1392
|
-
if (type.
|
|
1393
|
-
|
|
1792
|
+
if (type === "error" || type.endsWith(".failed")) {
|
|
1793
|
+
const error = payload.error;
|
|
1794
|
+
return [{
|
|
1795
|
+
type: "realtime-error",
|
|
1796
|
+
message: String(error?.message ?? payload.message ?? "Qwen realtime error"),
|
|
1797
|
+
providerMetadata: metadata
|
|
1798
|
+
}];
|
|
1394
1799
|
}
|
|
1395
1800
|
return [];
|
|
1396
1801
|
};
|
|
1802
|
+
const mapRealtimeTools = (config) => {
|
|
1803
|
+
const tools = toToolSet(config.tools);
|
|
1804
|
+
if (!tools) {
|
|
1805
|
+
return undefined;
|
|
1806
|
+
}
|
|
1807
|
+
return Object.values(tools).map((tool) => {
|
|
1808
|
+
if (!isCallableToolDefinition(tool)) {
|
|
1809
|
+
throw new UnsupportedFeatureError("Qwen realtime supports callable function tools only.");
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
type: "function",
|
|
1813
|
+
function: {
|
|
1814
|
+
name: tool.name,
|
|
1815
|
+
description: tool.description,
|
|
1816
|
+
parameters: toJSONSchema(tool.schema)
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
});
|
|
1820
|
+
};
|
|
1821
|
+
const mapRealtimeSession = (config) => ({
|
|
1822
|
+
...(config.providerOptions ?? {}),
|
|
1823
|
+
instructions: config.instructions,
|
|
1824
|
+
voice: config.voice,
|
|
1825
|
+
input_audio_format: "pcm",
|
|
1826
|
+
output_audio_format: "pcm",
|
|
1827
|
+
turn_detection: config.turnDetection,
|
|
1828
|
+
input_audio_transcription: typeof config.inputAudioTranscription === "object"
|
|
1829
|
+
? config.inputAudioTranscription
|
|
1830
|
+
: config.inputAudioTranscription
|
|
1831
|
+
? {}
|
|
1832
|
+
: undefined,
|
|
1833
|
+
tools: mapRealtimeTools(config)
|
|
1834
|
+
});
|
|
1835
|
+
const encodeRealtimeData = (data) => {
|
|
1836
|
+
if (typeof data === "string") {
|
|
1837
|
+
return data.startsWith("data:") ? data.slice(data.indexOf(",") + 1) : data;
|
|
1838
|
+
}
|
|
1839
|
+
return Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data)).toString("base64");
|
|
1840
|
+
};
|
|
1397
1841
|
class QwenRealtimeModel {
|
|
1398
1842
|
modelId;
|
|
1399
1843
|
apiKey;
|
|
@@ -1418,13 +1862,21 @@ class QwenRealtimeModel {
|
|
|
1418
1862
|
connection,
|
|
1419
1863
|
callbacks: {
|
|
1420
1864
|
parseEvent: parseRealtimeEvent,
|
|
1421
|
-
buildInitialPayloads: (value) => [{ type: "session.update", session:
|
|
1422
|
-
buildAudioPayloads: (frame) => [{ type: "input_audio_buffer.append", audio:
|
|
1423
|
-
buildMediaPayloads: (frame) =>
|
|
1865
|
+
buildInitialPayloads: (value) => [{ type: "session.update", session: mapRealtimeSession(value) }],
|
|
1866
|
+
buildAudioPayloads: (frame) => [{ type: "input_audio_buffer.append", audio: encodeRealtimeData(frame.data) }],
|
|
1867
|
+
buildMediaPayloads: (frame) => {
|
|
1868
|
+
if (!/^image\/(jpeg|jpg)$/i.test(frame.mediaType)) {
|
|
1869
|
+
throw new UnsupportedFeatureError("Qwen realtime media frames must be JPEG images.");
|
|
1870
|
+
}
|
|
1871
|
+
return [{ type: "input_image_buffer.append", image: encodeRealtimeData(frame.data) }];
|
|
1872
|
+
},
|
|
1424
1873
|
buildTextPayloads: (text) => [{ type: "conversation.item.create", item: { type: "message", role: "user", content: [{ type: "input_text", text }] } }, { type: "response.create" }],
|
|
1425
|
-
buildToolResultPayloads: (result) => [
|
|
1426
|
-
|
|
1427
|
-
|
|
1874
|
+
buildToolResultPayloads: (result) => [
|
|
1875
|
+
{ type: "conversation.item.create", item: { type: "function_call_output", call_id: result.toolCallId, output: JSON.stringify(result.isError ? result.error : result.output ?? null) } },
|
|
1876
|
+
{ type: "response.create" }
|
|
1877
|
+
],
|
|
1878
|
+
buildUpdatePayloads: (value) => [{ type: "session.update", session: mapRealtimeSession(value) }],
|
|
1879
|
+
buildClosePayloads: () => []
|
|
1428
1880
|
}
|
|
1429
1881
|
});
|
|
1430
1882
|
await session.initialize();
|
|
@@ -1436,25 +1888,33 @@ export const createQwen = (options = {}) => {
|
|
|
1436
1888
|
if (!apiKey) {
|
|
1437
1889
|
throw new ConfigurationError("Missing Qwen API key.");
|
|
1438
1890
|
}
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1891
|
+
const workspaceURLs = options.workspaceId
|
|
1892
|
+
? qwenWorkspaceURLs(options.workspaceId, options.region ?? "singapore")
|
|
1893
|
+
: undefined;
|
|
1894
|
+
const baseURL = options.baseURL ?? workspaceURLs?.baseURL ?? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
|
1895
|
+
const taskBaseURL = options.taskBaseURL ?? workspaceURLs?.taskBaseURL ?? qwenTaskBaseURLFrom(baseURL);
|
|
1896
|
+
const realtimeURL = options.realtimeURL ??
|
|
1897
|
+
workspaceURLs?.realtimeURL ??
|
|
1898
|
+
"wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime";
|
|
1442
1899
|
const fetcher = options.fetch ?? globalThis.fetch;
|
|
1443
1900
|
const responseLimits = resolveAudioResponseLimits(options.responseLimits);
|
|
1901
|
+
const speechAudioMaxRedirects = options.speechAudioMaxRedirects ?? 3;
|
|
1902
|
+
if (!Number.isInteger(speechAudioMaxRedirects) || speechAudioMaxRedirects < 0 || speechAudioMaxRedirects > 10) {
|
|
1903
|
+
throw new ConfigurationError("Qwen speechAudioMaxRedirects must be an integer between 0 and 10.");
|
|
1904
|
+
}
|
|
1444
1905
|
return createProviderAdapter({
|
|
1445
1906
|
name: "qwen",
|
|
1446
1907
|
languageModel: (modelId) => new QwenLanguageModel(modelId, apiKey, baseURL, fetcher),
|
|
1447
1908
|
embeddingModel: (modelId) => new QwenEmbeddingModel(modelId, apiKey, baseURL, fetcher),
|
|
1448
1909
|
transcriptionModel: (modelId) => new QwenTranscriptionModel(modelId, apiKey, baseURL, fetcher, responseLimits),
|
|
1449
|
-
speechModel: (modelId) => new QwenSpeechModel(modelId, apiKey,
|
|
1450
|
-
imageGenerationModel: (modelId) => new QwenImageGenerationModel(modelId, apiKey,
|
|
1910
|
+
speechModel: (modelId) => new QwenSpeechModel(modelId, apiKey, taskBaseURL, fetcher, responseLimits, options.speechAudioURLValidator, speechAudioMaxRedirects),
|
|
1911
|
+
imageGenerationModel: (modelId) => new QwenImageGenerationModel(modelId, apiKey, taskBaseURL, fetcher),
|
|
1451
1912
|
videoGenerationModel: (modelId) => new QwenVideoGenerationModel(modelId, apiKey, taskBaseURL, fetcher),
|
|
1452
1913
|
realtimeModel: (modelId) => new QwenRealtimeModel(modelId, apiKey, realtimeURL, options.realtimeConnectionFactory),
|
|
1453
1914
|
files: new QwenFilesClient(apiKey, baseURL, fetcher),
|
|
1454
|
-
fileSearchStores: new QwenFileSearchStoresClient(apiKey, baseURL, fetcher),
|
|
1455
1915
|
batches: new QwenBatchesClient(apiKey, baseURL, fetcher),
|
|
1456
|
-
rerankModel: (modelId) => new QwenRerankModelImpl(modelId, apiKey, baseURL, fetcher),
|
|
1457
|
-
multimodalEmbeddingModel: (modelId) => new
|
|
1916
|
+
rerankModel: (modelId) => new QwenRerankModelImpl(modelId, apiKey, baseURL, taskBaseURL, fetcher),
|
|
1917
|
+
multimodalEmbeddingModel: (modelId) => new QwenMultimodalEmbeddingModelImpl(modelId, apiKey, taskBaseURL, fetcher),
|
|
1458
1918
|
tasks: new QwenTasksClientImpl(apiKey, taskBaseURL, fetcher),
|
|
1459
1919
|
rawFetch: fetcher
|
|
1460
1920
|
});
|