@zhivex-ai/qwen 0.7.8 → 0.8.0

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