@zhivex-ai/qwen 0.7.8 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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: true,
9
+ parallelToolCalls: false,
10
10
  vision: true,
11
11
  files: false,
12
12
  audioInput: false,
@@ -57,6 +57,173 @@ 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
+ };
78
+ const QWEN_REALTIME_QUEUE_LIMIT = 256;
79
+ const nodeWebSocketDataToText = (data) => {
80
+ if (Array.isArray(data)) {
81
+ return Buffer.concat(data).toString("utf8");
82
+ }
83
+ if (data instanceof ArrayBuffer) {
84
+ return Buffer.from(data).toString("utf8");
85
+ }
86
+ return Buffer.from(data).toString("utf8");
87
+ };
88
+ const openQwenRealtimeConnection = async (url, headers, options) => {
89
+ if (typeof process === "undefined" || !process.versions?.node) {
90
+ return openWebSocketConnection(url, headers, options);
91
+ }
92
+ if (options?.signal?.aborted) {
93
+ throw new Error("Qwen realtime connection aborted.");
94
+ }
95
+ const { default: WebSocket } = await import("ws");
96
+ const socket = options?.subprotocols?.length
97
+ ? new WebSocket(url, options.subprotocols, { headers })
98
+ : new WebSocket(url, { headers });
99
+ await new Promise((resolve, reject) => {
100
+ let finished = false;
101
+ const finish = (callback) => {
102
+ if (finished) {
103
+ return;
104
+ }
105
+ finished = true;
106
+ if (timer) {
107
+ clearTimeout(timer);
108
+ }
109
+ options?.signal?.removeEventListener("abort", onAbort);
110
+ socket.off("open", onOpen);
111
+ socket.off("error", onError);
112
+ callback();
113
+ };
114
+ const onOpen = () => finish(resolve);
115
+ const onError = (error) => finish(() => reject(error));
116
+ const onAbort = () => {
117
+ socket.close();
118
+ finish(() => reject(new Error("Qwen realtime connection aborted.")));
119
+ };
120
+ const timer = options?.timeoutMs
121
+ ? setTimeout(() => {
122
+ socket.close();
123
+ finish(() => reject(new Error(`Qwen realtime connection timed out after ${options.timeoutMs}ms.`)));
124
+ }, options.timeoutMs)
125
+ : undefined;
126
+ socket.once("open", onOpen);
127
+ socket.once("error", onError);
128
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
129
+ });
130
+ const queue = [];
131
+ const readers = [];
132
+ let closed = false;
133
+ let connectionError;
134
+ const rejectReaders = (error) => {
135
+ for (const reader of readers.splice(0)) {
136
+ reader.reject(error);
137
+ }
138
+ };
139
+ const closeReaders = () => {
140
+ for (const reader of readers.splice(0)) {
141
+ reader.resolve(undefined);
142
+ }
143
+ };
144
+ socket.on("message", (data) => {
145
+ const text = nodeWebSocketDataToText(data);
146
+ const reader = readers.shift();
147
+ if (reader) {
148
+ try {
149
+ reader.resolve(JSON.parse(text));
150
+ }
151
+ catch (error) {
152
+ reader.reject(error);
153
+ }
154
+ return;
155
+ }
156
+ if (queue.length >= QWEN_REALTIME_QUEUE_LIMIT) {
157
+ connectionError = new Error(`Qwen realtime receive buffer exceeded ${QWEN_REALTIME_QUEUE_LIMIT} messages.`);
158
+ connectionError.name = "StreamBufferOverflowError";
159
+ socket.close();
160
+ return;
161
+ }
162
+ queue.push(text);
163
+ });
164
+ socket.on("error", (error) => {
165
+ connectionError = error;
166
+ rejectReaders(error);
167
+ });
168
+ socket.on("close", () => {
169
+ closed = true;
170
+ if (connectionError) {
171
+ rejectReaders(connectionError);
172
+ }
173
+ else {
174
+ closeReaders();
175
+ }
176
+ });
177
+ const connection = {
178
+ async sendJson(payload) {
179
+ if (connectionError) {
180
+ throw connectionError;
181
+ }
182
+ if (closed) {
183
+ throw new Error("Qwen realtime connection is closed.");
184
+ }
185
+ await new Promise((resolve, reject) => {
186
+ socket.send(JSON.stringify(payload), (error) => {
187
+ if (error) {
188
+ reject(error);
189
+ }
190
+ else {
191
+ resolve();
192
+ }
193
+ });
194
+ });
195
+ },
196
+ async recvJson() {
197
+ if (connectionError) {
198
+ throw connectionError;
199
+ }
200
+ if (queue.length > 0) {
201
+ return JSON.parse(queue.shift());
202
+ }
203
+ if (closed) {
204
+ return undefined;
205
+ }
206
+ return new Promise((resolve, reject) => readers.push({ resolve, reject }));
207
+ },
208
+ async close() {
209
+ if (closed) {
210
+ return;
211
+ }
212
+ await new Promise((resolve) => {
213
+ const timer = setTimeout(() => {
214
+ socket.terminate();
215
+ resolve();
216
+ }, 1_000);
217
+ socket.once("close", () => {
218
+ clearTimeout(timer);
219
+ resolve();
220
+ });
221
+ socket.close();
222
+ });
223
+ }
224
+ };
225
+ return connection;
226
+ };
60
227
  const toUint8Array = async (data) => {
61
228
  if (data instanceof Uint8Array) {
62
229
  return data;
@@ -70,6 +237,24 @@ const toUint8Array = async (data) => {
70
237
  return Uint8Array.from(Buffer.from(String(data), "base64"));
71
238
  };
72
239
  const toBase64 = async (data) => typeof data === "string" ? data : Buffer.from(await toUint8Array(data)).toString("base64");
240
+ const toDataURL = (data, mediaType) => {
241
+ if (typeof data === "string" && (/^data:/i.test(data) || /^https?:\/\//i.test(data))) {
242
+ return data;
243
+ }
244
+ const encoded = typeof data === "string"
245
+ ? data
246
+ : Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data)).toString("base64");
247
+ return `data:${mediaType};base64,${encoded}`;
248
+ };
249
+ const mediaValue = (input) => {
250
+ if (input.uri) {
251
+ return input.uri;
252
+ }
253
+ if (input.data === undefined) {
254
+ throw new ConfigurationError("Qwen media input requires either uri or data.");
255
+ }
256
+ return toDataURL(input.data, input.mediaType);
257
+ };
73
258
  const createFile = async (data, mediaType, filename) => {
74
259
  const bytes = await toUint8Array(data);
75
260
  return new File([bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)], filename, { type: mediaType });
@@ -83,6 +268,50 @@ const appendQuery = (url, query) => {
83
268
  }
84
269
  return parsed.toString();
85
270
  };
271
+ const encodeQwenResourceId = (value, resource) => {
272
+ if (!value || value === "." || value === ".." || /[\\/?#\s]/.test(value)) {
273
+ throw new ConfigurationError(`Qwen ${resource} ID must be a non-empty opaque identifier without path separators.`);
274
+ }
275
+ return encodeURIComponent(value);
276
+ };
277
+ const isQwenOmniLanguageModel = (modelId) => /^qwen3(?:\.5)?-omni-(?:plus|flash)(?:$|-)/i.test(modelId);
278
+ const isOfficialQwenSpeechAudioURL = (url) => !url.username &&
279
+ !url.password &&
280
+ /(?:^|\.)oss(?:-[a-z0-9-]+)?\.aliyuncs\.com$/i.test(url.hostname);
281
+ const parseQwenSpeechAudioURL = async (value, validator) => {
282
+ let url;
283
+ try {
284
+ url = new URL(value);
285
+ }
286
+ catch {
287
+ throw new ProviderHTTPError("Qwen speech response contained an invalid audio download URL.", 502);
288
+ }
289
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
290
+ throw new ProviderHTTPError("Qwen speech audio downloads require an HTTP(S) URL.", 502);
291
+ }
292
+ const allowed = validator ? await validator(url) : isOfficialQwenSpeechAudioURL(url);
293
+ if (!allowed) {
294
+ throw new ProviderHTTPError("Qwen speech response contained an audio download URL rejected by the configured safety policy.", 502);
295
+ }
296
+ return url;
297
+ };
298
+ const downloadQwenSpeechAudio = async (initialURL, fetcher, signal, validator, maxRedirects) => {
299
+ let url = await parseQwenSpeechAudioURL(initialURL, validator);
300
+ for (let redirects = 0;; redirects += 1) {
301
+ const response = await fetcher(url, { method: "GET", redirect: "manual", signal });
302
+ if (![301, 302, 303, 307, 308].includes(response.status)) {
303
+ return response;
304
+ }
305
+ if (redirects >= maxRedirects) {
306
+ throw new ProviderHTTPError(`Qwen speech audio download exceeded ${maxRedirects} redirects.`, 502);
307
+ }
308
+ const location = response.headers.get("location");
309
+ if (!location) {
310
+ throw new ProviderHTTPError("Qwen speech audio redirect did not include a Location header.", 502);
311
+ }
312
+ url = await parseQwenSpeechAudioURL(new URL(location, url).toString(), validator);
313
+ }
314
+ };
86
315
  const providerHeaders = (apiKey, providerOptions) => {
87
316
  const headers = jsonHeaders(apiKey);
88
317
  const sessionCache = providerOptions?.["x-dashscope-session-cache"];
@@ -96,26 +325,69 @@ const stripHeaderOptions = (providerOptions) => {
96
325
  delete next["x-dashscope-session-cache"];
97
326
  return next;
98
327
  };
328
+ const stripResponsesRequestOptions = (providerOptions) => {
329
+ const next = stripHeaderOptions(providerOptions);
330
+ for (const key of [
331
+ "model",
332
+ "input",
333
+ "stream",
334
+ "tools",
335
+ "tool_choice",
336
+ "temperature",
337
+ "reasoning",
338
+ "previous_response_id",
339
+ "max_output_tokens",
340
+ "response_format"
341
+ ]) {
342
+ delete next[key];
343
+ }
344
+ return next;
345
+ };
99
346
  const modelFamily = (modelId) => modelId.toLowerCase();
100
- const supportsQwenReasoning = (modelId) => /^(qwen-(plus|turbo|max|flash)|qwq|qwen3|qwen3\.)/i.test(modelId);
347
+ const supportsQwenReasoning = (modelId) => !isQwenOmniLanguageModel(modelId) &&
348
+ supportsQwenTools(modelId) &&
349
+ /^(qwen-(plus|turbo|max|flash)|qwq|qwen3|qwen3\.)/i.test(modelId);
101
350
  const supportsQwenVision = (modelId) => {
102
351
  const model = modelFamily(modelId);
103
352
  return (model.includes("vl") ||
104
353
  model.includes("omni") ||
354
+ model.includes("ocr") ||
105
355
  model.includes("vision") ||
106
- /^qwen3\.7-plus(?:$|-)/.test(model));
356
+ /^qwen3\.7-plus(?:$|-)/.test(model) ||
357
+ /^qwen3\.6-flash(?:$|-)/.test(model));
358
+ };
359
+ const supportsQwenTools = (modelId) => !/(embedding|rerank|asr|tts|image|realtime|ocr|translation|character|long-context)/.test(modelFamily(modelId));
360
+ const supportsQwenFiles = (modelId) => /^qwen3\.5-ocr(?:$|-)/.test(modelFamily(modelId));
361
+ const supportsQwenAudioInput = (modelId) => /(omni|audio|asr)/.test(modelFamily(modelId));
362
+ const qwenLanguageCapabilities = (modelId) => {
363
+ const tools = supportsQwenTools(modelId);
364
+ const omni = isQwenOmniLanguageModel(modelId);
365
+ const reasoning = supportsQwenReasoning(modelId);
366
+ return {
367
+ ...capabilities,
368
+ vision: supportsQwenVision(modelId),
369
+ tools,
370
+ structuredOutput: tools && !omni,
371
+ jsonMode: tools && !omni,
372
+ toolChoice: tools,
373
+ parallelToolCalls: false,
374
+ webSearch: tools && !omni,
375
+ files: supportsQwenFiles(modelId),
376
+ audioInput: supportsQwenAudioInput(modelId),
377
+ reasoning,
378
+ reasoningEfforts: reasoning ? ["none", "minimal", "low", "medium", "high"] : undefined,
379
+ agentCapabilities: {
380
+ ...capabilities.agentCapabilities,
381
+ supportTier: tools && !omni ? "tier-b" : "tier-c",
382
+ toolChoiceNone: tools,
383
+ hostedWebSearch: tools && !omni,
384
+ hostedFileSearch: tools && !omni,
385
+ remoteMcp: tools && !omni,
386
+ codeExecution: tools && !omni,
387
+ webExtraction: tools && !omni
388
+ }
389
+ };
107
390
  };
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
391
  const transcriptionCapabilities = {
120
392
  ...embeddingCapabilities,
121
393
  audioInput: true
@@ -176,23 +448,25 @@ const parseJson = async (response, options = {}) => {
176
448
  : response.json();
177
449
  };
178
450
  const mapContentParts = (message) => {
179
- const textParts = message.parts.filter((part) => part.type === "text");
180
- const imageParts = message.parts.filter((part) => part.type === "image");
181
- if (!imageParts.length) {
182
- return textParts.map((part) => part.text).join("");
451
+ const hasMedia = message.parts.some((part) => part.type === "image" || part.type === "audio");
452
+ if (!hasMedia) {
453
+ return message.parts
454
+ .filter((part) => part.type === "text")
455
+ .map((part) => part.text)
456
+ .join("");
183
457
  }
184
- return [
185
- ...textParts.map((part) => ({
186
- type: "text",
187
- text: part.text
188
- })),
189
- ...imageParts.map((part) => ({
190
- type: "image_url",
191
- image_url: {
192
- url: part.image
193
- }
194
- }))
195
- ];
458
+ return message.parts.flatMap((part) => {
459
+ if (part.type === "text") {
460
+ return [{ type: "text", text: part.text }];
461
+ }
462
+ if (part.type === "image") {
463
+ return [{ type: "image_url", image_url: { url: part.image } }];
464
+ }
465
+ if (part.type === "audio") {
466
+ return [{ type: "input_audio", input_audio: { data: toDataURL(part.data, part.mediaType) } }];
467
+ }
468
+ return [];
469
+ });
196
470
  };
197
471
  const mapMessages = (messages) => messages.map((message) => {
198
472
  if (message.role === "tool") {
@@ -264,7 +538,7 @@ const mapResponsesTools = (tools) => tools
264
538
  };
265
539
  })
266
540
  : undefined;
267
- const mapToolChoice = (toolChoice) => {
541
+ const mapChatToolChoice = (toolChoice) => {
268
542
  if (!toolChoice) {
269
543
  return undefined;
270
544
  }
@@ -278,21 +552,48 @@ const mapToolChoice = (toolChoice) => {
278
552
  }
279
553
  };
280
554
  };
281
- const mapStructuredOutput = (input) => {
282
- if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
555
+ const mapResponsesToolChoice = (toolChoice, tools) => {
556
+ if (!toolChoice) {
283
557
  return undefined;
284
558
  }
285
- return {
286
- type: "json_schema",
287
- json_schema: {
288
- name: input.structuredOutput.name ?? "response",
289
- strict: true,
290
- schema: toJSONSchema(input.structuredOutput.schema)
559
+ if (typeof toolChoice === "string") {
560
+ if (toolChoice === "required" && Object.keys(tools ?? {}).length !== 1) {
561
+ throw new UnsupportedFeatureError('Qwen Responses supports toolChoice "required" only when exactly one tool is provided.');
291
562
  }
563
+ return toolChoice;
564
+ }
565
+ return {
566
+ type: "allowed_tools",
567
+ mode: "required",
568
+ tools: [{ type: "function", name: toolChoice.toolName }]
292
569
  };
293
570
  };
571
+ const mapStructuredOutput = (input) => {
572
+ if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
573
+ return undefined;
574
+ }
575
+ return { type: "json_object" };
576
+ };
577
+ const withStructuredOutputMessages = (input) => {
578
+ if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
579
+ return input.messages;
580
+ }
581
+ const schema = JSON.stringify(toJSONSchema(input.structuredOutput.schema));
582
+ return [
583
+ {
584
+ role: "system",
585
+ parts: [
586
+ {
587
+ type: "text",
588
+ text: `Return only valid JSON matching this JSON Schema: ${schema}`
589
+ }
590
+ ]
591
+ },
592
+ ...input.messages
593
+ ];
594
+ };
294
595
  const hasPreservedReasoning = (messages) => messages.some((message) => message.role === "assistant" && reasoningContentFromMessage(message));
295
- const mapReasoning = (input) => {
596
+ const mapChatReasoning = (input) => {
296
597
  if (!input.reasoning) {
297
598
  return undefined;
298
599
  }
@@ -304,6 +605,63 @@ const mapReasoning = (input) => {
304
605
  ...(hasPreservedReasoning(input.messages) ? { preserve_thinking: true } : {})
305
606
  };
306
607
  };
608
+ const mapResponsesReasoning = (input) => {
609
+ const effort = input.reasoning?.effort;
610
+ if (!effort) {
611
+ return undefined;
612
+ }
613
+ return {
614
+ reasoning: {
615
+ effort: effort === "low" ? "minimal" : effort
616
+ }
617
+ };
618
+ };
619
+ const mapResponsesUsage = (usage) => usage
620
+ ? {
621
+ inputTokens: usage.input_tokens,
622
+ cachedInputTokens: usage.input_tokens_details?.cached_tokens,
623
+ outputTokens: usage.output_tokens,
624
+ reasoningTokens: usage.output_tokens_details?.reasoning_tokens,
625
+ totalTokens: usage.total_tokens
626
+ }
627
+ : undefined;
628
+ const mapChatUsage = (usage) => usage
629
+ ? {
630
+ inputTokens: usage.prompt_tokens,
631
+ cachedInputTokens: usage.prompt_tokens_details?.cached_tokens ?? usage.input_tokens_details?.cached_tokens,
632
+ outputTokens: usage.completion_tokens,
633
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? usage.output_tokens_details?.reasoning_tokens,
634
+ totalTokens: usage.total_tokens
635
+ }
636
+ : undefined;
637
+ const hasHostedTools = (tools) => Object.values(tools ?? {}).some((tool) => !isCallableToolDefinition(tool));
638
+ const hasMessagePart = (messages, type) => messages.some((message) => message.parts.some((part) => part.type === type));
639
+ const resolveApiMode = (modelId, requestedMode, input) => {
640
+ const needsChat = input.maxTokens !== undefined ||
641
+ input.reasoning?.budgetTokens !== undefined ||
642
+ input.structuredOutput?.mode === "native" ||
643
+ hasMessagePart(input.messages, "audio");
644
+ const needsResponses = hasHostedTools(input.tools) || hasMessagePart(input.messages, "file");
645
+ if (needsChat && needsResponses) {
646
+ throw new UnsupportedFeatureError("Qwen cannot combine Responses-only hosted/file inputs with Chat-only maxTokens, audio, reasoning budgets, or structured output.");
647
+ }
648
+ if (isQwenOmniLanguageModel(modelId)) {
649
+ if (requestedMode === "responses" || needsResponses) {
650
+ throw new UnsupportedFeatureError("Qwen Omni uses streaming Chat Completions and does not support Responses hosted tools or file inputs.");
651
+ }
652
+ return "chat";
653
+ }
654
+ if (requestedMode === "responses" && needsChat) {
655
+ throw new UnsupportedFeatureError('Qwen Responses does not process maxTokens, audio input, reasoning budgets, or native structured output; use apiMode "chat" or "auto".');
656
+ }
657
+ if (requestedMode === "chat" && needsResponses) {
658
+ throw new UnsupportedFeatureError('Qwen Chat Completions does not support Responses hosted tools or OCR file inputs.');
659
+ }
660
+ if (requestedMode === "responses" || requestedMode === "chat") {
661
+ return requestedMode;
662
+ }
663
+ return needsChat ? "chat" : "responses";
664
+ };
307
665
  const parseAssistantMessage = (message) => ({
308
666
  role: "assistant",
309
667
  parts: [
@@ -366,6 +724,16 @@ const toResponsesInput = (messages) => {
366
724
  case "image":
367
725
  content.push({ type: "input_image", image_url: part.image });
368
726
  break;
727
+ case "file":
728
+ if (!/^https?:\/\//i.test(part.data)) {
729
+ throw new UnsupportedFeatureError("Qwen Responses file input requires a public HTTP(S) file URL; DashScope Files IDs are reserved for batch jobs.");
730
+ }
731
+ content.push({
732
+ type: "input_file",
733
+ file_url: part.data,
734
+ ...(part.filename ? { filename: part.filename } : {})
735
+ });
736
+ break;
369
737
  case "tool-call":
370
738
  if (message.role === "assistant") {
371
739
  input.push({
@@ -376,6 +744,15 @@ const toResponsesInput = (messages) => {
376
744
  });
377
745
  }
378
746
  break;
747
+ case "provider-data":
748
+ if (message.role === "assistant" &&
749
+ part.provider === "qwen" &&
750
+ part.data &&
751
+ typeof part.data === "object" &&
752
+ typeof part.data.type === "string") {
753
+ input.push(part.data);
754
+ }
755
+ break;
379
756
  }
380
757
  }
381
758
  if (content.length) {
@@ -553,17 +930,18 @@ const streamResponses = async function* (response) {
553
930
  }
554
931
  if (type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
555
932
  const responseData = json.response ?? {};
933
+ if (typeof responseData.id === "string") {
934
+ yield {
935
+ type: "provider-data",
936
+ provider: "qwen",
937
+ data: { responseId: responseData.id }
938
+ };
939
+ }
556
940
  yield {
557
941
  type: "finish",
558
942
  finishReason: normalizeResponsesFinishReason(responseData.status, sawToolCalls),
559
943
  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
944
+ usage: mapResponsesUsage(responseData.usage)
567
945
  };
568
946
  }
569
947
  }
@@ -583,14 +961,17 @@ class QwenLanguageModel {
583
961
  this.capabilities = qwenLanguageCapabilities(modelId);
584
962
  }
585
963
  async generate(input) {
964
+ if (isQwenOmniLanguageModel(this.modelId)) {
965
+ throw new UnsupportedFeatureError(`Qwen Omni model "${this.modelId}" is streaming-only. Use streamText() instead of generateText().`);
966
+ }
586
967
  const { signal, cleanup } = withTimeoutSignal(input);
587
968
  const providerOptions = { ...(input.providerOptions ?? {}) };
588
- const apiMode = providerOptions.apiMode ?? "responses";
969
+ const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input);
589
970
  delete providerOptions.apiMode;
590
971
  try {
591
972
  if (apiMode === "responses") {
592
973
  const previousResponse = getProviderResponseId(input.messages);
593
- const responseProviderOptions = stripHeaderOptions(providerOptions);
974
+ const responseProviderOptions = stripResponsesRequestOptions(providerOptions);
594
975
  const messages = previousResponse && previousResponse.index < input.messages.length - 1
595
976
  ? input.messages.slice(previousResponse.index + 1)
596
977
  : input.messages;
@@ -603,12 +984,10 @@ class QwenLanguageModel {
603
984
  model: this.modelId,
604
985
  ...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
605
986
  ...(messages.length ? { input: toResponsesInput(messages) } : {}),
606
- ...(input.structuredOutput?.mode === "native" ? { response_format: mapStructuredOutput(input) } : {}),
607
987
  tools: mapResponsesTools(input.tools),
608
- tool_choice: mapToolChoice(input.toolChoice),
988
+ tool_choice: mapResponsesToolChoice(input.toolChoice, input.tools),
609
989
  temperature: input.temperature,
610
- max_output_tokens: input.maxTokens,
611
- ...mapReasoning(input),
990
+ ...mapResponsesReasoning(input),
612
991
  stream: false
613
992
  })
614
993
  }), input);
@@ -623,29 +1002,25 @@ class QwenLanguageModel {
623
1002
  .join(""),
624
1003
  finishReason: normalizeResponsesFinishReason(json.status, hasToolCalls),
625
1004
  providerFinishReason: json.status,
626
- usage: {
627
- inputTokens: json.usage?.input_tokens,
628
- outputTokens: json.usage?.output_tokens,
629
- totalTokens: json.usage?.total_tokens
630
- },
1005
+ usage: mapResponsesUsage(json.usage),
631
1006
  rawResponse: json
632
1007
  };
633
1008
  }
634
1009
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
635
1010
  method: "POST",
636
- headers: jsonHeaders(this.apiKey),
1011
+ headers: providerHeaders(this.apiKey, providerOptions),
637
1012
  signal,
638
1013
  body: JSON.stringify({
639
- ...providerOptions,
1014
+ ...stripHeaderOptions(providerOptions),
640
1015
  model: this.modelId,
641
- messages: mapMessages(input.messages),
1016
+ messages: mapMessages(withStructuredOutputMessages(input)),
642
1017
  tools: mapChatTools(input.tools),
643
- tool_choice: mapToolChoice(input.toolChoice),
1018
+ tool_choice: mapChatToolChoice(input.toolChoice),
644
1019
  response_format: mapStructuredOutput(input),
645
1020
  temperature: input.temperature,
646
1021
  max_tokens: input.maxTokens,
647
1022
  stream: false,
648
- ...mapReasoning(input)
1023
+ ...mapChatReasoning(input)
649
1024
  })
650
1025
  }), input);
651
1026
  const json = await parseJson(response);
@@ -660,11 +1035,7 @@ class QwenLanguageModel {
660
1035
  .join(""),
661
1036
  finishReason: normalizeFinishReason(choice?.finish_reason),
662
1037
  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
- },
1038
+ usage: mapChatUsage(json.usage),
668
1039
  rawResponse: json
669
1040
  };
670
1041
  }
@@ -675,10 +1046,14 @@ class QwenLanguageModel {
675
1046
  async stream(input) {
676
1047
  const { signal, cleanup } = withTimeoutSignal(input);
677
1048
  const providerOptions = { ...(input.providerOptions ?? {}) };
678
- const apiMode = providerOptions.apiMode ?? "responses";
1049
+ const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input);
679
1050
  delete providerOptions.apiMode;
680
1051
  if (apiMode === "responses") {
681
- const responseProviderOptions = stripHeaderOptions(providerOptions);
1052
+ const previousResponse = getProviderResponseId(input.messages);
1053
+ const responseProviderOptions = stripResponsesRequestOptions(providerOptions);
1054
+ const messages = previousResponse && previousResponse.index < input.messages.length - 1
1055
+ ? input.messages.slice(previousResponse.index + 1)
1056
+ : input.messages;
682
1057
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/responses`, {
683
1058
  method: "POST",
684
1059
  headers: providerHeaders(this.apiKey, providerOptions),
@@ -686,13 +1061,12 @@ class QwenLanguageModel {
686
1061
  body: JSON.stringify({
687
1062
  ...responseProviderOptions,
688
1063
  model: this.modelId,
689
- input: toResponsesInput(input.messages),
690
- ...(input.structuredOutput?.mode === "native" ? { response_format: mapStructuredOutput(input) } : {}),
1064
+ ...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
1065
+ ...(messages.length ? { input: toResponsesInput(messages) } : {}),
691
1066
  tools: mapResponsesTools(input.tools),
692
- tool_choice: mapToolChoice(input.toolChoice),
1067
+ tool_choice: mapResponsesToolChoice(input.toolChoice, input.tools),
693
1068
  temperature: input.temperature,
694
- max_output_tokens: input.maxTokens,
695
- ...mapReasoning(input),
1069
+ ...mapResponsesReasoning(input),
696
1070
  stream: true
697
1071
  })
698
1072
  }), input);
@@ -707,20 +1081,21 @@ class QwenLanguageModel {
707
1081
  }
708
1082
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
709
1083
  method: "POST",
710
- headers: jsonHeaders(this.apiKey),
1084
+ headers: providerHeaders(this.apiKey, providerOptions),
711
1085
  signal,
712
1086
  body: JSON.stringify({
713
- ...providerOptions,
1087
+ ...stripHeaderOptions(providerOptions),
714
1088
  model: this.modelId,
715
- messages: mapMessages(input.messages),
1089
+ modalities: isQwenOmniLanguageModel(this.modelId) ? providerOptions.modalities ?? ["text"] : undefined,
1090
+ messages: mapMessages(withStructuredOutputMessages(input)),
716
1091
  tools: mapChatTools(input.tools),
717
- tool_choice: mapToolChoice(input.toolChoice),
1092
+ tool_choice: mapChatToolChoice(input.toolChoice),
718
1093
  response_format: mapStructuredOutput(input),
719
1094
  temperature: input.temperature,
720
1095
  max_tokens: input.maxTokens,
721
1096
  stream: true,
722
1097
  stream_options: { include_usage: true },
723
- ...mapReasoning(input)
1098
+ ...mapChatReasoning(input)
724
1099
  })
725
1100
  }), input);
726
1101
  return (async function* () {
@@ -747,37 +1122,40 @@ class QwenLanguageModel {
747
1122
  yield { type: "text-delta", textDelta: delta.content };
748
1123
  }
749
1124
  for (const toolCall of delta?.tool_calls ?? []) {
750
- const id = toolCall.id ?? `${toolCall.index}`;
751
- const existing = toolBuffers.get(id) ?? {
1125
+ const index = Number(toolCall.index ?? 0);
1126
+ const existing = toolBuffers.get(index) ?? {
1127
+ id: toolCall.id ?? `${index}`,
752
1128
  name: toolCall.function?.name ?? "",
753
- args: ""
1129
+ args: "",
1130
+ emitted: false
754
1131
  };
1132
+ existing.id = toolCall.id ?? existing.id;
755
1133
  existing.name ||= toolCall.function?.name ?? "";
756
1134
  existing.args += toolCall.function?.arguments ?? "";
757
- toolBuffers.set(id, existing);
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
- }
1135
+ toolBuffers.set(index, existing);
768
1136
  }
769
1137
  if (choice?.finish_reason) {
1138
+ if (choice.finish_reason === "tool_calls") {
1139
+ for (const toolCall of toolBuffers.values()) {
1140
+ if (toolCall.emitted) {
1141
+ continue;
1142
+ }
1143
+ toolCall.emitted = true;
1144
+ yield {
1145
+ type: "tool-call",
1146
+ toolCall: {
1147
+ id: toolCall.id,
1148
+ name: toolCall.name,
1149
+ input: JSON.parse(toolCall.args || "{}")
1150
+ }
1151
+ };
1152
+ }
1153
+ }
770
1154
  yield {
771
1155
  type: "finish",
772
1156
  finishReason: normalizeFinishReason(choice.finish_reason),
773
1157
  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
1158
+ usage: mapChatUsage(json.usage)
781
1159
  };
782
1160
  }
783
1161
  }
@@ -834,6 +1212,67 @@ class QwenEmbeddingModel {
834
1212
  }
835
1213
  }
836
1214
  }
1215
+ const toMultimodalEmbeddingContent = (value) => {
1216
+ if (typeof value === "string") {
1217
+ return { text: value };
1218
+ }
1219
+ if (value.mediaType.startsWith("image/")) {
1220
+ return { image: mediaValue(value) };
1221
+ }
1222
+ if (value.mediaType.startsWith("video/")) {
1223
+ if (!value.uri) {
1224
+ throw new UnsupportedFeatureError("Qwen multimodal embedding requires a public URL for video input.");
1225
+ }
1226
+ return { video: value.uri };
1227
+ }
1228
+ throw new UnsupportedFeatureError(`Qwen multimodal embedding supports text, image, and video inputs, not "${value.mediaType}".`);
1229
+ };
1230
+ class QwenMultimodalEmbeddingModelImpl {
1231
+ modelId;
1232
+ apiKey;
1233
+ taskBaseURL;
1234
+ fetcher;
1235
+ provider = "qwen";
1236
+ capabilities = {
1237
+ ...embeddingCapabilities,
1238
+ vision: true
1239
+ };
1240
+ constructor(modelId, apiKey, taskBaseURL, fetcher) {
1241
+ this.modelId = modelId;
1242
+ this.apiKey = apiKey;
1243
+ this.taskBaseURL = taskBaseURL;
1244
+ this.fetcher = fetcher;
1245
+ }
1246
+ async embed(input) {
1247
+ const { signal, cleanup } = withTimeoutSignal(input);
1248
+ try {
1249
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/embeddings/multimodal-embedding/multimodal-embedding`, {
1250
+ method: "POST",
1251
+ headers: jsonHeaders(this.apiKey),
1252
+ signal,
1253
+ body: JSON.stringify({
1254
+ model: this.modelId,
1255
+ input: { contents: input.values.map(toMultimodalEmbeddingContent) },
1256
+ ...(input.providerOptions && Object.keys(input.providerOptions).length
1257
+ ? { parameters: input.providerOptions }
1258
+ : {})
1259
+ })
1260
+ }), input);
1261
+ const json = await parseJson(response);
1262
+ return {
1263
+ embeddings: (json.output?.embeddings ?? []).map((entry) => entry.embedding),
1264
+ usage: {
1265
+ inputTokens: json.usage?.input_tokens,
1266
+ totalTokens: json.usage?.total_tokens
1267
+ },
1268
+ rawResponse: json
1269
+ };
1270
+ }
1271
+ finally {
1272
+ cleanup();
1273
+ }
1274
+ }
1275
+ }
837
1276
  const normalizeUploadedFile = (json) => ({
838
1277
  name: json.id ?? json.name ?? json.file_id ?? "",
839
1278
  uri: json.url ?? json.uri,
@@ -844,14 +1283,6 @@ const normalizeUploadedFile = (json) => ({
844
1283
  rawResponse: json,
845
1284
  providerMetadata: json
846
1285
  });
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
1286
  const normalizeBatchJob = (json) => ({
856
1287
  name: json.id ?? json.name ?? "",
857
1288
  model: json.model,
@@ -863,10 +1294,13 @@ const normalizeBatchJob = (json) => ({
863
1294
  providerMetadata: json
864
1295
  });
865
1296
  const normalizeOperation = (json) => ({
866
- name: json.task_id ?? json.id ?? json.name ?? "",
1297
+ name: json.output?.task_id ?? json.task_id ?? json.id ?? json.name ?? "",
867
1298
  done: json.output?.task_status ? ["SUCCEEDED", "FAILED", "CANCELED"].includes(json.output.task_status) : json.done,
868
1299
  response: json.output ?? json.response,
869
- error: json.error,
1300
+ error: json.error ??
1301
+ (json.output?.task_status === "FAILED"
1302
+ ? { message: json.output?.message ?? "Qwen task failed.", code: json.output?.code }
1303
+ : undefined),
870
1304
  metadata: json.usage ?? json.metadata,
871
1305
  rawResponse: json
872
1306
  });
@@ -900,7 +1334,8 @@ class QwenFilesClient {
900
1334
  async get(input) {
901
1335
  const { signal, cleanup } = withTimeoutSignal(input);
902
1336
  try {
903
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/files/${input.name}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
1337
+ const fileId = encodeQwenResourceId(input.name, "file");
1338
+ const response = await withRetry(() => this.fetcher(`${this.baseURL}/files/${fileId}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
904
1339
  return normalizeUploadedFile(await parseJson(response));
905
1340
  }
906
1341
  finally {
@@ -925,88 +1360,8 @@ class QwenFilesClient {
925
1360
  async delete(input) {
926
1361
  const { signal, cleanup } = withTimeoutSignal(input);
927
1362
  try {
928
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/files/${input.name}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
929
- const json = await parseJson(response);
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);
1363
+ const fileId = encodeQwenResourceId(input.name, "file");
1364
+ const response = await withRetry(() => this.fetcher(`${this.baseURL}/files/${fileId}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
1010
1365
  const json = await parseJson(response);
1011
1366
  return { name: input.name, rawResponse: json };
1012
1367
  }
@@ -1025,6 +1380,9 @@ class QwenBatchesClient {
1025
1380
  this.fetcher = fetcher;
1026
1381
  }
1027
1382
  async create(input) {
1383
+ if (!input.fileName) {
1384
+ throw new ConfigurationError("Qwen batch creation requires a JSONL fileName uploaded with purpose \"batch\".");
1385
+ }
1028
1386
  const { signal, cleanup } = withTimeoutSignal(input);
1029
1387
  try {
1030
1388
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches`, {
@@ -1036,9 +1394,7 @@ class QwenBatchesClient {
1036
1394
  input_file_id: input.fileName,
1037
1395
  endpoint: input.providerOptions?.endpoint ?? "/v1/chat/completions",
1038
1396
  completion_window: input.providerOptions?.completion_window ?? "24h",
1039
- metadata: input.displayName ? { displayName: input.displayName } : undefined,
1040
- model: input.modelId,
1041
- requests: input.requests
1397
+ metadata: input.displayName ? { displayName: input.displayName } : undefined
1042
1398
  })
1043
1399
  }), input);
1044
1400
  return normalizeBatchJob(await parseJson(response));
@@ -1050,7 +1406,8 @@ class QwenBatchesClient {
1050
1406
  async get(input) {
1051
1407
  const { signal, cleanup } = withTimeoutSignal(input);
1052
1408
  try {
1053
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${input.name}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
1409
+ const batchId = encodeQwenResourceId(input.name, "batch");
1410
+ const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${batchId}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
1054
1411
  return normalizeBatchJob(await parseJson(response));
1055
1412
  }
1056
1413
  finally {
@@ -1071,7 +1428,8 @@ class QwenBatchesClient {
1071
1428
  async cancel(input) {
1072
1429
  const { signal, cleanup } = withTimeoutSignal(input);
1073
1430
  try {
1074
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${input.name}/cancel`, { method: "POST", headers: jsonHeaders(this.apiKey), signal }), input);
1431
+ const batchId = encodeQwenResourceId(input.name, "batch");
1432
+ const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${batchId}/cancel`, { method: "POST", headers: jsonHeaders(this.apiKey), signal }), input);
1075
1433
  return normalizeBatchJob(await parseJson(response));
1076
1434
  }
1077
1435
  finally {
@@ -1081,7 +1439,8 @@ class QwenBatchesClient {
1081
1439
  async delete(input) {
1082
1440
  const { signal, cleanup } = withTimeoutSignal(input);
1083
1441
  try {
1084
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${input.name}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
1442
+ const batchId = encodeQwenResourceId(input.name, "batch");
1443
+ const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches/${batchId}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
1085
1444
  const json = await parseJson(response);
1086
1445
  return { name: input.name, rawResponse: json };
1087
1446
  }
@@ -1106,29 +1465,59 @@ class QwenTranscriptionModel {
1106
1465
  this.responseLimits = responseLimits;
1107
1466
  }
1108
1467
  async transcribe(input) {
1468
+ if (input.prompt) {
1469
+ throw new UnsupportedFeatureError("Qwen3-ASR-Flash does not expose the common transcription prompt field; use providerOptions for documented asr_options.");
1470
+ }
1109
1471
  const { signal, cleanup, abort } = withTimeoutSignal(input);
1110
- const form = new FormData();
1111
- for (const [key, value] of Object.entries(input.providerOptions ?? {})) {
1112
- if (["model", "file", "prompt", "language"].includes(key)) {
1113
- continue;
1114
- }
1115
- form.set(key, typeof value === "string" ? value : JSON.stringify(value));
1116
- }
1117
- form.set("model", this.modelId);
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);
1472
+ const providerOptions = { ...(input.providerOptions ?? {}) };
1473
+ const nestedOptions = providerOptions.asr_options && typeof providerOptions.asr_options === "object"
1474
+ ? providerOptions.asr_options
1475
+ : {};
1476
+ delete providerOptions.asr_options;
1477
+ delete providerOptions.model;
1478
+ delete providerOptions.messages;
1479
+ delete providerOptions.stream;
1123
1480
  try {
1124
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/audio/transcriptions`, { method: "POST", headers: { authorization: `Bearer ${this.apiKey}` }, signal, body: form }), input);
1481
+ const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
1482
+ method: "POST",
1483
+ headers: jsonHeaders(this.apiKey),
1484
+ signal,
1485
+ body: JSON.stringify({
1486
+ model: this.modelId,
1487
+ messages: [
1488
+ {
1489
+ role: "user",
1490
+ content: [
1491
+ {
1492
+ type: "input_audio",
1493
+ input_audio: {
1494
+ data: toDataURL(input.audio.data, input.audio.mediaType)
1495
+ }
1496
+ }
1497
+ ]
1498
+ }
1499
+ ],
1500
+ stream: false,
1501
+ asr_options: {
1502
+ ...providerOptions,
1503
+ ...nestedOptions,
1504
+ ...(input.language ? { language: input.language } : {})
1505
+ }
1506
+ })
1507
+ }), input);
1125
1508
  const json = await parseJson(response, {
1126
1509
  maxBytes: this.responseLimits.transcriptionBytes,
1127
1510
  errorBodyBytes: this.responseLimits.errorBodyBytes,
1128
- endpoint: "audio/transcriptions",
1511
+ endpoint: "chat/completions",
1129
1512
  abort
1130
1513
  });
1131
- return { text: json.text ?? json.output?.text ?? "", rawResponse: json };
1514
+ const content = json.choices?.[0]?.message?.content;
1515
+ const text = Array.isArray(content)
1516
+ ? content.map((part) => part?.text ?? "").join("")
1517
+ : typeof content === "string"
1518
+ ? content
1519
+ : "";
1520
+ return { text, rawResponse: json };
1132
1521
  }
1133
1522
  finally {
1134
1523
  cleanup();
@@ -1138,73 +1527,106 @@ class QwenTranscriptionModel {
1138
1527
  class QwenSpeechModel {
1139
1528
  modelId;
1140
1529
  apiKey;
1141
- baseURL;
1530
+ taskBaseURL;
1142
1531
  fetcher;
1143
1532
  responseLimits;
1533
+ speechAudioURLValidator;
1534
+ speechAudioMaxRedirects;
1144
1535
  provider = "qwen";
1145
1536
  capabilities = speechCapabilities;
1146
- constructor(modelId, apiKey, baseURL, fetcher, responseLimits = resolveAudioResponseLimits()) {
1537
+ constructor(modelId, apiKey, taskBaseURL, fetcher, responseLimits = resolveAudioResponseLimits(), speechAudioURLValidator, speechAudioMaxRedirects = 3) {
1147
1538
  this.modelId = modelId;
1148
1539
  this.apiKey = apiKey;
1149
- this.baseURL = baseURL;
1540
+ this.taskBaseURL = taskBaseURL;
1150
1541
  this.fetcher = fetcher;
1151
1542
  this.responseLimits = responseLimits;
1543
+ this.speechAudioURLValidator = speechAudioURLValidator;
1544
+ this.speechAudioMaxRedirects = speechAudioMaxRedirects;
1152
1545
  }
1153
1546
  async generateSpeech(input) {
1154
1547
  const { signal, cleanup, abort } = withTimeoutSignal(input);
1548
+ const providerOptions = { ...(input.providerOptions ?? {}) };
1549
+ const nestedInput = providerOptions.input && typeof providerOptions.input === "object"
1550
+ ? providerOptions.input
1551
+ : {};
1552
+ const parameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
1553
+ ? providerOptions.parameters
1554
+ : undefined;
1555
+ delete providerOptions.input;
1556
+ delete providerOptions.parameters;
1557
+ delete providerOptions.model;
1155
1558
  try {
1156
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/audio/speech`, {
1559
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/multimodal-generation/generation`, {
1157
1560
  method: "POST",
1158
1561
  headers: jsonHeaders(this.apiKey),
1159
1562
  signal,
1160
- body: JSON.stringify({ ...input.providerOptions, model: this.modelId, input: input.input, voice: input.voice ?? "Chelsie" })
1563
+ body: JSON.stringify({
1564
+ model: this.modelId,
1565
+ input: {
1566
+ ...providerOptions,
1567
+ ...nestedInput,
1568
+ text: input.input,
1569
+ voice: input.voice ?? "Cherry"
1570
+ },
1571
+ ...(parameters ? { parameters } : {})
1572
+ })
1161
1573
  }), input);
1162
- if (response.headers.get("content-type")?.includes("application/json")) {
1163
- const json = await parseJson(response, {
1164
- maxBytes: this.responseLimits.speechJsonBytes,
1165
- errorBodyBytes: this.responseLimits.errorBodyBytes,
1166
- endpoint: "audio/speech",
1167
- abort
1168
- });
1169
- const audio = json.audio?.data ?? json.output?.audio?.data ?? "";
1574
+ const json = await parseJson(response, {
1575
+ maxBytes: this.responseLimits.speechJsonBytes,
1576
+ errorBodyBytes: this.responseLimits.errorBodyBytes,
1577
+ endpoint: "multimodal-generation/generation",
1578
+ abort
1579
+ });
1580
+ const audio = json.audio?.data ?? json.output?.audio?.data;
1581
+ const audioURL = json.audio?.url ?? json.output?.audio?.url;
1582
+ const rawResponse = {
1583
+ ...json,
1584
+ ...(json.audio && typeof json.audio === "object"
1585
+ ? { audio: { ...json.audio, data: undefined, data_omitted: audio ? true : undefined } }
1586
+ : {}),
1587
+ ...(json.output && typeof json.output === "object"
1588
+ ? {
1589
+ output: {
1590
+ ...json.output,
1591
+ ...(json.output.audio && typeof json.output.audio === "object"
1592
+ ? { audio: { ...json.output.audio, data: undefined, data_omitted: audio ? true : undefined } }
1593
+ : {})
1594
+ }
1595
+ }
1596
+ : {})
1597
+ };
1598
+ if (audio) {
1170
1599
  const decodedAudio = decodeBase64WithLimit(String(audio), {
1171
1600
  maxBytes: this.responseLimits.speechBytes,
1172
1601
  provider: "qwen",
1173
- endpoint: "audio/speech",
1602
+ endpoint: "multimodal-generation/generation",
1174
1603
  abort
1175
1604
  });
1176
- const rawResponse = {
1177
- ...json,
1178
- ...(json.audio && typeof json.audio === "object"
1179
- ? { audio: { ...json.audio, data: undefined, data_omitted: true } }
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
- : {})
1605
+ return {
1606
+ audio: decodedAudio,
1607
+ mediaType: json.audio?.media_type ?? json.output?.audio?.media_type ?? "audio/pcm",
1608
+ rawResponse
1191
1609
  };
1192
- return { audio: decodedAudio, mediaType: json.audio?.media_type ?? json.output?.audio?.media_type ?? "audio/mpeg", rawResponse };
1193
1610
  }
1194
- if (!response.ok) {
1195
- const body = await readErrorBodyWithLimit(response, this.responseLimits.errorBodyBytes);
1196
- throw new ProviderHTTPError(`Qwen request failed with status ${response.status}.`, response.status, {
1611
+ if (typeof audioURL !== "string" || !/^https?:\/\//i.test(audioURL)) {
1612
+ throw new ProviderHTTPError("Qwen speech response did not contain audio data or a downloadable audio URL.", 502);
1613
+ }
1614
+ const audioResponse = await withRetry(() => downloadQwenSpeechAudio(audioURL, this.fetcher, signal, this.speechAudioURLValidator, this.speechAudioMaxRedirects), input);
1615
+ if (!audioResponse.ok) {
1616
+ const body = await readErrorBodyWithLimit(audioResponse, this.responseLimits.errorBodyBytes);
1617
+ throw new ProviderHTTPError(`Qwen audio download failed with status ${audioResponse.status}.`, audioResponse.status, {
1197
1618
  responseBody: body
1198
1619
  });
1199
1620
  }
1200
1621
  return {
1201
- audio: await readBodyWithLimit(response, {
1622
+ audio: await readBodyWithLimit(audioResponse, {
1202
1623
  maxBytes: this.responseLimits.speechBytes,
1203
1624
  provider: "qwen",
1204
- endpoint: "audio/speech",
1625
+ endpoint: "speech-audio-download",
1205
1626
  abort
1206
1627
  }),
1207
- mediaType: response.headers.get("content-type") ?? "audio/mpeg"
1628
+ mediaType: audioResponse.headers.get("content-type") ?? json.output?.audio?.media_type ?? "audio/wav",
1629
+ rawResponse
1208
1630
  };
1209
1631
  }
1210
1632
  finally {
@@ -1213,7 +1635,7 @@ class QwenSpeechModel {
1213
1635
  }
1214
1636
  }
1215
1637
  const normalizeGeneratedMedia = (item, fallbackMimeType) => ({
1216
- uri: item.url ?? item.uri,
1638
+ uri: item.url ?? item.uri ?? item.image ?? item.video_url,
1217
1639
  data: item.b64_json || item.base64 ? Uint8Array.from(Buffer.from(item.b64_json ?? item.base64, "base64")) : undefined,
1218
1640
  mediaType: item.mime_type ?? item.mediaType ?? fallbackMimeType,
1219
1641
  text: item.revised_prompt ?? item.text,
@@ -1222,15 +1644,13 @@ const normalizeGeneratedMedia = (item, fallbackMimeType) => ({
1222
1644
  class QwenImageGenerationModel {
1223
1645
  modelId;
1224
1646
  apiKey;
1225
- baseURL;
1226
1647
  taskBaseURL;
1227
1648
  fetcher;
1228
1649
  provider = "qwen";
1229
1650
  capabilities = imageGenerationCapabilities;
1230
- constructor(modelId, apiKey, baseURL, taskBaseURL, fetcher) {
1651
+ constructor(modelId, apiKey, taskBaseURL, fetcher) {
1231
1652
  this.modelId = modelId;
1232
1653
  this.apiKey = apiKey;
1233
- this.baseURL = baseURL;
1234
1654
  this.taskBaseURL = taskBaseURL;
1235
1655
  this.fetcher = fetcher;
1236
1656
  }
@@ -1238,25 +1658,53 @@ class QwenImageGenerationModel {
1238
1658
  const { signal, cleanup } = withTimeoutSignal(input);
1239
1659
  const providerOptions = { ...(input.providerOptions ?? {}) };
1240
1660
  delete providerOptions.endpoint;
1241
- const endpoint = `${this.baseURL}/images/generations`;
1661
+ const nestedParameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
1662
+ ? providerOptions.parameters
1663
+ : {};
1664
+ delete providerOptions.parameters;
1665
+ delete providerOptions.model;
1666
+ delete providerOptions.input;
1667
+ const endpoint = `${this.taskBaseURL}/services/aigc/multimodal-generation/generation`;
1668
+ const sizeFromAspectRatio = {
1669
+ "1:1": "2048*2048",
1670
+ "16:9": "1664*928",
1671
+ "9:16": "928*1664",
1672
+ "4:3": "1472*1104",
1673
+ "3:4": "1104*1472"
1674
+ };
1242
1675
  try {
1243
1676
  const response = await withRetry(() => this.fetcher(endpoint, {
1244
1677
  method: "POST",
1245
1678
  headers: jsonHeaders(this.apiKey),
1246
1679
  signal,
1247
1680
  body: JSON.stringify({
1248
- ...providerOptions,
1249
1681
  model: this.modelId,
1250
- prompt: input.prompt,
1251
- n: input.count,
1252
- size: input.size,
1253
- negative_prompt: input.negativePrompt,
1254
- response_format: providerOptions.response_format,
1255
- input_image: input.images?.[0]?.uri
1682
+ input: {
1683
+ messages: [
1684
+ {
1685
+ role: "user",
1686
+ content: [
1687
+ ...(input.images ?? []).map((image) => ({ image: mediaValue(image) })),
1688
+ { text: input.prompt }
1689
+ ]
1690
+ }
1691
+ ]
1692
+ },
1693
+ parameters: {
1694
+ ...providerOptions,
1695
+ ...nestedParameters,
1696
+ n: input.count,
1697
+ size: input.size ?? (input.aspectRatio ? sizeFromAspectRatio[input.aspectRatio] : undefined),
1698
+ negative_prompt: input.negativePrompt
1699
+ }
1256
1700
  })
1257
1701
  }), input);
1258
1702
  const json = await parseJson(response);
1259
- const data = json.data ?? json.output?.results ?? json.output?.images ?? [];
1703
+ const data = json.output?.choices?.flatMap((choice) => choice.message?.content ?? []) ??
1704
+ json.output?.results ??
1705
+ json.output?.images ??
1706
+ json.data ??
1707
+ [];
1260
1708
  return {
1261
1709
  images: data.map((item) => normalizeGeneratedMedia(item, input.outputMimeType ?? "image/png")),
1262
1710
  text: json.output?.text,
@@ -1283,20 +1731,49 @@ class QwenVideoGenerationModel {
1283
1731
  }
1284
1732
  async generateVideo(input) {
1285
1733
  const { signal, cleanup } = withTimeoutSignal(input);
1734
+ const providerOptions = { ...(input.providerOptions ?? {}) };
1735
+ delete providerOptions.endpoint;
1736
+ const nestedInput = providerOptions.input && typeof providerOptions.input === "object"
1737
+ ? providerOptions.input
1738
+ : {};
1739
+ const nestedParameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
1740
+ ? providerOptions.parameters
1741
+ : {};
1742
+ delete providerOptions.input;
1743
+ delete providerOptions.parameters;
1744
+ delete providerOptions.model;
1745
+ const isWan27 = /^wan2\.7(?:$|-)/i.test(this.modelId);
1286
1746
  try {
1287
- const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/video-generation/generation`, {
1747
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/video-generation/video-synthesis`, {
1288
1748
  method: "POST",
1289
1749
  headers: { ...jsonHeaders(this.apiKey), "X-DashScope-Async": "enable" },
1290
1750
  signal,
1291
1751
  body: JSON.stringify({
1292
- ...input.providerOptions,
1293
1752
  model: this.modelId,
1294
- input: { prompt: input.prompt, img_url: input.image?.uri, negative_prompt: input.negativePrompt },
1295
- parameters: { size: input.aspectRatio, duration: input.durationSeconds, n: input.count, output_storage_uri: input.outputStorageUri }
1753
+ input: {
1754
+ ...nestedInput,
1755
+ prompt: input.prompt,
1756
+ negative_prompt: input.negativePrompt,
1757
+ ...(input.image
1758
+ ? isWan27
1759
+ ? { media: [{ type: "first_frame", url: mediaValue(input.image) }] }
1760
+ : { img_url: mediaValue(input.image) }
1761
+ : {})
1762
+ },
1763
+ parameters: {
1764
+ ...providerOptions,
1765
+ ...nestedParameters,
1766
+ ratio: input.aspectRatio,
1767
+ duration: input.durationSeconds,
1768
+ n: input.count,
1769
+ output_storage_uri: input.outputStorageUri
1770
+ }
1296
1771
  })
1297
1772
  }), input);
1298
1773
  const json = await parseJson(response);
1299
- const items = json.output?.results ?? json.output?.videos ?? [];
1774
+ const items = json.output?.results ??
1775
+ json.output?.videos ??
1776
+ (json.output?.video_url ? [{ url: json.output.video_url }] : []);
1300
1777
  return {
1301
1778
  videos: items.map((item) => normalizeGeneratedMedia(item, "video/mp4")),
1302
1779
  operationName: json.output?.task_id ?? json.task_id,
@@ -1311,23 +1788,47 @@ class QwenVideoGenerationModel {
1311
1788
  class QwenRerankModelImpl {
1312
1789
  modelId;
1313
1790
  apiKey;
1314
- baseURL;
1791
+ taskBaseURL;
1315
1792
  fetcher;
1316
1793
  provider = "qwen";
1317
- constructor(modelId, apiKey, baseURL, fetcher) {
1794
+ constructor(modelId, apiKey, taskBaseURL, fetcher) {
1318
1795
  this.modelId = modelId;
1319
1796
  this.apiKey = apiKey;
1320
- this.baseURL = baseURL;
1797
+ this.taskBaseURL = taskBaseURL;
1321
1798
  this.fetcher = fetcher;
1322
1799
  }
1323
1800
  async rerank(input) {
1324
1801
  const { signal, cleanup } = withTimeoutSignal(input);
1802
+ const isTextRerank = /^qwen3-rerank(?:$|-)/i.test(this.modelId);
1803
+ if (isTextRerank &&
1804
+ (typeof input.query !== "string" || input.documents.some((document) => typeof document !== "string"))) {
1805
+ throw new UnsupportedFeatureError("qwen3-rerank accepts text queries and text documents only.");
1806
+ }
1807
+ const toRerankContent = (value) => {
1808
+ if (typeof value === "string") {
1809
+ return /^qwen3-vl-rerank(?:$|-)/i.test(this.modelId) ? { text: value } : value;
1810
+ }
1811
+ return toMultimodalEmbeddingContent(value);
1812
+ };
1325
1813
  try {
1326
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/rerank`, {
1814
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/rerank/text-rerank/text-rerank`, {
1327
1815
  method: "POST",
1328
1816
  headers: jsonHeaders(this.apiKey),
1329
1817
  signal,
1330
- body: JSON.stringify({ ...input.providerOptions, model: this.modelId, query: input.query, documents: input.documents, top_n: input.topN })
1818
+ body: JSON.stringify({
1819
+ model: this.modelId,
1820
+ input: {
1821
+ query: isTextRerank ? input.query : toRerankContent(input.query),
1822
+ documents: isTextRerank
1823
+ ? input.documents
1824
+ : input.documents.map(toRerankContent)
1825
+ },
1826
+ parameters: {
1827
+ return_documents: true,
1828
+ ...input.providerOptions,
1829
+ top_n: input.topN
1830
+ }
1831
+ })
1331
1832
  }), input);
1332
1833
  const json = await parseJson(response);
1333
1834
  return {
@@ -1357,7 +1858,8 @@ class QwenTasksClientImpl {
1357
1858
  async get(input) {
1358
1859
  const { signal, cleanup } = withTimeoutSignal(input);
1359
1860
  try {
1360
- const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/tasks/${input.name}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
1861
+ const taskId = encodeQwenResourceId(input.name, "task");
1862
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/tasks/${taskId}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
1361
1863
  return normalizeOperation(await parseJson(response));
1362
1864
  }
1363
1865
  finally {
@@ -1367,7 +1869,8 @@ class QwenTasksClientImpl {
1367
1869
  async cancel(input) {
1368
1870
  const { signal, cleanup } = withTimeoutSignal(input);
1369
1871
  try {
1370
- const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/tasks/${input.name}/cancel`, { method: "POST", headers: jsonHeaders(this.apiKey), signal }), input);
1872
+ const taskId = encodeQwenResourceId(input.name, "task");
1873
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/tasks/${taskId}/cancel`, { method: "POST", headers: jsonHeaders(this.apiKey), signal }), input);
1371
1874
  return normalizeOperation(await parseJson(response));
1372
1875
  }
1373
1876
  finally {
@@ -1377,23 +1880,103 @@ class QwenTasksClientImpl {
1377
1880
  }
1378
1881
  const parseRealtimeEvent = (payload) => {
1379
1882
  const type = String(payload.type ?? "");
1380
- if (type.includes("text.delta") && typeof payload.delta === "string") {
1381
- return [{ type: "realtime-text-delta", textDelta: payload.delta, providerMetadata: payload }];
1382
- }
1383
- if (type.includes("audio.delta") && typeof payload.delta === "string") {
1384
- return [{ type: "realtime-audio-output", audio: Uint8Array.from(Buffer.from(payload.delta, "base64")), mediaType: "audio/pcm", providerMetadata: payload }];
1883
+ const metadata = payload;
1884
+ const itemId = typeof payload.item_id === "string" ? payload.item_id : undefined;
1885
+ const responseId = typeof payload.response_id === "string" ? payload.response_id : undefined;
1886
+ if (type === "response.text.delta" && typeof payload.delta === "string") {
1887
+ return [{ type: "realtime-text-delta", textDelta: payload.delta, itemId, responseId, providerMetadata: metadata }];
1888
+ }
1889
+ if (type === "response.audio.delta" && typeof payload.delta === "string") {
1890
+ return [{
1891
+ type: "realtime-audio-output",
1892
+ audio: Uint8Array.from(Buffer.from(payload.delta, "base64")),
1893
+ mediaType: "audio/pcm",
1894
+ sampleRateHz: 24_000,
1895
+ channels: 1,
1896
+ itemId,
1897
+ responseId,
1898
+ providerMetadata: metadata
1899
+ }];
1900
+ }
1901
+ if (type === "response.audio_transcript.delta" && typeof payload.delta === "string") {
1902
+ return [{ type: "realtime-transcript", text: payload.delta, role: "assistant", isFinal: false, itemId, responseId, providerMetadata: metadata }];
1903
+ }
1904
+ if (type === "response.audio_transcript.done" && typeof payload.transcript === "string") {
1905
+ return [{ type: "realtime-transcript", text: payload.transcript, role: "assistant", isFinal: true, itemId, responseId, providerMetadata: metadata }];
1906
+ }
1907
+ if (type === "conversation.item.input_audio_transcription.delta") {
1908
+ const text = `${typeof payload.text === "string" ? payload.text : ""}${typeof payload.stash === "string" ? payload.stash : ""}`;
1909
+ return [{ type: "realtime-transcript", text, role: "user", isFinal: false, itemId, providerMetadata: metadata }];
1910
+ }
1911
+ if (type === "conversation.item.input_audio_transcription.completed" && typeof payload.transcript === "string") {
1912
+ return [{ type: "realtime-transcript", text: payload.transcript, role: "user", isFinal: true, itemId, providerMetadata: metadata }];
1913
+ }
1914
+ if (type === "response.function_call_arguments.done") {
1915
+ return [{
1916
+ type: "realtime-tool-call",
1917
+ toolCall: {
1918
+ id: String(payload.call_id ?? payload.item_id ?? "qwen-tool-call"),
1919
+ name: String(payload.name ?? ""),
1920
+ input: JSON.parse(typeof payload.arguments === "string" ? payload.arguments : "{}")
1921
+ }
1922
+ }];
1385
1923
  }
1386
- if (type.includes("transcript") && typeof payload.text === "string") {
1387
- return [{ type: "realtime-transcript", text: payload.text, role: type.includes("input") ? "user" : "assistant", isFinal: type.includes("done"), providerMetadata: payload }];
1924
+ if (type === "response.done") {
1925
+ const response = payload.response;
1926
+ return [{ type: "realtime-response-complete", reason: typeof response?.status === "string" ? response.status : undefined, providerMetadata: metadata }];
1388
1927
  }
1389
- if (type.includes("completed") || type.includes("done")) {
1390
- return [{ type: "realtime-response-complete", providerMetadata: payload }];
1928
+ if (type === "session.finished") {
1929
+ return [{ type: "realtime-end", reason: "finished", providerMetadata: metadata }];
1391
1930
  }
1392
- if (type.includes("error")) {
1393
- return [{ type: "realtime-error", message: String(payload.error ?? payload.message ?? "Qwen realtime error"), providerMetadata: payload }];
1931
+ if (type === "error" || type.endsWith(".failed")) {
1932
+ const error = payload.error;
1933
+ return [{
1934
+ type: "realtime-error",
1935
+ message: String(error?.message ?? payload.message ?? "Qwen realtime error"),
1936
+ providerMetadata: metadata
1937
+ }];
1394
1938
  }
1395
1939
  return [];
1396
1940
  };
1941
+ const mapRealtimeTools = (config) => {
1942
+ const tools = toToolSet(config.tools);
1943
+ if (!tools) {
1944
+ return undefined;
1945
+ }
1946
+ return Object.values(tools).map((tool) => {
1947
+ if (!isCallableToolDefinition(tool)) {
1948
+ throw new UnsupportedFeatureError("Qwen realtime supports callable function tools only.");
1949
+ }
1950
+ return {
1951
+ type: "function",
1952
+ function: {
1953
+ name: tool.name,
1954
+ description: tool.description,
1955
+ parameters: toJSONSchema(tool.schema)
1956
+ }
1957
+ };
1958
+ });
1959
+ };
1960
+ const mapRealtimeSession = (config) => ({
1961
+ ...(config.providerOptions ?? {}),
1962
+ instructions: config.instructions,
1963
+ voice: config.voice,
1964
+ input_audio_format: "pcm",
1965
+ output_audio_format: "pcm",
1966
+ turn_detection: config.turnDetection,
1967
+ input_audio_transcription: typeof config.inputAudioTranscription === "object"
1968
+ ? config.inputAudioTranscription
1969
+ : config.inputAudioTranscription
1970
+ ? {}
1971
+ : undefined,
1972
+ tools: mapRealtimeTools(config)
1973
+ });
1974
+ const encodeRealtimeData = (data) => {
1975
+ if (typeof data === "string") {
1976
+ return data.startsWith("data:") ? data.slice(data.indexOf(",") + 1) : data;
1977
+ }
1978
+ return Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data)).toString("base64");
1979
+ };
1397
1980
  class QwenRealtimeModel {
1398
1981
  modelId;
1399
1982
  apiKey;
@@ -1409,7 +1992,7 @@ class QwenRealtimeModel {
1409
1992
  }
1410
1993
  async connect(config = {}, options) {
1411
1994
  const url = appendQuery(this.realtimeURL, { model: this.modelId });
1412
- const connection = await (this.connectionFactory ?? openWebSocketConnection)(url, { authorization: `Bearer ${this.apiKey}` }, options);
1995
+ const connection = await (this.connectionFactory ?? openQwenRealtimeConnection)(url, { authorization: `Bearer ${this.apiKey}` }, options);
1413
1996
  const session = new CallbackRealtimeSession({
1414
1997
  provider: this.provider,
1415
1998
  modelId: this.modelId,
@@ -1418,13 +2001,21 @@ class QwenRealtimeModel {
1418
2001
  connection,
1419
2002
  callbacks: {
1420
2003
  parseEvent: parseRealtimeEvent,
1421
- buildInitialPayloads: (value) => [{ type: "session.update", session: { instructions: value.instructions, voice: value.voice, ...value.providerOptions } }],
1422
- buildAudioPayloads: (frame) => [{ type: "input_audio_buffer.append", audio: typeof frame.data === "string" ? frame.data : Buffer.from(frame.data).toString("base64") }],
1423
- buildMediaPayloads: (frame) => [{ type: "input_media.append", media_type: frame.mediaType, data: typeof frame.data === "string" ? frame.data : Buffer.from(frame.data).toString("base64") }],
2004
+ buildInitialPayloads: (value) => [{ type: "session.update", session: mapRealtimeSession(value) }],
2005
+ buildAudioPayloads: (frame) => [{ type: "input_audio_buffer.append", audio: encodeRealtimeData(frame.data) }],
2006
+ buildMediaPayloads: (frame) => {
2007
+ if (!/^image\/(jpeg|jpg)$/i.test(frame.mediaType)) {
2008
+ throw new UnsupportedFeatureError("Qwen realtime media frames must be JPEG images.");
2009
+ }
2010
+ return [{ type: "input_image_buffer.append", image: encodeRealtimeData(frame.data) }];
2011
+ },
1424
2012
  buildTextPayloads: (text) => [{ type: "conversation.item.create", item: { type: "message", role: "user", content: [{ type: "input_text", text }] } }, { type: "response.create" }],
1425
- buildToolResultPayloads: (result) => [{ type: "conversation.item.create", item: { type: "function_call_output", call_id: result.toolCallId, output: JSON.stringify(result.isError ? result.error : result.output ?? null) } }],
1426
- buildUpdatePayloads: (value) => [{ type: "session.update", session: { instructions: value.instructions, voice: value.voice, ...value.providerOptions } }],
1427
- buildClosePayloads: () => [{ type: "session.close" }]
2013
+ buildToolResultPayloads: (result) => [
2014
+ { type: "conversation.item.create", item: { type: "function_call_output", call_id: result.toolCallId, output: JSON.stringify(result.isError ? result.error : result.output ?? null) } },
2015
+ { type: "response.create" }
2016
+ ],
2017
+ buildUpdatePayloads: (value) => [{ type: "session.update", session: mapRealtimeSession(value) }],
2018
+ buildClosePayloads: () => []
1428
2019
  }
1429
2020
  });
1430
2021
  await session.initialize();
@@ -1436,25 +2027,33 @@ export const createQwen = (options = {}) => {
1436
2027
  if (!apiKey) {
1437
2028
  throw new ConfigurationError("Missing Qwen API key.");
1438
2029
  }
1439
- const baseURL = options.baseURL ?? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
1440
- const taskBaseURL = options.taskBaseURL ?? qwenTaskBaseURLFrom(baseURL);
1441
- const realtimeURL = options.realtimeURL ?? "wss://dashscope-intl.aliyuncs.com/compatible-mode/v1/realtime";
2030
+ const workspaceURLs = options.workspaceId
2031
+ ? qwenWorkspaceURLs(options.workspaceId, options.region ?? "singapore")
2032
+ : undefined;
2033
+ const baseURL = options.baseURL ?? workspaceURLs?.baseURL ?? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
2034
+ const taskBaseURL = options.taskBaseURL ?? workspaceURLs?.taskBaseURL ?? qwenTaskBaseURLFrom(baseURL);
2035
+ const realtimeURL = options.realtimeURL ??
2036
+ workspaceURLs?.realtimeURL ??
2037
+ "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime";
1442
2038
  const fetcher = options.fetch ?? globalThis.fetch;
1443
2039
  const responseLimits = resolveAudioResponseLimits(options.responseLimits);
2040
+ const speechAudioMaxRedirects = options.speechAudioMaxRedirects ?? 3;
2041
+ if (!Number.isInteger(speechAudioMaxRedirects) || speechAudioMaxRedirects < 0 || speechAudioMaxRedirects > 10) {
2042
+ throw new ConfigurationError("Qwen speechAudioMaxRedirects must be an integer between 0 and 10.");
2043
+ }
1444
2044
  return createProviderAdapter({
1445
2045
  name: "qwen",
1446
2046
  languageModel: (modelId) => new QwenLanguageModel(modelId, apiKey, baseURL, fetcher),
1447
2047
  embeddingModel: (modelId) => new QwenEmbeddingModel(modelId, apiKey, baseURL, fetcher),
1448
2048
  transcriptionModel: (modelId) => new QwenTranscriptionModel(modelId, apiKey, baseURL, fetcher, responseLimits),
1449
- speechModel: (modelId) => new QwenSpeechModel(modelId, apiKey, baseURL, fetcher, responseLimits),
1450
- imageGenerationModel: (modelId) => new QwenImageGenerationModel(modelId, apiKey, baseURL, taskBaseURL, fetcher),
2049
+ speechModel: (modelId) => new QwenSpeechModel(modelId, apiKey, taskBaseURL, fetcher, responseLimits, options.speechAudioURLValidator, speechAudioMaxRedirects),
2050
+ imageGenerationModel: (modelId) => new QwenImageGenerationModel(modelId, apiKey, taskBaseURL, fetcher),
1451
2051
  videoGenerationModel: (modelId) => new QwenVideoGenerationModel(modelId, apiKey, taskBaseURL, fetcher),
1452
2052
  realtimeModel: (modelId) => new QwenRealtimeModel(modelId, apiKey, realtimeURL, options.realtimeConnectionFactory),
1453
2053
  files: new QwenFilesClient(apiKey, baseURL, fetcher),
1454
- fileSearchStores: new QwenFileSearchStoresClient(apiKey, baseURL, fetcher),
1455
2054
  batches: new QwenBatchesClient(apiKey, baseURL, fetcher),
1456
- rerankModel: (modelId) => new QwenRerankModelImpl(modelId, apiKey, baseURL, fetcher),
1457
- multimodalEmbeddingModel: (modelId) => new QwenEmbeddingModel(modelId, apiKey, baseURL, fetcher),
2055
+ rerankModel: (modelId) => new QwenRerankModelImpl(modelId, apiKey, taskBaseURL, fetcher),
2056
+ multimodalEmbeddingModel: (modelId) => new QwenMultimodalEmbeddingModelImpl(modelId, apiKey, taskBaseURL, fetcher),
1458
2057
  tasks: new QwenTasksClientImpl(apiKey, taskBaseURL, fetcher),
1459
2058
  rawFetch: fetcher
1460
2059
  });