@zhivex-ai/qwen 0.7.7 → 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 +140 -37
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +801 -278
- 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, ProviderHTTPError, 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
|
|
@@ -159,33 +282,42 @@ const jsonHeaders = (apiKey) => ({
|
|
|
159
282
|
"content-type": "application/json",
|
|
160
283
|
authorization: `Bearer ${apiKey}`
|
|
161
284
|
});
|
|
162
|
-
const parseJson = async (response) => {
|
|
285
|
+
const parseJson = async (response, options = {}) => {
|
|
163
286
|
if (!response.ok) {
|
|
164
|
-
const body = await response.
|
|
287
|
+
const body = await readErrorBodyWithLimit(response, options.errorBodyBytes);
|
|
165
288
|
throw new ProviderHTTPError(`Qwen request failed with status ${response.status}.`, response.status, {
|
|
166
289
|
responseBody: body
|
|
167
290
|
});
|
|
168
291
|
}
|
|
169
|
-
return
|
|
292
|
+
return options.maxBytes
|
|
293
|
+
? readJsonWithLimit(response, {
|
|
294
|
+
maxBytes: options.maxBytes,
|
|
295
|
+
provider: "qwen",
|
|
296
|
+
endpoint: options.endpoint,
|
|
297
|
+
abort: options.abort
|
|
298
|
+
})
|
|
299
|
+
: response.json();
|
|
170
300
|
};
|
|
171
301
|
const mapContentParts = (message) => {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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("");
|
|
176
308
|
}
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
type: "text",
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
});
|
|
189
321
|
};
|
|
190
322
|
const mapMessages = (messages) => messages.map((message) => {
|
|
191
323
|
if (message.role === "tool") {
|
|
@@ -257,7 +389,7 @@ const mapResponsesTools = (tools) => tools
|
|
|
257
389
|
};
|
|
258
390
|
})
|
|
259
391
|
: undefined;
|
|
260
|
-
const
|
|
392
|
+
const mapChatToolChoice = (toolChoice) => {
|
|
261
393
|
if (!toolChoice) {
|
|
262
394
|
return undefined;
|
|
263
395
|
}
|
|
@@ -271,21 +403,48 @@ const mapToolChoice = (toolChoice) => {
|
|
|
271
403
|
}
|
|
272
404
|
};
|
|
273
405
|
};
|
|
274
|
-
const
|
|
275
|
-
if (!
|
|
406
|
+
const mapResponsesToolChoice = (toolChoice, tools) => {
|
|
407
|
+
if (!toolChoice) {
|
|
276
408
|
return undefined;
|
|
277
409
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
name: input.structuredOutput.name ?? "response",
|
|
282
|
-
strict: true,
|
|
283
|
-
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.');
|
|
284
413
|
}
|
|
414
|
+
return toolChoice;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
type: "allowed_tools",
|
|
418
|
+
mode: "required",
|
|
419
|
+
tools: [{ type: "function", name: toolChoice.toolName }]
|
|
285
420
|
};
|
|
286
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
|
+
};
|
|
287
446
|
const hasPreservedReasoning = (messages) => messages.some((message) => message.role === "assistant" && reasoningContentFromMessage(message));
|
|
288
|
-
const
|
|
447
|
+
const mapChatReasoning = (input) => {
|
|
289
448
|
if (!input.reasoning) {
|
|
290
449
|
return undefined;
|
|
291
450
|
}
|
|
@@ -297,6 +456,63 @@ const mapReasoning = (input) => {
|
|
|
297
456
|
...(hasPreservedReasoning(input.messages) ? { preserve_thinking: true } : {})
|
|
298
457
|
};
|
|
299
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
|
+
};
|
|
300
516
|
const parseAssistantMessage = (message) => ({
|
|
301
517
|
role: "assistant",
|
|
302
518
|
parts: [
|
|
@@ -359,6 +575,16 @@ const toResponsesInput = (messages) => {
|
|
|
359
575
|
case "image":
|
|
360
576
|
content.push({ type: "input_image", image_url: part.image });
|
|
361
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;
|
|
362
588
|
case "tool-call":
|
|
363
589
|
if (message.role === "assistant") {
|
|
364
590
|
input.push({
|
|
@@ -369,6 +595,15 @@ const toResponsesInput = (messages) => {
|
|
|
369
595
|
});
|
|
370
596
|
}
|
|
371
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;
|
|
372
607
|
}
|
|
373
608
|
}
|
|
374
609
|
if (content.length) {
|
|
@@ -546,17 +781,18 @@ const streamResponses = async function* (response) {
|
|
|
546
781
|
}
|
|
547
782
|
if (type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
|
|
548
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
|
+
}
|
|
549
791
|
yield {
|
|
550
792
|
type: "finish",
|
|
551
793
|
finishReason: normalizeResponsesFinishReason(responseData.status, sawToolCalls),
|
|
552
794
|
providerFinishReason: responseData.status,
|
|
553
|
-
usage: responseData.usage
|
|
554
|
-
? {
|
|
555
|
-
inputTokens: responseData.usage.input_tokens,
|
|
556
|
-
outputTokens: responseData.usage.output_tokens,
|
|
557
|
-
totalTokens: responseData.usage.total_tokens
|
|
558
|
-
}
|
|
559
|
-
: undefined
|
|
795
|
+
usage: mapResponsesUsage(responseData.usage)
|
|
560
796
|
};
|
|
561
797
|
}
|
|
562
798
|
}
|
|
@@ -576,14 +812,17 @@ class QwenLanguageModel {
|
|
|
576
812
|
this.capabilities = qwenLanguageCapabilities(modelId);
|
|
577
813
|
}
|
|
578
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
|
+
}
|
|
579
818
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
580
819
|
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
581
|
-
const apiMode = providerOptions.apiMode ?? "
|
|
820
|
+
const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input);
|
|
582
821
|
delete providerOptions.apiMode;
|
|
583
822
|
try {
|
|
584
823
|
if (apiMode === "responses") {
|
|
585
824
|
const previousResponse = getProviderResponseId(input.messages);
|
|
586
|
-
const responseProviderOptions =
|
|
825
|
+
const responseProviderOptions = stripResponsesRequestOptions(providerOptions);
|
|
587
826
|
const messages = previousResponse && previousResponse.index < input.messages.length - 1
|
|
588
827
|
? input.messages.slice(previousResponse.index + 1)
|
|
589
828
|
: input.messages;
|
|
@@ -596,12 +835,10 @@ class QwenLanguageModel {
|
|
|
596
835
|
model: this.modelId,
|
|
597
836
|
...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
|
|
598
837
|
...(messages.length ? { input: toResponsesInput(messages) } : {}),
|
|
599
|
-
...(input.structuredOutput?.mode === "native" ? { response_format: mapStructuredOutput(input) } : {}),
|
|
600
838
|
tools: mapResponsesTools(input.tools),
|
|
601
|
-
tool_choice:
|
|
839
|
+
tool_choice: mapResponsesToolChoice(input.toolChoice, input.tools),
|
|
602
840
|
temperature: input.temperature,
|
|
603
|
-
|
|
604
|
-
...mapReasoning(input),
|
|
841
|
+
...mapResponsesReasoning(input),
|
|
605
842
|
stream: false
|
|
606
843
|
})
|
|
607
844
|
}), input);
|
|
@@ -616,29 +853,25 @@ class QwenLanguageModel {
|
|
|
616
853
|
.join(""),
|
|
617
854
|
finishReason: normalizeResponsesFinishReason(json.status, hasToolCalls),
|
|
618
855
|
providerFinishReason: json.status,
|
|
619
|
-
usage:
|
|
620
|
-
inputTokens: json.usage?.input_tokens,
|
|
621
|
-
outputTokens: json.usage?.output_tokens,
|
|
622
|
-
totalTokens: json.usage?.total_tokens
|
|
623
|
-
},
|
|
856
|
+
usage: mapResponsesUsage(json.usage),
|
|
624
857
|
rawResponse: json
|
|
625
858
|
};
|
|
626
859
|
}
|
|
627
860
|
const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
|
|
628
861
|
method: "POST",
|
|
629
|
-
headers:
|
|
862
|
+
headers: providerHeaders(this.apiKey, providerOptions),
|
|
630
863
|
signal,
|
|
631
864
|
body: JSON.stringify({
|
|
632
|
-
...providerOptions,
|
|
865
|
+
...stripHeaderOptions(providerOptions),
|
|
633
866
|
model: this.modelId,
|
|
634
|
-
messages: mapMessages(input
|
|
867
|
+
messages: mapMessages(withStructuredOutputMessages(input)),
|
|
635
868
|
tools: mapChatTools(input.tools),
|
|
636
|
-
tool_choice:
|
|
869
|
+
tool_choice: mapChatToolChoice(input.toolChoice),
|
|
637
870
|
response_format: mapStructuredOutput(input),
|
|
638
871
|
temperature: input.temperature,
|
|
639
872
|
max_tokens: input.maxTokens,
|
|
640
873
|
stream: false,
|
|
641
|
-
...
|
|
874
|
+
...mapChatReasoning(input)
|
|
642
875
|
})
|
|
643
876
|
}), input);
|
|
644
877
|
const json = await parseJson(response);
|
|
@@ -653,11 +886,7 @@ class QwenLanguageModel {
|
|
|
653
886
|
.join(""),
|
|
654
887
|
finishReason: normalizeFinishReason(choice?.finish_reason),
|
|
655
888
|
providerFinishReason: choice?.finish_reason,
|
|
656
|
-
usage:
|
|
657
|
-
inputTokens: json.usage?.prompt_tokens,
|
|
658
|
-
outputTokens: json.usage?.completion_tokens,
|
|
659
|
-
totalTokens: json.usage?.total_tokens
|
|
660
|
-
},
|
|
889
|
+
usage: mapChatUsage(json.usage),
|
|
661
890
|
rawResponse: json
|
|
662
891
|
};
|
|
663
892
|
}
|
|
@@ -668,10 +897,14 @@ class QwenLanguageModel {
|
|
|
668
897
|
async stream(input) {
|
|
669
898
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
670
899
|
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
671
|
-
const apiMode = providerOptions.apiMode ?? "
|
|
900
|
+
const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input);
|
|
672
901
|
delete providerOptions.apiMode;
|
|
673
902
|
if (apiMode === "responses") {
|
|
674
|
-
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;
|
|
675
908
|
const response = await withRetry(() => this.fetcher(`${this.baseURL}/responses`, {
|
|
676
909
|
method: "POST",
|
|
677
910
|
headers: providerHeaders(this.apiKey, providerOptions),
|
|
@@ -679,13 +912,12 @@ class QwenLanguageModel {
|
|
|
679
912
|
body: JSON.stringify({
|
|
680
913
|
...responseProviderOptions,
|
|
681
914
|
model: this.modelId,
|
|
682
|
-
|
|
683
|
-
...(
|
|
915
|
+
...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
|
|
916
|
+
...(messages.length ? { input: toResponsesInput(messages) } : {}),
|
|
684
917
|
tools: mapResponsesTools(input.tools),
|
|
685
|
-
tool_choice:
|
|
918
|
+
tool_choice: mapResponsesToolChoice(input.toolChoice, input.tools),
|
|
686
919
|
temperature: input.temperature,
|
|
687
|
-
|
|
688
|
-
...mapReasoning(input),
|
|
920
|
+
...mapResponsesReasoning(input),
|
|
689
921
|
stream: true
|
|
690
922
|
})
|
|
691
923
|
}), input);
|
|
@@ -700,20 +932,21 @@ class QwenLanguageModel {
|
|
|
700
932
|
}
|
|
701
933
|
const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
|
|
702
934
|
method: "POST",
|
|
703
|
-
headers:
|
|
935
|
+
headers: providerHeaders(this.apiKey, providerOptions),
|
|
704
936
|
signal,
|
|
705
937
|
body: JSON.stringify({
|
|
706
|
-
...providerOptions,
|
|
938
|
+
...stripHeaderOptions(providerOptions),
|
|
707
939
|
model: this.modelId,
|
|
708
|
-
|
|
940
|
+
modalities: isQwenOmniLanguageModel(this.modelId) ? providerOptions.modalities ?? ["text"] : undefined,
|
|
941
|
+
messages: mapMessages(withStructuredOutputMessages(input)),
|
|
709
942
|
tools: mapChatTools(input.tools),
|
|
710
|
-
tool_choice:
|
|
943
|
+
tool_choice: mapChatToolChoice(input.toolChoice),
|
|
711
944
|
response_format: mapStructuredOutput(input),
|
|
712
945
|
temperature: input.temperature,
|
|
713
946
|
max_tokens: input.maxTokens,
|
|
714
947
|
stream: true,
|
|
715
948
|
stream_options: { include_usage: true },
|
|
716
|
-
...
|
|
949
|
+
...mapChatReasoning(input)
|
|
717
950
|
})
|
|
718
951
|
}), input);
|
|
719
952
|
return (async function* () {
|
|
@@ -740,37 +973,40 @@ class QwenLanguageModel {
|
|
|
740
973
|
yield { type: "text-delta", textDelta: delta.content };
|
|
741
974
|
}
|
|
742
975
|
for (const toolCall of delta?.tool_calls ?? []) {
|
|
743
|
-
const
|
|
744
|
-
const existing = toolBuffers.get(
|
|
976
|
+
const index = Number(toolCall.index ?? 0);
|
|
977
|
+
const existing = toolBuffers.get(index) ?? {
|
|
978
|
+
id: toolCall.id ?? `${index}`,
|
|
745
979
|
name: toolCall.function?.name ?? "",
|
|
746
|
-
args: ""
|
|
980
|
+
args: "",
|
|
981
|
+
emitted: false
|
|
747
982
|
};
|
|
983
|
+
existing.id = toolCall.id ?? existing.id;
|
|
748
984
|
existing.name ||= toolCall.function?.name ?? "";
|
|
749
985
|
existing.args += toolCall.function?.arguments ?? "";
|
|
750
|
-
toolBuffers.set(
|
|
751
|
-
if (choice?.finish_reason === "tool_calls") {
|
|
752
|
-
yield {
|
|
753
|
-
type: "tool-call",
|
|
754
|
-
toolCall: {
|
|
755
|
-
id,
|
|
756
|
-
name: existing.name,
|
|
757
|
-
input: JSON.parse(existing.args || "{}")
|
|
758
|
-
}
|
|
759
|
-
};
|
|
760
|
-
}
|
|
986
|
+
toolBuffers.set(index, existing);
|
|
761
987
|
}
|
|
762
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
|
+
}
|
|
763
1005
|
yield {
|
|
764
1006
|
type: "finish",
|
|
765
1007
|
finishReason: normalizeFinishReason(choice.finish_reason),
|
|
766
1008
|
providerFinishReason: choice.finish_reason,
|
|
767
|
-
usage: json.usage
|
|
768
|
-
? {
|
|
769
|
-
inputTokens: json.usage.prompt_tokens,
|
|
770
|
-
outputTokens: json.usage.completion_tokens,
|
|
771
|
-
totalTokens: json.usage.total_tokens
|
|
772
|
-
}
|
|
773
|
-
: undefined
|
|
1009
|
+
usage: mapChatUsage(json.usage)
|
|
774
1010
|
};
|
|
775
1011
|
}
|
|
776
1012
|
}
|
|
@@ -827,6 +1063,67 @@ class QwenEmbeddingModel {
|
|
|
827
1063
|
}
|
|
828
1064
|
}
|
|
829
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
|
+
}
|
|
830
1127
|
const normalizeUploadedFile = (json) => ({
|
|
831
1128
|
name: json.id ?? json.name ?? json.file_id ?? "",
|
|
832
1129
|
uri: json.url ?? json.uri,
|
|
@@ -837,14 +1134,6 @@ const normalizeUploadedFile = (json) => ({
|
|
|
837
1134
|
rawResponse: json,
|
|
838
1135
|
providerMetadata: json
|
|
839
1136
|
});
|
|
840
|
-
const normalizeFileSearchStore = (json) => ({
|
|
841
|
-
name: json.id ?? json.name ?? "",
|
|
842
|
-
displayName: json.name ?? json.display_name ?? json.displayName,
|
|
843
|
-
createTime: json.created_at ? String(json.created_at) : json.createTime,
|
|
844
|
-
updateTime: json.updated_at ? String(json.updated_at) : json.updateTime,
|
|
845
|
-
rawResponse: json,
|
|
846
|
-
providerMetadata: json
|
|
847
|
-
});
|
|
848
1137
|
const normalizeBatchJob = (json) => ({
|
|
849
1138
|
name: json.id ?? json.name ?? "",
|
|
850
1139
|
model: json.model,
|
|
@@ -856,10 +1145,13 @@ const normalizeBatchJob = (json) => ({
|
|
|
856
1145
|
providerMetadata: json
|
|
857
1146
|
});
|
|
858
1147
|
const normalizeOperation = (json) => ({
|
|
859
|
-
name: json.task_id ?? json.id ?? json.name ?? "",
|
|
1148
|
+
name: json.output?.task_id ?? json.task_id ?? json.id ?? json.name ?? "",
|
|
860
1149
|
done: json.output?.task_status ? ["SUCCEEDED", "FAILED", "CANCELED"].includes(json.output.task_status) : json.done,
|
|
861
1150
|
response: json.output ?? json.response,
|
|
862
|
-
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),
|
|
863
1155
|
metadata: json.usage ?? json.metadata,
|
|
864
1156
|
rawResponse: json
|
|
865
1157
|
});
|
|
@@ -893,7 +1185,8 @@ class QwenFilesClient {
|
|
|
893
1185
|
async get(input) {
|
|
894
1186
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
895
1187
|
try {
|
|
896
|
-
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);
|
|
897
1190
|
return normalizeUploadedFile(await parseJson(response));
|
|
898
1191
|
}
|
|
899
1192
|
finally {
|
|
@@ -918,88 +1211,8 @@ class QwenFilesClient {
|
|
|
918
1211
|
async delete(input) {
|
|
919
1212
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
920
1213
|
try {
|
|
921
|
-
const
|
|
922
|
-
const
|
|
923
|
-
return { name: input.name, rawResponse: json };
|
|
924
|
-
}
|
|
925
|
-
finally {
|
|
926
|
-
cleanup();
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
class QwenFileSearchStoresClient {
|
|
931
|
-
apiKey;
|
|
932
|
-
baseURL;
|
|
933
|
-
fetcher;
|
|
934
|
-
constructor(apiKey, baseURL, fetcher) {
|
|
935
|
-
this.apiKey = apiKey;
|
|
936
|
-
this.baseURL = baseURL;
|
|
937
|
-
this.fetcher = fetcher;
|
|
938
|
-
}
|
|
939
|
-
async create(input = {}) {
|
|
940
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
941
|
-
try {
|
|
942
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores`, {
|
|
943
|
-
method: "POST",
|
|
944
|
-
headers: jsonHeaders(this.apiKey),
|
|
945
|
-
signal,
|
|
946
|
-
body: JSON.stringify({ ...input.providerOptions, name: input.displayName })
|
|
947
|
-
}), input);
|
|
948
|
-
return normalizeFileSearchStore(await parseJson(response));
|
|
949
|
-
}
|
|
950
|
-
finally {
|
|
951
|
-
cleanup();
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
async upload(input) {
|
|
955
|
-
const files = new QwenFilesClient(this.apiKey, this.baseURL, this.fetcher);
|
|
956
|
-
const file = await files.upload(input);
|
|
957
|
-
return this.importFile({ ...input, fileName: file.name });
|
|
958
|
-
}
|
|
959
|
-
async importFile(input) {
|
|
960
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
961
|
-
try {
|
|
962
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores/${input.storeName}/files`, {
|
|
963
|
-
method: "POST",
|
|
964
|
-
headers: jsonHeaders(this.apiKey),
|
|
965
|
-
signal,
|
|
966
|
-
body: JSON.stringify({ ...input.providerOptions, file_id: input.fileName })
|
|
967
|
-
}), input);
|
|
968
|
-
return normalizeOperation(await parseJson(response));
|
|
969
|
-
}
|
|
970
|
-
finally {
|
|
971
|
-
cleanup();
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
async get(input) {
|
|
975
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
976
|
-
try {
|
|
977
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores/${input.name}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
|
|
978
|
-
return normalizeFileSearchStore(await parseJson(response));
|
|
979
|
-
}
|
|
980
|
-
finally {
|
|
981
|
-
cleanup();
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
async list(input = {}) {
|
|
985
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
986
|
-
try {
|
|
987
|
-
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);
|
|
988
|
-
const json = await parseJson(response);
|
|
989
|
-
return {
|
|
990
|
-
stores: (json.data ?? json.file_search_stores ?? json.fileSearchStores ?? []).map(normalizeFileSearchStore),
|
|
991
|
-
nextPageToken: json.next ?? json.nextPageToken,
|
|
992
|
-
rawResponse: json
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
finally {
|
|
996
|
-
cleanup();
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
async delete(input) {
|
|
1000
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1001
|
-
try {
|
|
1002
|
-
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);
|
|
1003
1216
|
const json = await parseJson(response);
|
|
1004
1217
|
return { name: input.name, rawResponse: json };
|
|
1005
1218
|
}
|
|
@@ -1018,6 +1231,9 @@ class QwenBatchesClient {
|
|
|
1018
1231
|
this.fetcher = fetcher;
|
|
1019
1232
|
}
|
|
1020
1233
|
async create(input) {
|
|
1234
|
+
if (!input.fileName) {
|
|
1235
|
+
throw new ConfigurationError("Qwen batch creation requires a JSONL fileName uploaded with purpose \"batch\".");
|
|
1236
|
+
}
|
|
1021
1237
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1022
1238
|
try {
|
|
1023
1239
|
const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches`, {
|
|
@@ -1029,9 +1245,7 @@ class QwenBatchesClient {
|
|
|
1029
1245
|
input_file_id: input.fileName,
|
|
1030
1246
|
endpoint: input.providerOptions?.endpoint ?? "/v1/chat/completions",
|
|
1031
1247
|
completion_window: input.providerOptions?.completion_window ?? "24h",
|
|
1032
|
-
metadata: input.displayName ? { displayName: input.displayName } : undefined
|
|
1033
|
-
model: input.modelId,
|
|
1034
|
-
requests: input.requests
|
|
1248
|
+
metadata: input.displayName ? { displayName: input.displayName } : undefined
|
|
1035
1249
|
})
|
|
1036
1250
|
}), input);
|
|
1037
1251
|
return normalizeBatchJob(await parseJson(response));
|
|
@@ -1043,7 +1257,8 @@ class QwenBatchesClient {
|
|
|
1043
1257
|
async get(input) {
|
|
1044
1258
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1045
1259
|
try {
|
|
1046
|
-
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);
|
|
1047
1262
|
return normalizeBatchJob(await parseJson(response));
|
|
1048
1263
|
}
|
|
1049
1264
|
finally {
|
|
@@ -1064,7 +1279,8 @@ class QwenBatchesClient {
|
|
|
1064
1279
|
async cancel(input) {
|
|
1065
1280
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1066
1281
|
try {
|
|
1067
|
-
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);
|
|
1068
1284
|
return normalizeBatchJob(await parseJson(response));
|
|
1069
1285
|
}
|
|
1070
1286
|
finally {
|
|
@@ -1074,7 +1290,8 @@ class QwenBatchesClient {
|
|
|
1074
1290
|
async delete(input) {
|
|
1075
1291
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1076
1292
|
try {
|
|
1077
|
-
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);
|
|
1078
1295
|
const json = await parseJson(response);
|
|
1079
1296
|
return { name: input.name, rawResponse: json };
|
|
1080
1297
|
}
|
|
@@ -1088,30 +1305,70 @@ class QwenTranscriptionModel {
|
|
|
1088
1305
|
apiKey;
|
|
1089
1306
|
baseURL;
|
|
1090
1307
|
fetcher;
|
|
1308
|
+
responseLimits;
|
|
1091
1309
|
provider = "qwen";
|
|
1092
1310
|
capabilities = transcriptionCapabilities;
|
|
1093
|
-
constructor(modelId, apiKey, baseURL, fetcher) {
|
|
1311
|
+
constructor(modelId, apiKey, baseURL, fetcher, responseLimits = resolveAudioResponseLimits()) {
|
|
1094
1312
|
this.modelId = modelId;
|
|
1095
1313
|
this.apiKey = apiKey;
|
|
1096
1314
|
this.baseURL = baseURL;
|
|
1097
1315
|
this.fetcher = fetcher;
|
|
1316
|
+
this.responseLimits = responseLimits;
|
|
1098
1317
|
}
|
|
1099
1318
|
async transcribe(input) {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
form.set("model", this.modelId);
|
|
1103
|
-
form.set("file", await createFile(input.audio.data, input.audio.mediaType, input.audio.filename ?? "audio"));
|
|
1104
|
-
if (input.prompt)
|
|
1105
|
-
form.set("prompt", input.prompt);
|
|
1106
|
-
if (input.language)
|
|
1107
|
-
form.set("language", input.language);
|
|
1108
|
-
for (const [key, value] of Object.entries(input.providerOptions ?? {})) {
|
|
1109
|
-
form.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
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.");
|
|
1110
1321
|
}
|
|
1322
|
+
const { signal, cleanup, abort } = withTimeoutSignal(input);
|
|
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;
|
|
1111
1331
|
try {
|
|
1112
|
-
const response = await withRetry(() => this.fetcher(`${this.baseURL}/
|
|
1113
|
-
|
|
1114
|
-
|
|
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);
|
|
1359
|
+
const json = await parseJson(response, {
|
|
1360
|
+
maxBytes: this.responseLimits.transcriptionBytes,
|
|
1361
|
+
errorBodyBytes: this.responseLimits.errorBodyBytes,
|
|
1362
|
+
endpoint: "chat/completions",
|
|
1363
|
+
abort
|
|
1364
|
+
});
|
|
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 };
|
|
1115
1372
|
}
|
|
1116
1373
|
finally {
|
|
1117
1374
|
cleanup();
|
|
@@ -1121,31 +1378,107 @@ class QwenTranscriptionModel {
|
|
|
1121
1378
|
class QwenSpeechModel {
|
|
1122
1379
|
modelId;
|
|
1123
1380
|
apiKey;
|
|
1124
|
-
|
|
1381
|
+
taskBaseURL;
|
|
1125
1382
|
fetcher;
|
|
1383
|
+
responseLimits;
|
|
1384
|
+
speechAudioURLValidator;
|
|
1385
|
+
speechAudioMaxRedirects;
|
|
1126
1386
|
provider = "qwen";
|
|
1127
1387
|
capabilities = speechCapabilities;
|
|
1128
|
-
constructor(modelId, apiKey,
|
|
1388
|
+
constructor(modelId, apiKey, taskBaseURL, fetcher, responseLimits = resolveAudioResponseLimits(), speechAudioURLValidator, speechAudioMaxRedirects = 3) {
|
|
1129
1389
|
this.modelId = modelId;
|
|
1130
1390
|
this.apiKey = apiKey;
|
|
1131
|
-
this.
|
|
1391
|
+
this.taskBaseURL = taskBaseURL;
|
|
1132
1392
|
this.fetcher = fetcher;
|
|
1393
|
+
this.responseLimits = responseLimits;
|
|
1394
|
+
this.speechAudioURLValidator = speechAudioURLValidator;
|
|
1395
|
+
this.speechAudioMaxRedirects = speechAudioMaxRedirects;
|
|
1133
1396
|
}
|
|
1134
1397
|
async generateSpeech(input) {
|
|
1135
|
-
const { signal, cleanup } = withTimeoutSignal(input);
|
|
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;
|
|
1136
1409
|
try {
|
|
1137
|
-
const response = await withRetry(() => this.fetcher(`${this.
|
|
1410
|
+
const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/multimodal-generation/generation`, {
|
|
1138
1411
|
method: "POST",
|
|
1139
1412
|
headers: jsonHeaders(this.apiKey),
|
|
1140
1413
|
signal,
|
|
1141
|
-
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
|
+
})
|
|
1142
1424
|
}), input);
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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) {
|
|
1450
|
+
const decodedAudio = decodeBase64WithLimit(String(audio), {
|
|
1451
|
+
maxBytes: this.responseLimits.speechBytes,
|
|
1452
|
+
provider: "qwen",
|
|
1453
|
+
endpoint: "multimodal-generation/generation",
|
|
1454
|
+
abort
|
|
1455
|
+
});
|
|
1456
|
+
return {
|
|
1457
|
+
audio: decodedAudio,
|
|
1458
|
+
mediaType: json.audio?.media_type ?? json.output?.audio?.media_type ?? "audio/pcm",
|
|
1459
|
+
rawResponse
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
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, {
|
|
1469
|
+
responseBody: body
|
|
1470
|
+
});
|
|
1147
1471
|
}
|
|
1148
|
-
return {
|
|
1472
|
+
return {
|
|
1473
|
+
audio: await readBodyWithLimit(audioResponse, {
|
|
1474
|
+
maxBytes: this.responseLimits.speechBytes,
|
|
1475
|
+
provider: "qwen",
|
|
1476
|
+
endpoint: "speech-audio-download",
|
|
1477
|
+
abort
|
|
1478
|
+
}),
|
|
1479
|
+
mediaType: audioResponse.headers.get("content-type") ?? json.output?.audio?.media_type ?? "audio/wav",
|
|
1480
|
+
rawResponse
|
|
1481
|
+
};
|
|
1149
1482
|
}
|
|
1150
1483
|
finally {
|
|
1151
1484
|
cleanup();
|
|
@@ -1153,7 +1486,7 @@ class QwenSpeechModel {
|
|
|
1153
1486
|
}
|
|
1154
1487
|
}
|
|
1155
1488
|
const normalizeGeneratedMedia = (item, fallbackMimeType) => ({
|
|
1156
|
-
uri: item.url ?? item.uri,
|
|
1489
|
+
uri: item.url ?? item.uri ?? item.image ?? item.video_url,
|
|
1157
1490
|
data: item.b64_json || item.base64 ? Uint8Array.from(Buffer.from(item.b64_json ?? item.base64, "base64")) : undefined,
|
|
1158
1491
|
mediaType: item.mime_type ?? item.mediaType ?? fallbackMimeType,
|
|
1159
1492
|
text: item.revised_prompt ?? item.text,
|
|
@@ -1162,39 +1495,67 @@ const normalizeGeneratedMedia = (item, fallbackMimeType) => ({
|
|
|
1162
1495
|
class QwenImageGenerationModel {
|
|
1163
1496
|
modelId;
|
|
1164
1497
|
apiKey;
|
|
1165
|
-
baseURL;
|
|
1166
1498
|
taskBaseURL;
|
|
1167
1499
|
fetcher;
|
|
1168
1500
|
provider = "qwen";
|
|
1169
1501
|
capabilities = imageGenerationCapabilities;
|
|
1170
|
-
constructor(modelId, apiKey,
|
|
1502
|
+
constructor(modelId, apiKey, taskBaseURL, fetcher) {
|
|
1171
1503
|
this.modelId = modelId;
|
|
1172
1504
|
this.apiKey = apiKey;
|
|
1173
|
-
this.baseURL = baseURL;
|
|
1174
1505
|
this.taskBaseURL = taskBaseURL;
|
|
1175
1506
|
this.fetcher = fetcher;
|
|
1176
1507
|
}
|
|
1177
1508
|
async generateImage(input) {
|
|
1178
1509
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1179
|
-
const
|
|
1510
|
+
const providerOptions = { ...(input.providerOptions ?? {}) };
|
|
1511
|
+
delete providerOptions.endpoint;
|
|
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
|
+
};
|
|
1180
1526
|
try {
|
|
1181
1527
|
const response = await withRetry(() => this.fetcher(endpoint, {
|
|
1182
1528
|
method: "POST",
|
|
1183
1529
|
headers: jsonHeaders(this.apiKey),
|
|
1184
1530
|
signal,
|
|
1185
1531
|
body: JSON.stringify({
|
|
1186
|
-
...input.providerOptions,
|
|
1187
1532
|
model: this.modelId,
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
+
}
|
|
1194
1551
|
})
|
|
1195
1552
|
}), input);
|
|
1196
1553
|
const json = await parseJson(response);
|
|
1197
|
-
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
|
+
[];
|
|
1198
1559
|
return {
|
|
1199
1560
|
images: data.map((item) => normalizeGeneratedMedia(item, input.outputMimeType ?? "image/png")),
|
|
1200
1561
|
text: json.output?.text,
|
|
@@ -1221,20 +1582,49 @@ class QwenVideoGenerationModel {
|
|
|
1221
1582
|
}
|
|
1222
1583
|
async generateVideo(input) {
|
|
1223
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);
|
|
1224
1597
|
try {
|
|
1225
|
-
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`, {
|
|
1226
1599
|
method: "POST",
|
|
1227
1600
|
headers: { ...jsonHeaders(this.apiKey), "X-DashScope-Async": "enable" },
|
|
1228
1601
|
signal,
|
|
1229
1602
|
body: JSON.stringify({
|
|
1230
|
-
...input.providerOptions,
|
|
1231
1603
|
model: this.modelId,
|
|
1232
|
-
input: {
|
|
1233
|
-
|
|
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
|
+
}
|
|
1234
1622
|
})
|
|
1235
1623
|
}), input);
|
|
1236
1624
|
const json = await parseJson(response);
|
|
1237
|
-
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 }] : []);
|
|
1238
1628
|
return {
|
|
1239
1629
|
videos: items.map((item) => normalizeGeneratedMedia(item, "video/mp4")),
|
|
1240
1630
|
operationName: json.output?.task_id ?? json.task_id,
|
|
@@ -1250,22 +1640,56 @@ class QwenRerankModelImpl {
|
|
|
1250
1640
|
modelId;
|
|
1251
1641
|
apiKey;
|
|
1252
1642
|
baseURL;
|
|
1643
|
+
taskBaseURL;
|
|
1253
1644
|
fetcher;
|
|
1254
1645
|
provider = "qwen";
|
|
1255
|
-
constructor(modelId, apiKey, baseURL, fetcher) {
|
|
1646
|
+
constructor(modelId, apiKey, baseURL, taskBaseURL, fetcher) {
|
|
1256
1647
|
this.modelId = modelId;
|
|
1257
1648
|
this.apiKey = apiKey;
|
|
1258
1649
|
this.baseURL = baseURL;
|
|
1650
|
+
this.taskBaseURL = taskBaseURL;
|
|
1259
1651
|
this.fetcher = fetcher;
|
|
1260
1652
|
}
|
|
1261
1653
|
async rerank(input) {
|
|
1262
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
|
+
};
|
|
1263
1666
|
try {
|
|
1264
|
-
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`, {
|
|
1265
1670
|
method: "POST",
|
|
1266
1671
|
headers: jsonHeaders(this.apiKey),
|
|
1267
1672
|
signal,
|
|
1268
|
-
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
|
+
})
|
|
1269
1693
|
}), input);
|
|
1270
1694
|
const json = await parseJson(response);
|
|
1271
1695
|
return {
|
|
@@ -1295,7 +1719,8 @@ class QwenTasksClientImpl {
|
|
|
1295
1719
|
async get(input) {
|
|
1296
1720
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1297
1721
|
try {
|
|
1298
|
-
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);
|
|
1299
1724
|
return normalizeOperation(await parseJson(response));
|
|
1300
1725
|
}
|
|
1301
1726
|
finally {
|
|
@@ -1305,7 +1730,8 @@ class QwenTasksClientImpl {
|
|
|
1305
1730
|
async cancel(input) {
|
|
1306
1731
|
const { signal, cleanup } = withTimeoutSignal(input);
|
|
1307
1732
|
try {
|
|
1308
|
-
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);
|
|
1309
1735
|
return normalizeOperation(await parseJson(response));
|
|
1310
1736
|
}
|
|
1311
1737
|
finally {
|
|
@@ -1315,23 +1741,103 @@ class QwenTasksClientImpl {
|
|
|
1315
1741
|
}
|
|
1316
1742
|
const parseRealtimeEvent = (payload) => {
|
|
1317
1743
|
const type = String(payload.type ?? "");
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (type
|
|
1322
|
-
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
|
+
}];
|
|
1323
1784
|
}
|
|
1324
|
-
if (type
|
|
1325
|
-
|
|
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 }];
|
|
1326
1788
|
}
|
|
1327
|
-
if (type
|
|
1328
|
-
return [{ type: "realtime-
|
|
1789
|
+
if (type === "session.finished") {
|
|
1790
|
+
return [{ type: "realtime-end", reason: "finished", providerMetadata: metadata }];
|
|
1329
1791
|
}
|
|
1330
|
-
if (type.
|
|
1331
|
-
|
|
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
|
+
}];
|
|
1332
1799
|
}
|
|
1333
1800
|
return [];
|
|
1334
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
|
+
};
|
|
1335
1841
|
class QwenRealtimeModel {
|
|
1336
1842
|
modelId;
|
|
1337
1843
|
apiKey;
|
|
@@ -1356,13 +1862,21 @@ class QwenRealtimeModel {
|
|
|
1356
1862
|
connection,
|
|
1357
1863
|
callbacks: {
|
|
1358
1864
|
parseEvent: parseRealtimeEvent,
|
|
1359
|
-
buildInitialPayloads: (value) => [{ type: "session.update", session:
|
|
1360
|
-
buildAudioPayloads: (frame) => [{ type: "input_audio_buffer.append", audio:
|
|
1361
|
-
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
|
+
},
|
|
1362
1873
|
buildTextPayloads: (text) => [{ type: "conversation.item.create", item: { type: "message", role: "user", content: [{ type: "input_text", text }] } }, { type: "response.create" }],
|
|
1363
|
-
buildToolResultPayloads: (result) => [
|
|
1364
|
-
|
|
1365
|
-
|
|
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: () => []
|
|
1366
1880
|
}
|
|
1367
1881
|
});
|
|
1368
1882
|
await session.initialize();
|
|
@@ -1374,24 +1888,33 @@ export const createQwen = (options = {}) => {
|
|
|
1374
1888
|
if (!apiKey) {
|
|
1375
1889
|
throw new ConfigurationError("Missing Qwen API key.");
|
|
1376
1890
|
}
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
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";
|
|
1380
1899
|
const fetcher = options.fetch ?? globalThis.fetch;
|
|
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
|
+
}
|
|
1381
1905
|
return createProviderAdapter({
|
|
1382
1906
|
name: "qwen",
|
|
1383
1907
|
languageModel: (modelId) => new QwenLanguageModel(modelId, apiKey, baseURL, fetcher),
|
|
1384
1908
|
embeddingModel: (modelId) => new QwenEmbeddingModel(modelId, apiKey, baseURL, fetcher),
|
|
1385
|
-
transcriptionModel: (modelId) => new QwenTranscriptionModel(modelId, apiKey, baseURL, fetcher),
|
|
1386
|
-
speechModel: (modelId) => new QwenSpeechModel(modelId, apiKey,
|
|
1387
|
-
imageGenerationModel: (modelId) => new QwenImageGenerationModel(modelId, apiKey,
|
|
1909
|
+
transcriptionModel: (modelId) => new QwenTranscriptionModel(modelId, apiKey, baseURL, fetcher, responseLimits),
|
|
1910
|
+
speechModel: (modelId) => new QwenSpeechModel(modelId, apiKey, taskBaseURL, fetcher, responseLimits, options.speechAudioURLValidator, speechAudioMaxRedirects),
|
|
1911
|
+
imageGenerationModel: (modelId) => new QwenImageGenerationModel(modelId, apiKey, taskBaseURL, fetcher),
|
|
1388
1912
|
videoGenerationModel: (modelId) => new QwenVideoGenerationModel(modelId, apiKey, taskBaseURL, fetcher),
|
|
1389
1913
|
realtimeModel: (modelId) => new QwenRealtimeModel(modelId, apiKey, realtimeURL, options.realtimeConnectionFactory),
|
|
1390
1914
|
files: new QwenFilesClient(apiKey, baseURL, fetcher),
|
|
1391
|
-
fileSearchStores: new QwenFileSearchStoresClient(apiKey, baseURL, fetcher),
|
|
1392
1915
|
batches: new QwenBatchesClient(apiKey, baseURL, fetcher),
|
|
1393
|
-
rerankModel: (modelId) => new QwenRerankModelImpl(modelId, apiKey, baseURL, fetcher),
|
|
1394
|
-
multimodalEmbeddingModel: (modelId) => new
|
|
1916
|
+
rerankModel: (modelId) => new QwenRerankModelImpl(modelId, apiKey, baseURL, taskBaseURL, fetcher),
|
|
1917
|
+
multimodalEmbeddingModel: (modelId) => new QwenMultimodalEmbeddingModelImpl(modelId, apiKey, taskBaseURL, fetcher),
|
|
1395
1918
|
tasks: new QwenTasksClientImpl(apiKey, taskBaseURL, fetcher),
|
|
1396
1919
|
rawFetch: fetcher
|
|
1397
1920
|
});
|