@zhivex-ai/qwen 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { toJSONSchema } from "zod";
2
- import { CallbackRealtimeSession, ConfigurationError, ProviderHTTPError, UnsupportedFeatureError, createProviderAdapter, hostedTool, openWebSocketConnection, isCallableToolDefinition, normalizeFinishReason, providerDataPart, streamSSE, withRetry, withTimeoutSignal } from "@zhivex-ai/core";
2
+ import { CallbackRealtimeSession, ConfigurationError, decodeBase64WithLimit, ProviderHTTPError, readBodyWithLimit, readErrorBodyWithLimit, readJsonWithLimit, resolveAudioResponseLimits, UnsupportedFeatureError, createProviderAdapter, hostedTool, openWebSocketConnection, isCallableToolDefinition, normalizeFinishReason, providerDataPart, streamSSE, toToolSet, withRetry, withTimeoutSignal } from "@zhivex-ai/core";
3
3
  const capabilities = {
4
4
  streaming: true,
5
5
  tools: true,
6
6
  structuredOutput: true,
7
7
  jsonMode: true,
8
8
  toolChoice: true,
9
- parallelToolCalls: 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
@@ -159,33 +282,42 @@ const jsonHeaders = (apiKey) => ({
159
282
  "content-type": "application/json",
160
283
  authorization: `Bearer ${apiKey}`
161
284
  });
162
- const parseJson = async (response) => {
285
+ const parseJson = async (response, options = {}) => {
163
286
  if (!response.ok) {
164
- const body = await response.text();
287
+ const body = await readErrorBodyWithLimit(response, options.errorBodyBytes);
165
288
  throw new ProviderHTTPError(`Qwen request failed with status ${response.status}.`, response.status, {
166
289
  responseBody: body
167
290
  });
168
291
  }
169
- return response.json();
292
+ return options.maxBytes
293
+ ? readJsonWithLimit(response, {
294
+ maxBytes: options.maxBytes,
295
+ provider: "qwen",
296
+ endpoint: options.endpoint,
297
+ abort: options.abort
298
+ })
299
+ : response.json();
170
300
  };
171
301
  const mapContentParts = (message) => {
172
- const textParts = message.parts.filter((part) => part.type === "text");
173
- const imageParts = message.parts.filter((part) => part.type === "image");
174
- if (!imageParts.length) {
175
- 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("");
176
308
  }
177
- return [
178
- ...textParts.map((part) => ({
179
- type: "text",
180
- text: part.text
181
- })),
182
- ...imageParts.map((part) => ({
183
- type: "image_url",
184
- image_url: {
185
- url: part.image
186
- }
187
- }))
188
- ];
309
+ return message.parts.flatMap((part) => {
310
+ if (part.type === "text") {
311
+ return [{ type: "text", text: part.text }];
312
+ }
313
+ if (part.type === "image") {
314
+ return [{ type: "image_url", image_url: { url: part.image } }];
315
+ }
316
+ if (part.type === "audio") {
317
+ return [{ type: "input_audio", input_audio: { data: toDataURL(part.data, part.mediaType) } }];
318
+ }
319
+ return [];
320
+ });
189
321
  };
190
322
  const mapMessages = (messages) => messages.map((message) => {
191
323
  if (message.role === "tool") {
@@ -257,7 +389,7 @@ const mapResponsesTools = (tools) => tools
257
389
  };
258
390
  })
259
391
  : undefined;
260
- const mapToolChoice = (toolChoice) => {
392
+ const mapChatToolChoice = (toolChoice) => {
261
393
  if (!toolChoice) {
262
394
  return undefined;
263
395
  }
@@ -271,21 +403,48 @@ const mapToolChoice = (toolChoice) => {
271
403
  }
272
404
  };
273
405
  };
274
- const mapStructuredOutput = (input) => {
275
- if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
406
+ const mapResponsesToolChoice = (toolChoice, tools) => {
407
+ if (!toolChoice) {
276
408
  return undefined;
277
409
  }
278
- return {
279
- type: "json_schema",
280
- json_schema: {
281
- name: input.structuredOutput.name ?? "response",
282
- strict: true,
283
- schema: toJSONSchema(input.structuredOutput.schema)
410
+ if (typeof toolChoice === "string") {
411
+ if (toolChoice === "required" && Object.keys(tools ?? {}).length !== 1) {
412
+ throw new UnsupportedFeatureError('Qwen Responses supports toolChoice "required" only when exactly one tool is provided.');
284
413
  }
414
+ return toolChoice;
415
+ }
416
+ return {
417
+ type: "allowed_tools",
418
+ mode: "required",
419
+ tools: [{ type: "function", name: toolChoice.toolName }]
285
420
  };
286
421
  };
422
+ const mapStructuredOutput = (input) => {
423
+ if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
424
+ return undefined;
425
+ }
426
+ return { type: "json_object" };
427
+ };
428
+ const withStructuredOutputMessages = (input) => {
429
+ if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
430
+ return input.messages;
431
+ }
432
+ const schema = JSON.stringify(toJSONSchema(input.structuredOutput.schema));
433
+ return [
434
+ {
435
+ role: "system",
436
+ parts: [
437
+ {
438
+ type: "text",
439
+ text: `Return only valid JSON matching this JSON Schema: ${schema}`
440
+ }
441
+ ]
442
+ },
443
+ ...input.messages
444
+ ];
445
+ };
287
446
  const hasPreservedReasoning = (messages) => messages.some((message) => message.role === "assistant" && reasoningContentFromMessage(message));
288
- const mapReasoning = (input) => {
447
+ const mapChatReasoning = (input) => {
289
448
  if (!input.reasoning) {
290
449
  return undefined;
291
450
  }
@@ -297,6 +456,63 @@ const mapReasoning = (input) => {
297
456
  ...(hasPreservedReasoning(input.messages) ? { preserve_thinking: true } : {})
298
457
  };
299
458
  };
459
+ const mapResponsesReasoning = (input) => {
460
+ const effort = input.reasoning?.effort;
461
+ if (!effort) {
462
+ return undefined;
463
+ }
464
+ return {
465
+ reasoning: {
466
+ effort: effort === "low" ? "minimal" : effort
467
+ }
468
+ };
469
+ };
470
+ const mapResponsesUsage = (usage) => usage
471
+ ? {
472
+ inputTokens: usage.input_tokens,
473
+ cachedInputTokens: usage.input_tokens_details?.cached_tokens,
474
+ outputTokens: usage.output_tokens,
475
+ reasoningTokens: usage.output_tokens_details?.reasoning_tokens,
476
+ totalTokens: usage.total_tokens
477
+ }
478
+ : undefined;
479
+ const mapChatUsage = (usage) => usage
480
+ ? {
481
+ inputTokens: usage.prompt_tokens,
482
+ cachedInputTokens: usage.prompt_tokens_details?.cached_tokens ?? usage.input_tokens_details?.cached_tokens,
483
+ outputTokens: usage.completion_tokens,
484
+ reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? usage.output_tokens_details?.reasoning_tokens,
485
+ totalTokens: usage.total_tokens
486
+ }
487
+ : undefined;
488
+ const hasHostedTools = (tools) => Object.values(tools ?? {}).some((tool) => !isCallableToolDefinition(tool));
489
+ const hasMessagePart = (messages, type) => messages.some((message) => message.parts.some((part) => part.type === type));
490
+ const resolveApiMode = (modelId, requestedMode, input) => {
491
+ const needsChat = input.maxTokens !== undefined ||
492
+ input.reasoning?.budgetTokens !== undefined ||
493
+ input.structuredOutput?.mode === "native" ||
494
+ hasMessagePart(input.messages, "audio");
495
+ const needsResponses = hasHostedTools(input.tools) || hasMessagePart(input.messages, "file");
496
+ if (needsChat && needsResponses) {
497
+ throw new UnsupportedFeatureError("Qwen cannot combine Responses-only hosted/file inputs with Chat-only maxTokens, audio, reasoning budgets, or structured output.");
498
+ }
499
+ if (isQwenOmniLanguageModel(modelId)) {
500
+ if (requestedMode === "responses" || needsResponses) {
501
+ throw new UnsupportedFeatureError("Qwen Omni uses streaming Chat Completions and does not support Responses hosted tools or file inputs.");
502
+ }
503
+ return "chat";
504
+ }
505
+ if (requestedMode === "responses" && needsChat) {
506
+ throw new UnsupportedFeatureError('Qwen Responses does not process maxTokens, audio input, reasoning budgets, or native structured output; use apiMode "chat" or "auto".');
507
+ }
508
+ if (requestedMode === "chat" && needsResponses) {
509
+ throw new UnsupportedFeatureError('Qwen Chat Completions does not support Responses hosted tools or OCR file inputs.');
510
+ }
511
+ if (requestedMode === "responses" || requestedMode === "chat") {
512
+ return requestedMode;
513
+ }
514
+ return needsChat ? "chat" : "responses";
515
+ };
300
516
  const parseAssistantMessage = (message) => ({
301
517
  role: "assistant",
302
518
  parts: [
@@ -359,6 +575,16 @@ const toResponsesInput = (messages) => {
359
575
  case "image":
360
576
  content.push({ type: "input_image", image_url: part.image });
361
577
  break;
578
+ case "file":
579
+ if (!/^https?:\/\//i.test(part.data)) {
580
+ throw new UnsupportedFeatureError("Qwen Responses file input requires a public HTTP(S) file URL; DashScope Files IDs are reserved for batch jobs.");
581
+ }
582
+ content.push({
583
+ type: "input_file",
584
+ file_url: part.data,
585
+ ...(part.filename ? { filename: part.filename } : {})
586
+ });
587
+ break;
362
588
  case "tool-call":
363
589
  if (message.role === "assistant") {
364
590
  input.push({
@@ -369,6 +595,15 @@ const toResponsesInput = (messages) => {
369
595
  });
370
596
  }
371
597
  break;
598
+ case "provider-data":
599
+ if (message.role === "assistant" &&
600
+ part.provider === "qwen" &&
601
+ part.data &&
602
+ typeof part.data === "object" &&
603
+ typeof part.data.type === "string") {
604
+ input.push(part.data);
605
+ }
606
+ break;
372
607
  }
373
608
  }
374
609
  if (content.length) {
@@ -546,17 +781,18 @@ const streamResponses = async function* (response) {
546
781
  }
547
782
  if (type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
548
783
  const responseData = json.response ?? {};
784
+ if (typeof responseData.id === "string") {
785
+ yield {
786
+ type: "provider-data",
787
+ provider: "qwen",
788
+ data: { responseId: responseData.id }
789
+ };
790
+ }
549
791
  yield {
550
792
  type: "finish",
551
793
  finishReason: normalizeResponsesFinishReason(responseData.status, sawToolCalls),
552
794
  providerFinishReason: responseData.status,
553
- usage: responseData.usage
554
- ? {
555
- inputTokens: responseData.usage.input_tokens,
556
- outputTokens: responseData.usage.output_tokens,
557
- totalTokens: responseData.usage.total_tokens
558
- }
559
- : undefined
795
+ usage: mapResponsesUsage(responseData.usage)
560
796
  };
561
797
  }
562
798
  }
@@ -576,14 +812,17 @@ class QwenLanguageModel {
576
812
  this.capabilities = qwenLanguageCapabilities(modelId);
577
813
  }
578
814
  async generate(input) {
815
+ if (isQwenOmniLanguageModel(this.modelId)) {
816
+ throw new UnsupportedFeatureError(`Qwen Omni model "${this.modelId}" is streaming-only. Use streamText() instead of generateText().`);
817
+ }
579
818
  const { signal, cleanup } = withTimeoutSignal(input);
580
819
  const providerOptions = { ...(input.providerOptions ?? {}) };
581
- const apiMode = providerOptions.apiMode ?? "responses";
820
+ const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input);
582
821
  delete providerOptions.apiMode;
583
822
  try {
584
823
  if (apiMode === "responses") {
585
824
  const previousResponse = getProviderResponseId(input.messages);
586
- const responseProviderOptions = stripHeaderOptions(providerOptions);
825
+ const responseProviderOptions = stripResponsesRequestOptions(providerOptions);
587
826
  const messages = previousResponse && previousResponse.index < input.messages.length - 1
588
827
  ? input.messages.slice(previousResponse.index + 1)
589
828
  : input.messages;
@@ -596,12 +835,10 @@ class QwenLanguageModel {
596
835
  model: this.modelId,
597
836
  ...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
598
837
  ...(messages.length ? { input: toResponsesInput(messages) } : {}),
599
- ...(input.structuredOutput?.mode === "native" ? { response_format: mapStructuredOutput(input) } : {}),
600
838
  tools: mapResponsesTools(input.tools),
601
- tool_choice: mapToolChoice(input.toolChoice),
839
+ tool_choice: mapResponsesToolChoice(input.toolChoice, input.tools),
602
840
  temperature: input.temperature,
603
- max_output_tokens: input.maxTokens,
604
- ...mapReasoning(input),
841
+ ...mapResponsesReasoning(input),
605
842
  stream: false
606
843
  })
607
844
  }), input);
@@ -616,29 +853,25 @@ class QwenLanguageModel {
616
853
  .join(""),
617
854
  finishReason: normalizeResponsesFinishReason(json.status, hasToolCalls),
618
855
  providerFinishReason: json.status,
619
- usage: {
620
- inputTokens: json.usage?.input_tokens,
621
- outputTokens: json.usage?.output_tokens,
622
- totalTokens: json.usage?.total_tokens
623
- },
856
+ usage: mapResponsesUsage(json.usage),
624
857
  rawResponse: json
625
858
  };
626
859
  }
627
860
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
628
861
  method: "POST",
629
- headers: jsonHeaders(this.apiKey),
862
+ headers: providerHeaders(this.apiKey, providerOptions),
630
863
  signal,
631
864
  body: JSON.stringify({
632
- ...providerOptions,
865
+ ...stripHeaderOptions(providerOptions),
633
866
  model: this.modelId,
634
- messages: mapMessages(input.messages),
867
+ messages: mapMessages(withStructuredOutputMessages(input)),
635
868
  tools: mapChatTools(input.tools),
636
- tool_choice: mapToolChoice(input.toolChoice),
869
+ tool_choice: mapChatToolChoice(input.toolChoice),
637
870
  response_format: mapStructuredOutput(input),
638
871
  temperature: input.temperature,
639
872
  max_tokens: input.maxTokens,
640
873
  stream: false,
641
- ...mapReasoning(input)
874
+ ...mapChatReasoning(input)
642
875
  })
643
876
  }), input);
644
877
  const json = await parseJson(response);
@@ -653,11 +886,7 @@ class QwenLanguageModel {
653
886
  .join(""),
654
887
  finishReason: normalizeFinishReason(choice?.finish_reason),
655
888
  providerFinishReason: choice?.finish_reason,
656
- usage: {
657
- inputTokens: json.usage?.prompt_tokens,
658
- outputTokens: json.usage?.completion_tokens,
659
- totalTokens: json.usage?.total_tokens
660
- },
889
+ usage: mapChatUsage(json.usage),
661
890
  rawResponse: json
662
891
  };
663
892
  }
@@ -668,10 +897,14 @@ class QwenLanguageModel {
668
897
  async stream(input) {
669
898
  const { signal, cleanup } = withTimeoutSignal(input);
670
899
  const providerOptions = { ...(input.providerOptions ?? {}) };
671
- const apiMode = providerOptions.apiMode ?? "responses";
900
+ const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input);
672
901
  delete providerOptions.apiMode;
673
902
  if (apiMode === "responses") {
674
- 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;
675
908
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/responses`, {
676
909
  method: "POST",
677
910
  headers: providerHeaders(this.apiKey, providerOptions),
@@ -679,13 +912,12 @@ class QwenLanguageModel {
679
912
  body: JSON.stringify({
680
913
  ...responseProviderOptions,
681
914
  model: this.modelId,
682
- input: toResponsesInput(input.messages),
683
- ...(input.structuredOutput?.mode === "native" ? { response_format: mapStructuredOutput(input) } : {}),
915
+ ...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
916
+ ...(messages.length ? { input: toResponsesInput(messages) } : {}),
684
917
  tools: mapResponsesTools(input.tools),
685
- tool_choice: mapToolChoice(input.toolChoice),
918
+ tool_choice: mapResponsesToolChoice(input.toolChoice, input.tools),
686
919
  temperature: input.temperature,
687
- max_output_tokens: input.maxTokens,
688
- ...mapReasoning(input),
920
+ ...mapResponsesReasoning(input),
689
921
  stream: true
690
922
  })
691
923
  }), input);
@@ -700,20 +932,21 @@ class QwenLanguageModel {
700
932
  }
701
933
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
702
934
  method: "POST",
703
- headers: jsonHeaders(this.apiKey),
935
+ headers: providerHeaders(this.apiKey, providerOptions),
704
936
  signal,
705
937
  body: JSON.stringify({
706
- ...providerOptions,
938
+ ...stripHeaderOptions(providerOptions),
707
939
  model: this.modelId,
708
- messages: mapMessages(input.messages),
940
+ modalities: isQwenOmniLanguageModel(this.modelId) ? providerOptions.modalities ?? ["text"] : undefined,
941
+ messages: mapMessages(withStructuredOutputMessages(input)),
709
942
  tools: mapChatTools(input.tools),
710
- tool_choice: mapToolChoice(input.toolChoice),
943
+ tool_choice: mapChatToolChoice(input.toolChoice),
711
944
  response_format: mapStructuredOutput(input),
712
945
  temperature: input.temperature,
713
946
  max_tokens: input.maxTokens,
714
947
  stream: true,
715
948
  stream_options: { include_usage: true },
716
- ...mapReasoning(input)
949
+ ...mapChatReasoning(input)
717
950
  })
718
951
  }), input);
719
952
  return (async function* () {
@@ -740,37 +973,40 @@ class QwenLanguageModel {
740
973
  yield { type: "text-delta", textDelta: delta.content };
741
974
  }
742
975
  for (const toolCall of delta?.tool_calls ?? []) {
743
- const id = toolCall.id ?? `${toolCall.index}`;
744
- const existing = toolBuffers.get(id) ?? {
976
+ const index = Number(toolCall.index ?? 0);
977
+ const existing = toolBuffers.get(index) ?? {
978
+ id: toolCall.id ?? `${index}`,
745
979
  name: toolCall.function?.name ?? "",
746
- args: ""
980
+ args: "",
981
+ emitted: false
747
982
  };
983
+ existing.id = toolCall.id ?? existing.id;
748
984
  existing.name ||= toolCall.function?.name ?? "";
749
985
  existing.args += toolCall.function?.arguments ?? "";
750
- toolBuffers.set(id, existing);
751
- if (choice?.finish_reason === "tool_calls") {
752
- yield {
753
- type: "tool-call",
754
- toolCall: {
755
- id,
756
- name: existing.name,
757
- input: JSON.parse(existing.args || "{}")
758
- }
759
- };
760
- }
986
+ toolBuffers.set(index, existing);
761
987
  }
762
988
  if (choice?.finish_reason) {
989
+ if (choice.finish_reason === "tool_calls") {
990
+ for (const toolCall of toolBuffers.values()) {
991
+ if (toolCall.emitted) {
992
+ continue;
993
+ }
994
+ toolCall.emitted = true;
995
+ yield {
996
+ type: "tool-call",
997
+ toolCall: {
998
+ id: toolCall.id,
999
+ name: toolCall.name,
1000
+ input: JSON.parse(toolCall.args || "{}")
1001
+ }
1002
+ };
1003
+ }
1004
+ }
763
1005
  yield {
764
1006
  type: "finish",
765
1007
  finishReason: normalizeFinishReason(choice.finish_reason),
766
1008
  providerFinishReason: choice.finish_reason,
767
- usage: json.usage
768
- ? {
769
- inputTokens: json.usage.prompt_tokens,
770
- outputTokens: json.usage.completion_tokens,
771
- totalTokens: json.usage.total_tokens
772
- }
773
- : undefined
1009
+ usage: mapChatUsage(json.usage)
774
1010
  };
775
1011
  }
776
1012
  }
@@ -827,6 +1063,67 @@ class QwenEmbeddingModel {
827
1063
  }
828
1064
  }
829
1065
  }
1066
+ const toMultimodalEmbeddingContent = (value) => {
1067
+ if (typeof value === "string") {
1068
+ return { text: value };
1069
+ }
1070
+ if (value.mediaType.startsWith("image/")) {
1071
+ return { image: mediaValue(value) };
1072
+ }
1073
+ if (value.mediaType.startsWith("video/")) {
1074
+ if (!value.uri) {
1075
+ throw new UnsupportedFeatureError("Qwen multimodal embedding requires a public URL for video input.");
1076
+ }
1077
+ return { video: value.uri };
1078
+ }
1079
+ throw new UnsupportedFeatureError(`Qwen multimodal embedding supports text, image, and video inputs, not "${value.mediaType}".`);
1080
+ };
1081
+ class QwenMultimodalEmbeddingModelImpl {
1082
+ modelId;
1083
+ apiKey;
1084
+ taskBaseURL;
1085
+ fetcher;
1086
+ provider = "qwen";
1087
+ capabilities = {
1088
+ ...embeddingCapabilities,
1089
+ vision: true
1090
+ };
1091
+ constructor(modelId, apiKey, taskBaseURL, fetcher) {
1092
+ this.modelId = modelId;
1093
+ this.apiKey = apiKey;
1094
+ this.taskBaseURL = taskBaseURL;
1095
+ this.fetcher = fetcher;
1096
+ }
1097
+ async embed(input) {
1098
+ const { signal, cleanup } = withTimeoutSignal(input);
1099
+ try {
1100
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/embeddings/multimodal-embedding/multimodal-embedding`, {
1101
+ method: "POST",
1102
+ headers: jsonHeaders(this.apiKey),
1103
+ signal,
1104
+ body: JSON.stringify({
1105
+ model: this.modelId,
1106
+ input: { contents: input.values.map(toMultimodalEmbeddingContent) },
1107
+ ...(input.providerOptions && Object.keys(input.providerOptions).length
1108
+ ? { parameters: input.providerOptions }
1109
+ : {})
1110
+ })
1111
+ }), input);
1112
+ const json = await parseJson(response);
1113
+ return {
1114
+ embeddings: (json.output?.embeddings ?? []).map((entry) => entry.embedding),
1115
+ usage: {
1116
+ inputTokens: json.usage?.input_tokens,
1117
+ totalTokens: json.usage?.total_tokens
1118
+ },
1119
+ rawResponse: json
1120
+ };
1121
+ }
1122
+ finally {
1123
+ cleanup();
1124
+ }
1125
+ }
1126
+ }
830
1127
  const normalizeUploadedFile = (json) => ({
831
1128
  name: json.id ?? json.name ?? json.file_id ?? "",
832
1129
  uri: json.url ?? json.uri,
@@ -837,14 +1134,6 @@ const normalizeUploadedFile = (json) => ({
837
1134
  rawResponse: json,
838
1135
  providerMetadata: json
839
1136
  });
840
- const normalizeFileSearchStore = (json) => ({
841
- name: json.id ?? json.name ?? "",
842
- displayName: json.name ?? json.display_name ?? json.displayName,
843
- createTime: json.created_at ? String(json.created_at) : json.createTime,
844
- updateTime: json.updated_at ? String(json.updated_at) : json.updateTime,
845
- rawResponse: json,
846
- providerMetadata: json
847
- });
848
1137
  const normalizeBatchJob = (json) => ({
849
1138
  name: json.id ?? json.name ?? "",
850
1139
  model: json.model,
@@ -856,10 +1145,13 @@ const normalizeBatchJob = (json) => ({
856
1145
  providerMetadata: json
857
1146
  });
858
1147
  const normalizeOperation = (json) => ({
859
- name: json.task_id ?? json.id ?? json.name ?? "",
1148
+ name: json.output?.task_id ?? json.task_id ?? json.id ?? json.name ?? "",
860
1149
  done: json.output?.task_status ? ["SUCCEEDED", "FAILED", "CANCELED"].includes(json.output.task_status) : json.done,
861
1150
  response: json.output ?? json.response,
862
- error: json.error,
1151
+ error: json.error ??
1152
+ (json.output?.task_status === "FAILED"
1153
+ ? { message: json.output?.message ?? "Qwen task failed.", code: json.output?.code }
1154
+ : undefined),
863
1155
  metadata: json.usage ?? json.metadata,
864
1156
  rawResponse: json
865
1157
  });
@@ -893,7 +1185,8 @@ class QwenFilesClient {
893
1185
  async get(input) {
894
1186
  const { signal, cleanup } = withTimeoutSignal(input);
895
1187
  try {
896
- const 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);
897
1190
  return normalizeUploadedFile(await parseJson(response));
898
1191
  }
899
1192
  finally {
@@ -918,88 +1211,8 @@ class QwenFilesClient {
918
1211
  async delete(input) {
919
1212
  const { signal, cleanup } = withTimeoutSignal(input);
920
1213
  try {
921
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/files/${input.name}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
922
- const json = await parseJson(response);
923
- return { name: input.name, rawResponse: json };
924
- }
925
- finally {
926
- cleanup();
927
- }
928
- }
929
- }
930
- class QwenFileSearchStoresClient {
931
- apiKey;
932
- baseURL;
933
- fetcher;
934
- constructor(apiKey, baseURL, fetcher) {
935
- this.apiKey = apiKey;
936
- this.baseURL = baseURL;
937
- this.fetcher = fetcher;
938
- }
939
- async create(input = {}) {
940
- const { signal, cleanup } = withTimeoutSignal(input);
941
- try {
942
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores`, {
943
- method: "POST",
944
- headers: jsonHeaders(this.apiKey),
945
- signal,
946
- body: JSON.stringify({ ...input.providerOptions, name: input.displayName })
947
- }), input);
948
- return normalizeFileSearchStore(await parseJson(response));
949
- }
950
- finally {
951
- cleanup();
952
- }
953
- }
954
- async upload(input) {
955
- const files = new QwenFilesClient(this.apiKey, this.baseURL, this.fetcher);
956
- const file = await files.upload(input);
957
- return this.importFile({ ...input, fileName: file.name });
958
- }
959
- async importFile(input) {
960
- const { signal, cleanup } = withTimeoutSignal(input);
961
- try {
962
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores/${input.storeName}/files`, {
963
- method: "POST",
964
- headers: jsonHeaders(this.apiKey),
965
- signal,
966
- body: JSON.stringify({ ...input.providerOptions, file_id: input.fileName })
967
- }), input);
968
- return normalizeOperation(await parseJson(response));
969
- }
970
- finally {
971
- cleanup();
972
- }
973
- }
974
- async get(input) {
975
- const { signal, cleanup } = withTimeoutSignal(input);
976
- try {
977
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores/${input.name}`, { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
978
- return normalizeFileSearchStore(await parseJson(response));
979
- }
980
- finally {
981
- cleanup();
982
- }
983
- }
984
- async list(input = {}) {
985
- const { signal, cleanup } = withTimeoutSignal(input);
986
- try {
987
- const response = await withRetry(() => this.fetcher(appendQuery(`${this.baseURL}/file_search_stores`, { limit: input.pageSize, after: input.pageToken }), { method: "GET", headers: jsonHeaders(this.apiKey), signal }), input);
988
- const json = await parseJson(response);
989
- return {
990
- stores: (json.data ?? json.file_search_stores ?? json.fileSearchStores ?? []).map(normalizeFileSearchStore),
991
- nextPageToken: json.next ?? json.nextPageToken,
992
- rawResponse: json
993
- };
994
- }
995
- finally {
996
- cleanup();
997
- }
998
- }
999
- async delete(input) {
1000
- const { signal, cleanup } = withTimeoutSignal(input);
1001
- try {
1002
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/file_search_stores/${input.name}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
1214
+ const fileId = encodeQwenResourceId(input.name, "file");
1215
+ const response = await withRetry(() => this.fetcher(`${this.baseURL}/files/${fileId}`, { method: "DELETE", headers: jsonHeaders(this.apiKey), signal }), input);
1003
1216
  const json = await parseJson(response);
1004
1217
  return { name: input.name, rawResponse: json };
1005
1218
  }
@@ -1018,6 +1231,9 @@ class QwenBatchesClient {
1018
1231
  this.fetcher = fetcher;
1019
1232
  }
1020
1233
  async create(input) {
1234
+ if (!input.fileName) {
1235
+ throw new ConfigurationError("Qwen batch creation requires a JSONL fileName uploaded with purpose \"batch\".");
1236
+ }
1021
1237
  const { signal, cleanup } = withTimeoutSignal(input);
1022
1238
  try {
1023
1239
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/batches`, {
@@ -1029,9 +1245,7 @@ class QwenBatchesClient {
1029
1245
  input_file_id: input.fileName,
1030
1246
  endpoint: input.providerOptions?.endpoint ?? "/v1/chat/completions",
1031
1247
  completion_window: input.providerOptions?.completion_window ?? "24h",
1032
- metadata: input.displayName ? { displayName: input.displayName } : undefined,
1033
- model: input.modelId,
1034
- requests: input.requests
1248
+ metadata: input.displayName ? { displayName: input.displayName } : undefined
1035
1249
  })
1036
1250
  }), input);
1037
1251
  return normalizeBatchJob(await parseJson(response));
@@ -1043,7 +1257,8 @@ class QwenBatchesClient {
1043
1257
  async get(input) {
1044
1258
  const { signal, cleanup } = withTimeoutSignal(input);
1045
1259
  try {
1046
- const 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);
1047
1262
  return normalizeBatchJob(await parseJson(response));
1048
1263
  }
1049
1264
  finally {
@@ -1064,7 +1279,8 @@ class QwenBatchesClient {
1064
1279
  async cancel(input) {
1065
1280
  const { signal, cleanup } = withTimeoutSignal(input);
1066
1281
  try {
1067
- const 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);
1068
1284
  return normalizeBatchJob(await parseJson(response));
1069
1285
  }
1070
1286
  finally {
@@ -1074,7 +1290,8 @@ class QwenBatchesClient {
1074
1290
  async delete(input) {
1075
1291
  const { signal, cleanup } = withTimeoutSignal(input);
1076
1292
  try {
1077
- const 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);
1078
1295
  const json = await parseJson(response);
1079
1296
  return { name: input.name, rawResponse: json };
1080
1297
  }
@@ -1088,30 +1305,70 @@ class QwenTranscriptionModel {
1088
1305
  apiKey;
1089
1306
  baseURL;
1090
1307
  fetcher;
1308
+ responseLimits;
1091
1309
  provider = "qwen";
1092
1310
  capabilities = transcriptionCapabilities;
1093
- constructor(modelId, apiKey, baseURL, fetcher) {
1311
+ constructor(modelId, apiKey, baseURL, fetcher, responseLimits = resolveAudioResponseLimits()) {
1094
1312
  this.modelId = modelId;
1095
1313
  this.apiKey = apiKey;
1096
1314
  this.baseURL = baseURL;
1097
1315
  this.fetcher = fetcher;
1316
+ this.responseLimits = responseLimits;
1098
1317
  }
1099
1318
  async transcribe(input) {
1100
- const { signal, cleanup } = withTimeoutSignal(input);
1101
- const form = new FormData();
1102
- form.set("model", this.modelId);
1103
- form.set("file", await createFile(input.audio.data, input.audio.mediaType, input.audio.filename ?? "audio"));
1104
- if (input.prompt)
1105
- form.set("prompt", input.prompt);
1106
- if (input.language)
1107
- form.set("language", input.language);
1108
- for (const [key, value] of Object.entries(input.providerOptions ?? {})) {
1109
- form.set(key, typeof value === "string" ? value : JSON.stringify(value));
1319
+ if (input.prompt) {
1320
+ throw new UnsupportedFeatureError("Qwen3-ASR-Flash does not expose the common transcription prompt field; use providerOptions for documented asr_options.");
1110
1321
  }
1322
+ const { signal, cleanup, abort } = withTimeoutSignal(input);
1323
+ const providerOptions = { ...(input.providerOptions ?? {}) };
1324
+ const nestedOptions = providerOptions.asr_options && typeof providerOptions.asr_options === "object"
1325
+ ? providerOptions.asr_options
1326
+ : {};
1327
+ delete providerOptions.asr_options;
1328
+ delete providerOptions.model;
1329
+ delete providerOptions.messages;
1330
+ delete providerOptions.stream;
1111
1331
  try {
1112
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/audio/transcriptions`, { method: "POST", headers: { authorization: `Bearer ${this.apiKey}` }, signal, body: form }), input);
1113
- const json = await parseJson(response);
1114
- return { text: json.text ?? json.output?.text ?? "", rawResponse: json };
1332
+ const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
1333
+ method: "POST",
1334
+ headers: jsonHeaders(this.apiKey),
1335
+ signal,
1336
+ body: JSON.stringify({
1337
+ model: this.modelId,
1338
+ messages: [
1339
+ {
1340
+ role: "user",
1341
+ content: [
1342
+ {
1343
+ type: "input_audio",
1344
+ input_audio: {
1345
+ data: toDataURL(input.audio.data, input.audio.mediaType)
1346
+ }
1347
+ }
1348
+ ]
1349
+ }
1350
+ ],
1351
+ stream: false,
1352
+ asr_options: {
1353
+ ...providerOptions,
1354
+ ...nestedOptions,
1355
+ ...(input.language ? { language: input.language } : {})
1356
+ }
1357
+ })
1358
+ }), input);
1359
+ const json = await parseJson(response, {
1360
+ maxBytes: this.responseLimits.transcriptionBytes,
1361
+ errorBodyBytes: this.responseLimits.errorBodyBytes,
1362
+ endpoint: "chat/completions",
1363
+ abort
1364
+ });
1365
+ const content = json.choices?.[0]?.message?.content;
1366
+ const text = Array.isArray(content)
1367
+ ? content.map((part) => part?.text ?? "").join("")
1368
+ : typeof content === "string"
1369
+ ? content
1370
+ : "";
1371
+ return { text, rawResponse: json };
1115
1372
  }
1116
1373
  finally {
1117
1374
  cleanup();
@@ -1121,31 +1378,107 @@ class QwenTranscriptionModel {
1121
1378
  class QwenSpeechModel {
1122
1379
  modelId;
1123
1380
  apiKey;
1124
- baseURL;
1381
+ taskBaseURL;
1125
1382
  fetcher;
1383
+ responseLimits;
1384
+ speechAudioURLValidator;
1385
+ speechAudioMaxRedirects;
1126
1386
  provider = "qwen";
1127
1387
  capabilities = speechCapabilities;
1128
- constructor(modelId, apiKey, baseURL, fetcher) {
1388
+ constructor(modelId, apiKey, taskBaseURL, fetcher, responseLimits = resolveAudioResponseLimits(), speechAudioURLValidator, speechAudioMaxRedirects = 3) {
1129
1389
  this.modelId = modelId;
1130
1390
  this.apiKey = apiKey;
1131
- this.baseURL = baseURL;
1391
+ this.taskBaseURL = taskBaseURL;
1132
1392
  this.fetcher = fetcher;
1393
+ this.responseLimits = responseLimits;
1394
+ this.speechAudioURLValidator = speechAudioURLValidator;
1395
+ this.speechAudioMaxRedirects = speechAudioMaxRedirects;
1133
1396
  }
1134
1397
  async generateSpeech(input) {
1135
- const { signal, cleanup } = withTimeoutSignal(input);
1398
+ const { signal, cleanup, abort } = withTimeoutSignal(input);
1399
+ const providerOptions = { ...(input.providerOptions ?? {}) };
1400
+ const nestedInput = providerOptions.input && typeof providerOptions.input === "object"
1401
+ ? providerOptions.input
1402
+ : {};
1403
+ const parameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
1404
+ ? providerOptions.parameters
1405
+ : undefined;
1406
+ delete providerOptions.input;
1407
+ delete providerOptions.parameters;
1408
+ delete providerOptions.model;
1136
1409
  try {
1137
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/audio/speech`, {
1410
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/multimodal-generation/generation`, {
1138
1411
  method: "POST",
1139
1412
  headers: jsonHeaders(this.apiKey),
1140
1413
  signal,
1141
- body: JSON.stringify({ ...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
+ })
1142
1424
  }), input);
1143
- if (response.headers.get("content-type")?.includes("application/json")) {
1144
- const json = await parseJson(response);
1145
- const audio = json.audio?.data ?? json.output?.audio?.data ?? "";
1146
- return { audio: Uint8Array.from(Buffer.from(audio, "base64")), mediaType: json.audio?.media_type ?? json.output?.audio?.media_type ?? "audio/mpeg", rawResponse: json };
1425
+ const json = await parseJson(response, {
1426
+ maxBytes: this.responseLimits.speechJsonBytes,
1427
+ errorBodyBytes: this.responseLimits.errorBodyBytes,
1428
+ endpoint: "multimodal-generation/generation",
1429
+ abort
1430
+ });
1431
+ const audio = json.audio?.data ?? json.output?.audio?.data;
1432
+ const audioURL = json.audio?.url ?? json.output?.audio?.url;
1433
+ const rawResponse = {
1434
+ ...json,
1435
+ ...(json.audio && typeof json.audio === "object"
1436
+ ? { audio: { ...json.audio, data: undefined, data_omitted: audio ? true : undefined } }
1437
+ : {}),
1438
+ ...(json.output && typeof json.output === "object"
1439
+ ? {
1440
+ output: {
1441
+ ...json.output,
1442
+ ...(json.output.audio && typeof json.output.audio === "object"
1443
+ ? { audio: { ...json.output.audio, data: undefined, data_omitted: audio ? true : undefined } }
1444
+ : {})
1445
+ }
1446
+ }
1447
+ : {})
1448
+ };
1449
+ if (audio) {
1450
+ const decodedAudio = decodeBase64WithLimit(String(audio), {
1451
+ maxBytes: this.responseLimits.speechBytes,
1452
+ provider: "qwen",
1453
+ endpoint: "multimodal-generation/generation",
1454
+ abort
1455
+ });
1456
+ return {
1457
+ audio: decodedAudio,
1458
+ mediaType: json.audio?.media_type ?? json.output?.audio?.media_type ?? "audio/pcm",
1459
+ rawResponse
1460
+ };
1461
+ }
1462
+ if (typeof audioURL !== "string" || !/^https?:\/\//i.test(audioURL)) {
1463
+ throw new ProviderHTTPError("Qwen speech response did not contain audio data or a downloadable audio URL.", 502);
1464
+ }
1465
+ const audioResponse = await withRetry(() => downloadQwenSpeechAudio(audioURL, this.fetcher, signal, this.speechAudioURLValidator, this.speechAudioMaxRedirects), input);
1466
+ if (!audioResponse.ok) {
1467
+ const body = await readErrorBodyWithLimit(audioResponse, this.responseLimits.errorBodyBytes);
1468
+ throw new ProviderHTTPError(`Qwen audio download failed with status ${audioResponse.status}.`, audioResponse.status, {
1469
+ responseBody: body
1470
+ });
1147
1471
  }
1148
- return { audio: new Uint8Array(await response.arrayBuffer()), mediaType: response.headers.get("content-type") ?? "audio/mpeg" };
1472
+ return {
1473
+ audio: await readBodyWithLimit(audioResponse, {
1474
+ maxBytes: this.responseLimits.speechBytes,
1475
+ provider: "qwen",
1476
+ endpoint: "speech-audio-download",
1477
+ abort
1478
+ }),
1479
+ mediaType: audioResponse.headers.get("content-type") ?? json.output?.audio?.media_type ?? "audio/wav",
1480
+ rawResponse
1481
+ };
1149
1482
  }
1150
1483
  finally {
1151
1484
  cleanup();
@@ -1153,7 +1486,7 @@ class QwenSpeechModel {
1153
1486
  }
1154
1487
  }
1155
1488
  const normalizeGeneratedMedia = (item, fallbackMimeType) => ({
1156
- uri: item.url ?? item.uri,
1489
+ uri: item.url ?? item.uri ?? item.image ?? item.video_url,
1157
1490
  data: item.b64_json || item.base64 ? Uint8Array.from(Buffer.from(item.b64_json ?? item.base64, "base64")) : undefined,
1158
1491
  mediaType: item.mime_type ?? item.mediaType ?? fallbackMimeType,
1159
1492
  text: item.revised_prompt ?? item.text,
@@ -1162,39 +1495,67 @@ const normalizeGeneratedMedia = (item, fallbackMimeType) => ({
1162
1495
  class QwenImageGenerationModel {
1163
1496
  modelId;
1164
1497
  apiKey;
1165
- baseURL;
1166
1498
  taskBaseURL;
1167
1499
  fetcher;
1168
1500
  provider = "qwen";
1169
1501
  capabilities = imageGenerationCapabilities;
1170
- constructor(modelId, apiKey, baseURL, taskBaseURL, fetcher) {
1502
+ constructor(modelId, apiKey, taskBaseURL, fetcher) {
1171
1503
  this.modelId = modelId;
1172
1504
  this.apiKey = apiKey;
1173
- this.baseURL = baseURL;
1174
1505
  this.taskBaseURL = taskBaseURL;
1175
1506
  this.fetcher = fetcher;
1176
1507
  }
1177
1508
  async generateImage(input) {
1178
1509
  const { signal, cleanup } = withTimeoutSignal(input);
1179
- const endpoint = String(input.providerOptions?.endpoint ?? `${this.baseURL}/images/generations`);
1510
+ const providerOptions = { ...(input.providerOptions ?? {}) };
1511
+ delete providerOptions.endpoint;
1512
+ const nestedParameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
1513
+ ? providerOptions.parameters
1514
+ : {};
1515
+ delete providerOptions.parameters;
1516
+ delete providerOptions.model;
1517
+ delete providerOptions.input;
1518
+ const endpoint = `${this.taskBaseURL}/services/aigc/multimodal-generation/generation`;
1519
+ const sizeFromAspectRatio = {
1520
+ "1:1": "2048*2048",
1521
+ "16:9": "1664*928",
1522
+ "9:16": "928*1664",
1523
+ "4:3": "1472*1104",
1524
+ "3:4": "1104*1472"
1525
+ };
1180
1526
  try {
1181
1527
  const response = await withRetry(() => this.fetcher(endpoint, {
1182
1528
  method: "POST",
1183
1529
  headers: jsonHeaders(this.apiKey),
1184
1530
  signal,
1185
1531
  body: JSON.stringify({
1186
- ...input.providerOptions,
1187
1532
  model: this.modelId,
1188
- prompt: input.prompt,
1189
- n: input.count,
1190
- size: input.size,
1191
- negative_prompt: input.negativePrompt,
1192
- response_format: input.providerOptions?.response_format,
1193
- 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
+ }
1194
1551
  })
1195
1552
  }), input);
1196
1553
  const json = await parseJson(response);
1197
- 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
+ [];
1198
1559
  return {
1199
1560
  images: data.map((item) => normalizeGeneratedMedia(item, input.outputMimeType ?? "image/png")),
1200
1561
  text: json.output?.text,
@@ -1221,20 +1582,49 @@ class QwenVideoGenerationModel {
1221
1582
  }
1222
1583
  async generateVideo(input) {
1223
1584
  const { signal, cleanup } = withTimeoutSignal(input);
1585
+ const providerOptions = { ...(input.providerOptions ?? {}) };
1586
+ delete providerOptions.endpoint;
1587
+ const nestedInput = providerOptions.input && typeof providerOptions.input === "object"
1588
+ ? providerOptions.input
1589
+ : {};
1590
+ const nestedParameters = providerOptions.parameters && typeof providerOptions.parameters === "object"
1591
+ ? providerOptions.parameters
1592
+ : {};
1593
+ delete providerOptions.input;
1594
+ delete providerOptions.parameters;
1595
+ delete providerOptions.model;
1596
+ const isWan27 = /^wan2\.7(?:$|-)/i.test(this.modelId);
1224
1597
  try {
1225
- const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/video-generation/generation`, {
1598
+ const response = await withRetry(() => this.fetcher(`${this.taskBaseURL}/services/aigc/video-generation/video-synthesis`, {
1226
1599
  method: "POST",
1227
1600
  headers: { ...jsonHeaders(this.apiKey), "X-DashScope-Async": "enable" },
1228
1601
  signal,
1229
1602
  body: JSON.stringify({
1230
- ...input.providerOptions,
1231
1603
  model: this.modelId,
1232
- input: { prompt: input.prompt, img_url: input.image?.uri, negative_prompt: input.negativePrompt },
1233
- 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
+ }
1234
1622
  })
1235
1623
  }), input);
1236
1624
  const json = await parseJson(response);
1237
- 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 }] : []);
1238
1628
  return {
1239
1629
  videos: items.map((item) => normalizeGeneratedMedia(item, "video/mp4")),
1240
1630
  operationName: json.output?.task_id ?? json.task_id,
@@ -1250,22 +1640,56 @@ class QwenRerankModelImpl {
1250
1640
  modelId;
1251
1641
  apiKey;
1252
1642
  baseURL;
1643
+ taskBaseURL;
1253
1644
  fetcher;
1254
1645
  provider = "qwen";
1255
- constructor(modelId, apiKey, baseURL, fetcher) {
1646
+ constructor(modelId, apiKey, baseURL, taskBaseURL, fetcher) {
1256
1647
  this.modelId = modelId;
1257
1648
  this.apiKey = apiKey;
1258
1649
  this.baseURL = baseURL;
1650
+ this.taskBaseURL = taskBaseURL;
1259
1651
  this.fetcher = fetcher;
1260
1652
  }
1261
1653
  async rerank(input) {
1262
1654
  const { signal, cleanup } = withTimeoutSignal(input);
1655
+ const isCompatibleRerank = /^qwen3-rerank(?:$|-)/i.test(this.modelId);
1656
+ if (isCompatibleRerank &&
1657
+ (typeof input.query !== "string" || input.documents.some((document) => typeof document !== "string"))) {
1658
+ throw new UnsupportedFeatureError("qwen3-rerank accepts text queries and text documents only.");
1659
+ }
1660
+ const toRerankContent = (value) => {
1661
+ if (typeof value === "string") {
1662
+ return /^qwen3-vl-rerank(?:$|-)/i.test(this.modelId) ? { text: value } : value;
1663
+ }
1664
+ return toMultimodalEmbeddingContent(value);
1665
+ };
1263
1666
  try {
1264
- const response = await withRetry(() => this.fetcher(`${this.baseURL}/rerank`, {
1667
+ const response = await withRetry(() => this.fetcher(isCompatibleRerank
1668
+ ? `${this.baseURL}/reranks`
1669
+ : `${this.taskBaseURL}/services/rerank/text-rerank/text-rerank`, {
1265
1670
  method: "POST",
1266
1671
  headers: jsonHeaders(this.apiKey),
1267
1672
  signal,
1268
- body: JSON.stringify({ ...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
+ })
1269
1693
  }), input);
1270
1694
  const json = await parseJson(response);
1271
1695
  return {
@@ -1295,7 +1719,8 @@ class QwenTasksClientImpl {
1295
1719
  async get(input) {
1296
1720
  const { signal, cleanup } = withTimeoutSignal(input);
1297
1721
  try {
1298
- const 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);
1299
1724
  return normalizeOperation(await parseJson(response));
1300
1725
  }
1301
1726
  finally {
@@ -1305,7 +1730,8 @@ class QwenTasksClientImpl {
1305
1730
  async cancel(input) {
1306
1731
  const { signal, cleanup } = withTimeoutSignal(input);
1307
1732
  try {
1308
- const 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);
1309
1735
  return normalizeOperation(await parseJson(response));
1310
1736
  }
1311
1737
  finally {
@@ -1315,23 +1741,103 @@ class QwenTasksClientImpl {
1315
1741
  }
1316
1742
  const parseRealtimeEvent = (payload) => {
1317
1743
  const type = String(payload.type ?? "");
1318
- if (type.includes("text.delta") && typeof payload.delta === "string") {
1319
- return [{ type: "realtime-text-delta", textDelta: payload.delta, providerMetadata: payload }];
1320
- }
1321
- if (type.includes("audio.delta") && typeof payload.delta === "string") {
1322
- 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
+ }];
1323
1784
  }
1324
- if (type.includes("transcript") && typeof payload.text === "string") {
1325
- 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 }];
1326
1788
  }
1327
- if (type.includes("completed") || type.includes("done")) {
1328
- return [{ type: "realtime-response-complete", providerMetadata: payload }];
1789
+ if (type === "session.finished") {
1790
+ return [{ type: "realtime-end", reason: "finished", providerMetadata: metadata }];
1329
1791
  }
1330
- if (type.includes("error")) {
1331
- 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
+ }];
1332
1799
  }
1333
1800
  return [];
1334
1801
  };
1802
+ const mapRealtimeTools = (config) => {
1803
+ const tools = toToolSet(config.tools);
1804
+ if (!tools) {
1805
+ return undefined;
1806
+ }
1807
+ return Object.values(tools).map((tool) => {
1808
+ if (!isCallableToolDefinition(tool)) {
1809
+ throw new UnsupportedFeatureError("Qwen realtime supports callable function tools only.");
1810
+ }
1811
+ return {
1812
+ type: "function",
1813
+ function: {
1814
+ name: tool.name,
1815
+ description: tool.description,
1816
+ parameters: toJSONSchema(tool.schema)
1817
+ }
1818
+ };
1819
+ });
1820
+ };
1821
+ const mapRealtimeSession = (config) => ({
1822
+ ...(config.providerOptions ?? {}),
1823
+ instructions: config.instructions,
1824
+ voice: config.voice,
1825
+ input_audio_format: "pcm",
1826
+ output_audio_format: "pcm",
1827
+ turn_detection: config.turnDetection,
1828
+ input_audio_transcription: typeof config.inputAudioTranscription === "object"
1829
+ ? config.inputAudioTranscription
1830
+ : config.inputAudioTranscription
1831
+ ? {}
1832
+ : undefined,
1833
+ tools: mapRealtimeTools(config)
1834
+ });
1835
+ const encodeRealtimeData = (data) => {
1836
+ if (typeof data === "string") {
1837
+ return data.startsWith("data:") ? data.slice(data.indexOf(",") + 1) : data;
1838
+ }
1839
+ return Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data)).toString("base64");
1840
+ };
1335
1841
  class QwenRealtimeModel {
1336
1842
  modelId;
1337
1843
  apiKey;
@@ -1356,13 +1862,21 @@ class QwenRealtimeModel {
1356
1862
  connection,
1357
1863
  callbacks: {
1358
1864
  parseEvent: parseRealtimeEvent,
1359
- buildInitialPayloads: (value) => [{ type: "session.update", session: { instructions: value.instructions, voice: value.voice, ...value.providerOptions } }],
1360
- buildAudioPayloads: (frame) => [{ type: "input_audio_buffer.append", audio: typeof frame.data === "string" ? frame.data : Buffer.from(frame.data).toString("base64") }],
1361
- 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
+ },
1362
1873
  buildTextPayloads: (text) => [{ type: "conversation.item.create", item: { type: "message", role: "user", content: [{ type: "input_text", text }] } }, { type: "response.create" }],
1363
- 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) } }],
1364
- buildUpdatePayloads: (value) => [{ type: "session.update", session: { instructions: value.instructions, voice: value.voice, ...value.providerOptions } }],
1365
- 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: () => []
1366
1880
  }
1367
1881
  });
1368
1882
  await session.initialize();
@@ -1374,24 +1888,33 @@ export const createQwen = (options = {}) => {
1374
1888
  if (!apiKey) {
1375
1889
  throw new ConfigurationError("Missing Qwen API key.");
1376
1890
  }
1377
- const baseURL = options.baseURL ?? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
1378
- const taskBaseURL = options.taskBaseURL ?? qwenTaskBaseURLFrom(baseURL);
1379
- 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";
1380
1899
  const fetcher = options.fetch ?? globalThis.fetch;
1900
+ const responseLimits = resolveAudioResponseLimits(options.responseLimits);
1901
+ const speechAudioMaxRedirects = options.speechAudioMaxRedirects ?? 3;
1902
+ if (!Number.isInteger(speechAudioMaxRedirects) || speechAudioMaxRedirects < 0 || speechAudioMaxRedirects > 10) {
1903
+ throw new ConfigurationError("Qwen speechAudioMaxRedirects must be an integer between 0 and 10.");
1904
+ }
1381
1905
  return createProviderAdapter({
1382
1906
  name: "qwen",
1383
1907
  languageModel: (modelId) => new QwenLanguageModel(modelId, apiKey, baseURL, fetcher),
1384
1908
  embeddingModel: (modelId) => new QwenEmbeddingModel(modelId, apiKey, baseURL, fetcher),
1385
- transcriptionModel: (modelId) => new QwenTranscriptionModel(modelId, apiKey, baseURL, fetcher),
1386
- speechModel: (modelId) => new QwenSpeechModel(modelId, apiKey, baseURL, fetcher),
1387
- imageGenerationModel: (modelId) => new QwenImageGenerationModel(modelId, apiKey, baseURL, taskBaseURL, fetcher),
1909
+ transcriptionModel: (modelId) => new QwenTranscriptionModel(modelId, apiKey, baseURL, fetcher, responseLimits),
1910
+ speechModel: (modelId) => new QwenSpeechModel(modelId, apiKey, taskBaseURL, fetcher, responseLimits, options.speechAudioURLValidator, speechAudioMaxRedirects),
1911
+ imageGenerationModel: (modelId) => new QwenImageGenerationModel(modelId, apiKey, taskBaseURL, fetcher),
1388
1912
  videoGenerationModel: (modelId) => new QwenVideoGenerationModel(modelId, apiKey, taskBaseURL, fetcher),
1389
1913
  realtimeModel: (modelId) => new QwenRealtimeModel(modelId, apiKey, realtimeURL, options.realtimeConnectionFactory),
1390
1914
  files: new QwenFilesClient(apiKey, baseURL, fetcher),
1391
- fileSearchStores: new QwenFileSearchStoresClient(apiKey, baseURL, fetcher),
1392
1915
  batches: new QwenBatchesClient(apiKey, baseURL, fetcher),
1393
- rerankModel: (modelId) => new QwenRerankModelImpl(modelId, apiKey, baseURL, fetcher),
1394
- multimodalEmbeddingModel: (modelId) => new QwenEmbeddingModel(modelId, apiKey, baseURL, fetcher),
1916
+ rerankModel: (modelId) => new QwenRerankModelImpl(modelId, apiKey, baseURL, taskBaseURL, fetcher),
1917
+ multimodalEmbeddingModel: (modelId) => new QwenMultimodalEmbeddingModelImpl(modelId, apiKey, taskBaseURL, fetcher),
1395
1918
  tasks: new QwenTasksClientImpl(apiKey, taskBaseURL, fetcher),
1396
1919
  rawFetch: fetcher
1397
1920
  });