@zhivex-ai/qwen 0.10.2 → 0.11.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,59 +1,7 @@
1
1
  import { toJSONSchema } from "zod";
2
+ import { capabilities, embeddingCapabilities, imageGenerationCapabilities, realtimeCapabilities, speechCapabilities, transcriptionCapabilities, videoGenerationCapabilities, } from "./capabilities.js";
2
3
  import { CallbackRealtimeSession, ConfigurationError, decodeBase64WithLimit, ProviderHTTPError, ValidationError, assertTrustedEndpoint, readBodyWithLimit, readErrorBodyWithLimit, readJsonWithLimit, resolveAudioResponseLimits, UnsupportedFeatureError, createProviderAdapter, hostedTool, openWebSocketConnection, isCallableToolDefinition, normalizeFinishReason, providerDataPart, streamSSE, toToolSet, withRetry, withTimeoutSignal } from "@zhivex-ai/core";
3
4
  export const QWEN_TOKEN_PLAN_BASE_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1";
4
- const capabilities = {
5
- streaming: true,
6
- tools: true,
7
- structuredOutput: true,
8
- jsonMode: true,
9
- toolChoice: true,
10
- parallelToolCalls: false,
11
- vision: true,
12
- files: false,
13
- audioInput: false,
14
- audioOutput: false,
15
- embeddings: true,
16
- reasoning: false,
17
- webSearch: true,
18
- agentCapabilities: {
19
- supportTier: "tier-b",
20
- toolChoiceNone: true,
21
- approvalRequests: false,
22
- hostedWebSearch: true,
23
- hostedFileSearch: true,
24
- remoteMcp: true,
25
- computerUse: false,
26
- codeExecution: true,
27
- webExtraction: true,
28
- toolsets: false
29
- }
30
- };
31
- const embeddingCapabilities = {
32
- streaming: false,
33
- tools: false,
34
- structuredOutput: false,
35
- jsonMode: false,
36
- toolChoice: false,
37
- parallelToolCalls: false,
38
- vision: false,
39
- files: false,
40
- audioInput: false,
41
- audioOutput: false,
42
- embeddings: true,
43
- reasoning: false,
44
- webSearch: false,
45
- agentCapabilities: {
46
- supportTier: "tier-c",
47
- toolChoiceNone: false,
48
- approvalRequests: false,
49
- hostedWebSearch: false,
50
- hostedFileSearch: false,
51
- remoteMcp: false,
52
- computerUse: false,
53
- codeExecution: false,
54
- toolsets: false
55
- }
56
- };
57
5
  const qwenTaskBaseURLFrom = (baseURL) => {
58
6
  const url = new URL(baseURL);
59
7
  return `${url.protocol}//${url.host}/api/v1`;
@@ -375,8 +323,10 @@ const stripResponsesRequestOptions = (providerOptions) => {
375
323
  };
376
324
  const modelFamily = (modelId) => modelId.toLowerCase();
377
325
  const isQwen38Max = (modelId) => modelFamily(modelId) === "qwen3.8-max";
326
+ const isQwen38Flash = (modelId) => modelFamily(modelId) === "qwen3.8-flash";
378
327
  const isQwen38MaxPreview = (modelId) => modelFamily(modelId) === "qwen3.8-max-preview";
379
- const isQwen38MaxFamily = (modelId) => isQwen38Max(modelId) || isQwen38MaxPreview(modelId);
328
+ const isQwen38ProductionModel = (modelId) => isQwen38Max(modelId) || isQwen38Flash(modelId);
329
+ const isQwen38ReasoningModel = (modelId) => isQwen38ProductionModel(modelId) || isQwen38MaxPreview(modelId);
380
330
  const isQwenTokenPlanURL = (url) => {
381
331
  const path = url.pathname.replace(/\/+$/, "");
382
332
  const tokenPlanURL = new URL(QWEN_TOKEN_PLAN_BASE_URL);
@@ -399,7 +349,7 @@ const validateQwen38BaseURL = (baseURL) => {
399
349
  throw new ConfigurationError(`Qwen qwen3.8-max-preview is Token Plan only. Configure baseURL with QWEN_TOKEN_PLAN_BASE_URL (${QWEN_TOKEN_PLAN_BASE_URL}); pay-as-you-go and workspace endpoints cannot serve this model.`);
400
350
  }
401
351
  };
402
- const validateQwen38MaxBaseURL = (baseURL) => {
352
+ const validateQwen38ProductionBaseURL = (modelId, baseURL) => {
403
353
  let url;
404
354
  try {
405
355
  url = new URL(baseURL);
@@ -408,7 +358,7 @@ const validateQwen38MaxBaseURL = (baseURL) => {
408
358
  return;
409
359
  }
410
360
  if (isQwenTokenPlanURL(url)) {
411
- throw new ConfigurationError("Qwen qwen3.8-max uses standard Model Studio credentials and endpoints; QWEN_TOKEN_PLAN_BASE_URL is reserved for qwen3.8-max-preview.");
361
+ throw new ConfigurationError(`Qwen ${modelId} uses standard Model Studio credentials and endpoints; QWEN_TOKEN_PLAN_BASE_URL is reserved for qwen3.8-max-preview.`);
412
362
  }
413
363
  };
414
364
  const stripQwen38ReasoningOptions = (providerOptions) => {
@@ -428,7 +378,7 @@ const supportsQwenVision = (modelId) => {
428
378
  model.includes("omni") ||
429
379
  model.includes("ocr") ||
430
380
  model.includes("vision") ||
431
- isQwen38Max(modelId) ||
381
+ isQwen38ProductionModel(modelId) ||
432
382
  isQwen38MaxPreview(modelId) ||
433
383
  /^qwen3\.7-plus(?:$|-)/.test(model) ||
434
384
  /^qwen3\.6-flash(?:$|-)/.test(model));
@@ -440,7 +390,7 @@ const qwenLanguageCapabilities = (modelId) => {
440
390
  const tools = supportsQwenTools(modelId);
441
391
  const omni = isQwenOmniLanguageModel(modelId);
442
392
  const reasoning = supportsQwenReasoning(modelId);
443
- const qwen38Max = isQwen38Max(modelId);
393
+ const qwen38Production = isQwen38ProductionModel(modelId);
444
394
  const qwen38MaxPreview = isQwen38MaxPreview(modelId);
445
395
  return {
446
396
  ...capabilities,
@@ -449,12 +399,12 @@ const qwenLanguageCapabilities = (modelId) => {
449
399
  structuredOutput: tools && !omni && !qwen38MaxPreview,
450
400
  jsonMode: tools && !omni && !qwen38MaxPreview,
451
401
  toolChoice: tools,
452
- parallelToolCalls: qwen38Max,
402
+ parallelToolCalls: qwen38Production,
453
403
  webSearch: tools && !omni,
454
- files: supportsQwenFiles(modelId) || qwen38Max,
404
+ files: supportsQwenFiles(modelId) || qwen38Production,
455
405
  audioInput: supportsQwenAudioInput(modelId),
456
406
  reasoning,
457
- reasoningEfforts: qwen38Max
407
+ reasoningEfforts: qwen38Production
458
408
  ? ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
459
409
  : qwen38MaxPreview
460
410
  ? ["minimal", "low", "medium", "high", "xhigh", "max"]
@@ -473,53 +423,6 @@ const qwenLanguageCapabilities = (modelId) => {
473
423
  }
474
424
  };
475
425
  };
476
- const transcriptionCapabilities = {
477
- ...embeddingCapabilities,
478
- audioInput: true
479
- };
480
- const speechCapabilities = {
481
- ...embeddingCapabilities,
482
- audioOutput: true
483
- };
484
- const imageGenerationCapabilities = {
485
- ...embeddingCapabilities,
486
- imageGeneration: true
487
- };
488
- const videoGenerationCapabilities = {
489
- ...embeddingCapabilities,
490
- videoGeneration: true
491
- };
492
- const realtimeCapabilities = {
493
- ...capabilities,
494
- streaming: false,
495
- structuredOutput: false,
496
- jsonMode: false,
497
- toolChoice: false,
498
- parallelToolCalls: false,
499
- files: false,
500
- audioInput: true,
501
- audioOutput: true,
502
- embeddings: false,
503
- reasoning: false,
504
- webSearch: false,
505
- agentCapabilities: {
506
- ...capabilities.agentCapabilities,
507
- toolChoiceNone: true,
508
- hostedWebSearch: false,
509
- hostedFileSearch: false,
510
- remoteMcp: false,
511
- codeExecution: false,
512
- webExtraction: false
513
- },
514
- realtime: {
515
- sessions: true,
516
- audioInput: true,
517
- audioOutput: true,
518
- imageInput: true,
519
- tools: true,
520
- browserTokens: false
521
- }
522
- };
523
426
  const reasoningContentFromMessage = (message) => message.parts
524
427
  .filter((part) => {
525
428
  if (part.type !== "provider-data" || part.provider !== "qwen") {
@@ -683,16 +586,29 @@ const mapResponsesToolChoice = (toolChoice, tools) => {
683
586
  tools: [{ type: "function", name: toolChoice.toolName }]
684
587
  };
685
588
  };
686
- const mapStructuredOutput = (input) => {
589
+ const mapStructuredOutput = (modelId, input) => {
687
590
  if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
688
591
  return undefined;
689
592
  }
593
+ if (isQwen38Flash(modelId)) {
594
+ return {
595
+ type: "json_schema",
596
+ json_schema: {
597
+ name: input.structuredOutput.name ?? "response",
598
+ strict: true,
599
+ schema: toJSONSchema(input.structuredOutput.schema)
600
+ }
601
+ };
602
+ }
690
603
  return { type: "json_object" };
691
604
  };
692
- const withStructuredOutputMessages = (input) => {
605
+ const withStructuredOutputMessages = (modelId, input) => {
693
606
  if (!input.structuredOutput || input.structuredOutput.mode !== "native") {
694
607
  return input.messages;
695
608
  }
609
+ if (isQwen38Flash(modelId)) {
610
+ return input.messages;
611
+ }
696
612
  const schema = JSON.stringify(toJSONSchema(input.structuredOutput.schema));
697
613
  return [
698
614
  {
@@ -766,26 +682,28 @@ const validateQwen38Request = (modelId, input, providerOptions) => {
766
682
  if (sharedBudget !== undefined && sharedBudget > 262_144) {
767
683
  throw new ConfigurationError(`Qwen ${modelId} reasoning.budgetTokens cannot exceed 262144.`);
768
684
  }
769
- if (isQwen38Max(modelId) &&
685
+ if (isQwen38ProductionModel(modelId) &&
770
686
  providerOptions.enable_thinking === false &&
771
687
  ((effort !== undefined && effort !== "none") || budget !== undefined)) {
772
- throw new ConfigurationError("Qwen qwen3.8-max cannot disable thinking while also configuring a reasoning effort or thinking budget.");
688
+ throw new ConfigurationError(`Qwen ${modelId} cannot disable thinking while also configuring a reasoning effort or thinking budget.`);
773
689
  }
774
- if (isQwen38Max(modelId) && providerOptions.enable_thinking === true && effort === "none") {
775
- throw new ConfigurationError('Qwen qwen3.8-max cannot enable thinking with reasoning effort "none".');
690
+ if (isQwen38ProductionModel(modelId) &&
691
+ providerOptions.enable_thinking === true &&
692
+ effort === "none") {
693
+ throw new ConfigurationError(`Qwen ${modelId} cannot enable thinking with reasoning effort "none".`);
776
694
  }
777
695
  const thinkingEnabled = isQwen38MaxPreview(modelId) ||
778
696
  !(effort === "none" || providerOptions.enable_thinking === false);
779
697
  if (thinkingEnabled && (input.toolChoice === "required" || typeof input.toolChoice === "object")) {
780
698
  throw new UnsupportedFeatureError(`Qwen ${modelId} supports toolChoice "required" or a named tool only when thinking is disabled.`);
781
699
  }
782
- if (isQwen38Max(modelId) &&
700
+ if (isQwen38ProductionModel(modelId) &&
783
701
  input.messages.some((message) => message.parts.some((part) => part.type === "file" && !part.mediaType.toLowerCase().startsWith("video/")))) {
784
- throw new UnsupportedFeatureError("Qwen qwen3.8-max FilePart input is reserved for video/* media. Use ImagePart for images.");
702
+ throw new UnsupportedFeatureError(`Qwen ${modelId} FilePart input is reserved for video/* media. Use ImagePart for images.`);
785
703
  }
786
704
  };
787
705
  const mapChatReasoning = (modelId, input, providerOptions) => {
788
- if (isQwen38MaxFamily(modelId)) {
706
+ if (isQwen38ReasoningModel(modelId)) {
789
707
  const rawEffort = input.reasoning?.effort ?? providerOptions.reasoning_effort;
790
708
  const thinkingDisabled = rawEffort === "none" || providerOptions.enable_thinking === false;
791
709
  if (thinkingDisabled) {
@@ -818,7 +736,9 @@ const mapChatReasoning = (modelId, input, providerOptions) => {
818
736
  };
819
737
  const mapResponsesReasoning = (modelId, input, providerOptions) => {
820
738
  const effort = input.reasoning?.effort ?? providerOptions.reasoning_effort;
821
- const qwen38Effort = isQwen38Max(modelId) && providerOptions.enable_thinking === false && effort === undefined
739
+ const qwen38Effort = isQwen38ProductionModel(modelId) &&
740
+ providerOptions.enable_thinking === false &&
741
+ effort === undefined
822
742
  ? "none"
823
743
  : effort;
824
744
  if (!qwen38Effort) {
@@ -828,7 +748,7 @@ const mapResponsesReasoning = (modelId, input, providerOptions) => {
828
748
  reasoning: {
829
749
  effort: isQwen38MaxPreview(modelId)
830
750
  ? mapQwen38ChatReasoningEffort(modelId, qwen38Effort)
831
- : isQwen38Max(modelId)
751
+ : isQwen38ProductionModel(modelId)
832
752
  ? qwen38Effort
833
753
  : qwen38Effort === "low"
834
754
  ? "minimal"
@@ -860,7 +780,7 @@ const hasVideoInput = (messages) => messages.some((message) => message.parts.som
860
780
  const hasNonVideoFileInput = (messages) => messages.some((message) => message.parts.some((part) => part.type === "file" && !part.mediaType.toLowerCase().startsWith("video/")));
861
781
  const resolveApiMode = (modelId, requestedMode, input, providerOptions) => {
862
782
  const videoInput = hasVideoInput(input.messages);
863
- if (videoInput && !isQwen38Max(modelId)) {
783
+ if (videoInput && !isQwen38ProductionModel(modelId)) {
864
784
  throw new UnsupportedFeatureError(`Qwen model "${modelId}" does not support video FilePart input through this adapter.`);
865
785
  }
866
786
  const needsChat = input.maxTokens !== undefined ||
@@ -891,25 +811,87 @@ const resolveApiMode = (modelId, requestedMode, input, providerOptions) => {
891
811
  }
892
812
  return needsChat ? "chat" : "responses";
893
813
  };
894
- const parseAssistantMessage = (message) => ({
895
- role: "assistant",
896
- parts: [
897
- ...(typeof message.reasoning_content === "string" && message.reasoning_content
898
- ? [providerDataPart("qwen", { type: "reasoning_content", reasoningContent: message.reasoning_content })]
899
- : []),
900
- ...(typeof message.content === "string" && message.content
901
- ? [{ type: "text", text: message.content }]
902
- : []),
903
- ...((message.tool_calls ?? []).map((call) => ({
904
- type: "tool-call",
905
- toolCall: {
906
- id: call.id,
907
- name: call.function.name,
908
- input: JSON.parse(call.function.arguments ?? "{}")
909
- }
910
- })) ?? [])
911
- ]
912
- });
814
+ const classifyToolCallId = (value) => {
815
+ if (typeof value === "number" && Number.isFinite(value)) {
816
+ return { kind: "transient" };
817
+ }
818
+ if (typeof value !== "string") {
819
+ return { kind: "missing" };
820
+ }
821
+ const trimmed = value.trim();
822
+ if (!trimmed) {
823
+ return { kind: "missing" };
824
+ }
825
+ if (/^\d+$/.test(trimmed)) {
826
+ return { kind: "transient" };
827
+ }
828
+ return { kind: "stable", id: value };
829
+ };
830
+ const stableToolCallId = (value) => {
831
+ const classification = classifyToolCallId(value);
832
+ return classification.kind === "stable" ? classification.id : undefined;
833
+ };
834
+ const qwenResponsesProviderCallId = (value) => typeof value === "string" && value.trim().length > 0 ? value : undefined;
835
+ const existingToolCallIds = (messages) => new Set(messages.flatMap((message) => message.parts.flatMap((part) => part.type === "tool-call" ? [part.toolCall.id] : [])));
836
+ const nextFallbackToolCallGeneration = (surface, input, seenIds) => {
837
+ const prefix = `qwen-${surface}-tool-`;
838
+ let generation = input.messages.length;
839
+ for (const id of seenIds) {
840
+ if (!id.startsWith(prefix))
841
+ continue;
842
+ const separator = id.indexOf("-", prefix.length);
843
+ if (separator < 0)
844
+ continue;
845
+ const generationText = id.slice(prefix.length, separator);
846
+ if (!generationText)
847
+ continue;
848
+ const retainedGeneration = Number(generationText);
849
+ if (Number.isSafeInteger(retainedGeneration) && retainedGeneration >= generation) {
850
+ generation = retainedGeneration + 1;
851
+ }
852
+ }
853
+ return generation;
854
+ };
855
+ const fallbackToolCallId = (surface, generation, index) => `qwen-${surface}-tool-${generation}-${index}`;
856
+ class QwenToolCallIdError extends ValidationError {
857
+ diagnosticCode = "QWEN_DUPLICATE_TOOL_CALL_ID";
858
+ constructor() {
859
+ super("Qwen returned a duplicate tool-call id.");
860
+ this.name = "QwenToolCallIdError";
861
+ }
862
+ }
863
+ const resolveToolCallId = (candidates, fallback, seenIds) => {
864
+ const candidate = candidates.map(stableToolCallId).find((value) => value !== undefined);
865
+ const id = candidate ?? (typeof fallback === "function" ? fallback() : fallback);
866
+ if (seenIds.has(id)) {
867
+ throw new QwenToolCallIdError();
868
+ }
869
+ seenIds.add(id);
870
+ return id;
871
+ };
872
+ const parseAssistantMessage = (message, input) => {
873
+ const seenIds = existingToolCallIds(input.messages);
874
+ const fallbackGeneration = nextFallbackToolCallGeneration("chat", input, seenIds);
875
+ return {
876
+ role: "assistant",
877
+ parts: [
878
+ ...(typeof message.reasoning_content === "string" && message.reasoning_content
879
+ ? [providerDataPart("qwen", { type: "reasoning_content", reasoningContent: message.reasoning_content })]
880
+ : []),
881
+ ...(typeof message.content === "string" && message.content
882
+ ? [{ type: "text", text: message.content }]
883
+ : []),
884
+ ...((message.tool_calls ?? []).map((call, index) => ({
885
+ type: "tool-call",
886
+ toolCall: {
887
+ id: resolveToolCallId([call.id], fallbackToolCallId("chat", fallbackGeneration, index), seenIds),
888
+ name: call.function.name,
889
+ input: JSON.parse(call.function.arguments ?? "{}")
890
+ }
891
+ })) ?? [])
892
+ ]
893
+ };
894
+ };
913
895
  const getProviderResponseId = (messages) => {
914
896
  for (let index = messages.length - 1; index >= 0; index -= 1) {
915
897
  const message = messages[index];
@@ -932,11 +914,16 @@ const getProviderResponseId = (messages) => {
932
914
  };
933
915
  const serializeResponsesToolOutput = (message) => message.parts
934
916
  .filter((part) => part.type === "tool-result")
935
- .map((part) => ({
936
- type: "function_call_output",
937
- call_id: part.toolResult.toolCallId,
938
- output: JSON.stringify(part.toolResult.isError ? part.toolResult.error : part.toolResult.output ?? null)
939
- }));
917
+ .map((part) => {
918
+ const providerCallId = part.toolResult.providerMetadata?.qwenResponsesCallId;
919
+ return {
920
+ type: "function_call_output",
921
+ call_id: typeof providerCallId === "string" && providerCallId.trim().length > 0
922
+ ? providerCallId
923
+ : part.toolResult.toolCallId,
924
+ output: JSON.stringify(part.toolResult.isError ? part.toolResult.error : part.toolResult.output ?? null)
925
+ };
926
+ });
940
927
  const toResponsesInput = (messages) => {
941
928
  const input = [];
942
929
  for (const message of messages) {
@@ -1003,8 +990,10 @@ const parseResponsesProviderData = (item) => {
1003
990
  }
1004
991
  return item;
1005
992
  };
1006
- const parseResponsesAssistantMessage = (json) => {
993
+ const parseResponsesAssistantMessage = (json, input) => {
1007
994
  const parts = [];
995
+ const seenIds = existingToolCallIds(input.messages);
996
+ const fallbackGeneration = nextFallbackToolCallGeneration("responses", input, seenIds);
1008
997
  for (const [index, item] of (json.output ?? []).entries()) {
1009
998
  if (item?.type === "message") {
1010
999
  for (const content of item.content ?? []) {
@@ -1015,12 +1004,17 @@ const parseResponsesAssistantMessage = (json) => {
1015
1004
  continue;
1016
1005
  }
1017
1006
  if (item?.type === "function_call") {
1007
+ const providerCallId = qwenResponsesProviderCallId(item.call_id);
1008
+ const id = resolveToolCallId([item.call_id, item.id], fallbackToolCallId("responses", fallbackGeneration, index), seenIds);
1018
1009
  parts.push({
1019
1010
  type: "tool-call",
1020
1011
  toolCall: {
1021
- id: item.call_id ?? item.id ?? `${item.name}-${index}`,
1012
+ id,
1022
1013
  name: item.name,
1023
- input: JSON.parse(item.arguments ?? "{}")
1014
+ input: JSON.parse(item.arguments ?? "{}"),
1015
+ ...(providerCallId !== undefined && providerCallId !== id
1016
+ ? { providerMetadata: { qwenResponsesCallId: providerCallId } }
1017
+ : {})
1024
1018
  }
1025
1019
  });
1026
1020
  continue;
@@ -1053,9 +1047,30 @@ const normalizeResponsesFinishReason = (status, hasToolCalls) => {
1053
1047
  }
1054
1048
  return normalizeFinishReason(status);
1055
1049
  };
1056
- const streamResponses = async function* (response) {
1050
+ const streamResponses = async function* (response, input) {
1057
1051
  const toolBuffers = new Map();
1052
+ const toolBufferAliases = new Map();
1053
+ const seenIds = existingToolCallIds(input.messages);
1054
+ const fallbackGeneration = nextFallbackToolCallGeneration("responses", input, seenIds);
1058
1055
  let sawToolCalls = false;
1056
+ const bufferKey = (outputIndex, candidates, fallbackId) => {
1057
+ const stableIds = candidates
1058
+ .map(stableToolCallId)
1059
+ .filter((value) => value !== undefined);
1060
+ if ((typeof outputIndex === "number" && Number.isSafeInteger(outputIndex) && outputIndex >= 0) ||
1061
+ (typeof outputIndex === "string" && outputIndex.trim().length > 0)) {
1062
+ const key = `output:${outputIndex}`;
1063
+ for (const stableId of stableIds) {
1064
+ toolBufferAliases.set(`id:${stableId}`, key);
1065
+ }
1066
+ return key;
1067
+ }
1068
+ for (const stableId of stableIds) {
1069
+ const identityKey = `id:${stableId}`;
1070
+ return toolBufferAliases.get(identityKey) ?? identityKey;
1071
+ }
1072
+ return `fallback:${fallbackId}`;
1073
+ };
1059
1074
  const emitToolCall = (key) => {
1060
1075
  const toolCall = toolBuffers.get(key);
1061
1076
  if (!toolCall || toolCall.emitted || !toolCall.name) {
@@ -1063,12 +1078,16 @@ const streamResponses = async function* (response) {
1063
1078
  }
1064
1079
  toolCall.emitted = true;
1065
1080
  sawToolCalls = true;
1081
+ const id = resolveToolCallId([toolCall.callId], toolCall.fallbackId, seenIds);
1066
1082
  return {
1067
1083
  type: "tool-call",
1068
1084
  toolCall: {
1069
- id: toolCall.callId,
1085
+ id,
1070
1086
  name: toolCall.name,
1071
- input: JSON.parse(toolCall.args || "{}")
1087
+ input: JSON.parse(toolCall.args || "{}"),
1088
+ ...(toolCall.providerCallId !== undefined && toolCall.providerCallId !== id
1089
+ ? { providerMetadata: { qwenResponsesCallId: toolCall.providerCallId } }
1090
+ : {})
1072
1091
  }
1073
1092
  };
1074
1093
  };
@@ -1097,14 +1116,19 @@ const streamResponses = async function* (response) {
1097
1116
  if (type === "response.output_item.added" || type === "response.output_item.done") {
1098
1117
  const item = json.item;
1099
1118
  if (item?.type === "function_call") {
1100
- const key = item.id ?? json.item_id ?? `${json.output_index ?? toolBuffers.size}`;
1119
+ const outputIndex = json.output_index ?? toolBuffers.size;
1120
+ const fallbackId = fallbackToolCallId("responses", fallbackGeneration, outputIndex);
1121
+ const key = bufferKey(json.output_index, [item.id, json.item_id, item.call_id], fallbackId);
1101
1122
  const existing = toolBuffers.get(key) ?? {
1102
- callId: item.call_id ?? key,
1123
+ callId: stableToolCallId(item.call_id) ?? stableToolCallId(item.id) ?? stableToolCallId(json.item_id),
1124
+ providerCallId: qwenResponsesProviderCallId(item.call_id),
1125
+ fallbackId,
1103
1126
  name: item.name ?? "",
1104
1127
  args: "",
1105
1128
  emitted: false
1106
1129
  };
1107
- existing.callId = item.call_id ?? existing.callId;
1130
+ existing.callId = stableToolCallId(item.call_id) ?? existing.callId;
1131
+ existing.providerCallId = qwenResponsesProviderCallId(item.call_id) ?? existing.providerCallId;
1108
1132
  existing.name ||= item.name ?? "";
1109
1133
  if (typeof item.arguments === "string") {
1110
1134
  existing.args = item.arguments;
@@ -1128,25 +1152,35 @@ const streamResponses = async function* (response) {
1128
1152
  continue;
1129
1153
  }
1130
1154
  if (type === "response.function_call_arguments.delta") {
1131
- const key = json.item_id ?? `${json.output_index ?? toolBuffers.size}`;
1155
+ const outputIndex = json.output_index ?? toolBuffers.size;
1156
+ const fallbackId = fallbackToolCallId("responses", fallbackGeneration, outputIndex);
1157
+ const key = bufferKey(json.output_index, [json.item_id, json.call_id], fallbackId);
1132
1158
  const existing = toolBuffers.get(key) ?? {
1133
- callId: key,
1159
+ callId: stableToolCallId(json.call_id) ?? stableToolCallId(json.item_id),
1160
+ providerCallId: qwenResponsesProviderCallId(json.call_id),
1161
+ fallbackId,
1134
1162
  name: "",
1135
1163
  args: "",
1136
1164
  emitted: false
1137
1165
  };
1166
+ existing.providerCallId = qwenResponsesProviderCallId(json.call_id) ?? existing.providerCallId;
1138
1167
  existing.args += typeof json.delta === "string" ? json.delta : "";
1139
1168
  toolBuffers.set(key, existing);
1140
1169
  continue;
1141
1170
  }
1142
1171
  if (type === "response.function_call_arguments.done") {
1143
- const key = json.item_id ?? `${json.output_index ?? toolBuffers.size}`;
1172
+ const outputIndex = json.output_index ?? toolBuffers.size;
1173
+ const fallbackId = fallbackToolCallId("responses", fallbackGeneration, outputIndex);
1174
+ const key = bufferKey(json.output_index, [json.item_id, json.call_id], fallbackId);
1144
1175
  const existing = toolBuffers.get(key) ?? {
1145
- callId: key,
1176
+ callId: stableToolCallId(json.call_id) ?? stableToolCallId(json.item_id),
1177
+ providerCallId: qwenResponsesProviderCallId(json.call_id),
1178
+ fallbackId,
1146
1179
  name: "",
1147
1180
  args: "",
1148
1181
  emitted: false
1149
1182
  };
1183
+ existing.providerCallId = qwenResponsesProviderCallId(json.call_id) ?? existing.providerCallId;
1150
1184
  if (typeof json.arguments === "string") {
1151
1185
  existing.args = json.arguments;
1152
1186
  }
@@ -1195,11 +1229,11 @@ class QwenLanguageModel {
1195
1229
  }
1196
1230
  const providerOptions = { ...(input.providerOptions ?? {}) };
1197
1231
  validateQwenToolStream(this.modelId, providerOptions, false);
1198
- if (isQwen38MaxFamily(this.modelId)) {
1232
+ if (isQwen38ReasoningModel(this.modelId)) {
1199
1233
  validateQwen38Request(this.modelId, input, providerOptions);
1200
1234
  }
1201
- if (isQwen38Max(this.modelId)) {
1202
- validateQwen38MaxBaseURL(this.baseURL);
1235
+ if (isQwen38ProductionModel(this.modelId)) {
1236
+ validateQwen38ProductionBaseURL(this.modelId, this.baseURL);
1203
1237
  }
1204
1238
  if (isQwen38MaxPreview(this.modelId)) {
1205
1239
  validateQwen38BaseURL(this.baseURL);
@@ -1211,7 +1245,7 @@ class QwenLanguageModel {
1211
1245
  if (apiMode === "responses") {
1212
1246
  const previousResponse = getProviderResponseId(input.messages);
1213
1247
  const baseResponseProviderOptions = stripResponsesRequestOptions(providerOptions);
1214
- const responseProviderOptions = isQwen38MaxFamily(this.modelId)
1248
+ const responseProviderOptions = isQwen38ReasoningModel(this.modelId)
1215
1249
  ? stripQwen38ReasoningOptions(baseResponseProviderOptions)
1216
1250
  : baseResponseProviderOptions;
1217
1251
  const messages = previousResponse && previousResponse.index < input.messages.length - 1
@@ -1234,7 +1268,7 @@ class QwenLanguageModel {
1234
1268
  })
1235
1269
  }), input);
1236
1270
  const json = await parseJson(response);
1237
- const assistantMessage = parseResponsesAssistantMessage(json);
1271
+ const assistantMessage = parseResponsesAssistantMessage(json, input);
1238
1272
  const hasToolCalls = assistantMessage.parts.some((part) => part.type === "tool-call");
1239
1273
  return {
1240
1274
  messages: [assistantMessage],
@@ -1249,7 +1283,7 @@ class QwenLanguageModel {
1249
1283
  };
1250
1284
  }
1251
1285
  const baseChatProviderOptions = stripHeaderOptions(providerOptions);
1252
- const chatProviderOptions = isQwen38MaxFamily(this.modelId)
1286
+ const chatProviderOptions = isQwen38ReasoningModel(this.modelId)
1253
1287
  ? stripQwen38ReasoningOptions(baseChatProviderOptions)
1254
1288
  : baseChatProviderOptions;
1255
1289
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
@@ -1259,13 +1293,13 @@ class QwenLanguageModel {
1259
1293
  body: JSON.stringify({
1260
1294
  ...chatProviderOptions,
1261
1295
  model: this.modelId,
1262
- messages: mapMessages(withStructuredOutputMessages(input)),
1296
+ messages: mapMessages(withStructuredOutputMessages(this.modelId, input)),
1263
1297
  tools: mapChatTools(input.tools),
1264
1298
  tool_choice: mapChatToolChoice(input.toolChoice),
1265
- response_format: mapStructuredOutput(input),
1299
+ response_format: mapStructuredOutput(this.modelId, input),
1266
1300
  temperature: input.temperature,
1267
- max_tokens: isQwen38MaxFamily(this.modelId) ? undefined : input.maxTokens,
1268
- max_completion_tokens: isQwen38MaxFamily(this.modelId) ? input.maxTokens : undefined,
1301
+ max_tokens: isQwen38ReasoningModel(this.modelId) ? undefined : input.maxTokens,
1302
+ max_completion_tokens: isQwen38ReasoningModel(this.modelId) ? input.maxTokens : undefined,
1269
1303
  stream: false,
1270
1304
  ...mapChatReasoning(this.modelId, input, providerOptions)
1271
1305
  })
@@ -1273,7 +1307,7 @@ class QwenLanguageModel {
1273
1307
  const json = await parseJson(response);
1274
1308
  const choice = json.choices?.[0];
1275
1309
  const message = choice?.message ?? {};
1276
- const assistantMessage = parseAssistantMessage(message);
1310
+ const assistantMessage = parseAssistantMessage(message, input);
1277
1311
  return {
1278
1312
  messages: [assistantMessage],
1279
1313
  text: assistantMessage.parts
@@ -1293,11 +1327,11 @@ class QwenLanguageModel {
1293
1327
  async stream(input) {
1294
1328
  const providerOptions = { ...(input.providerOptions ?? {}) };
1295
1329
  validateQwenToolStream(this.modelId, providerOptions, true);
1296
- if (isQwen38MaxFamily(this.modelId)) {
1330
+ if (isQwen38ReasoningModel(this.modelId)) {
1297
1331
  validateQwen38Request(this.modelId, input, providerOptions);
1298
1332
  }
1299
- if (isQwen38Max(this.modelId)) {
1300
- validateQwen38MaxBaseURL(this.baseURL);
1333
+ if (isQwen38ProductionModel(this.modelId)) {
1334
+ validateQwen38ProductionBaseURL(this.modelId, this.baseURL);
1301
1335
  }
1302
1336
  if (isQwen38MaxPreview(this.modelId)) {
1303
1337
  validateQwen38BaseURL(this.baseURL);
@@ -1308,7 +1342,7 @@ class QwenLanguageModel {
1308
1342
  if (apiMode === "responses") {
1309
1343
  const previousResponse = getProviderResponseId(input.messages);
1310
1344
  const baseResponseProviderOptions = stripResponsesRequestOptions(providerOptions);
1311
- const responseProviderOptions = isQwen38MaxFamily(this.modelId)
1345
+ const responseProviderOptions = isQwen38ReasoningModel(this.modelId)
1312
1346
  ? stripQwen38ReasoningOptions(baseResponseProviderOptions)
1313
1347
  : baseResponseProviderOptions;
1314
1348
  const messages = previousResponse && previousResponse.index < input.messages.length - 1
@@ -1332,7 +1366,7 @@ class QwenLanguageModel {
1332
1366
  }), input);
1333
1367
  return (async function* () {
1334
1368
  try {
1335
- yield* streamResponses(response);
1369
+ yield* streamResponses(response, input);
1336
1370
  }
1337
1371
  finally {
1338
1372
  cleanup();
@@ -1340,7 +1374,7 @@ class QwenLanguageModel {
1340
1374
  })();
1341
1375
  }
1342
1376
  const baseChatProviderOptions = stripHeaderOptions(providerOptions);
1343
- const chatProviderOptions = isQwen38MaxFamily(this.modelId)
1377
+ const chatProviderOptions = isQwen38ReasoningModel(this.modelId)
1344
1378
  ? stripQwen38ReasoningOptions(baseChatProviderOptions)
1345
1379
  : baseChatProviderOptions;
1346
1380
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/chat/completions`, {
@@ -1351,13 +1385,13 @@ class QwenLanguageModel {
1351
1385
  ...chatProviderOptions,
1352
1386
  model: this.modelId,
1353
1387
  modalities: isQwenOmniLanguageModel(this.modelId) ? providerOptions.modalities ?? ["text"] : undefined,
1354
- messages: mapMessages(withStructuredOutputMessages(input)),
1388
+ messages: mapMessages(withStructuredOutputMessages(this.modelId, input)),
1355
1389
  tools: mapChatTools(input.tools),
1356
1390
  tool_choice: mapChatToolChoice(input.toolChoice),
1357
- response_format: mapStructuredOutput(input),
1391
+ response_format: mapStructuredOutput(this.modelId, input),
1358
1392
  temperature: input.temperature,
1359
- max_tokens: isQwen38MaxFamily(this.modelId) ? undefined : input.maxTokens,
1360
- max_completion_tokens: isQwen38MaxFamily(this.modelId) ? input.maxTokens : undefined,
1393
+ max_tokens: isQwen38ReasoningModel(this.modelId) ? undefined : input.maxTokens,
1394
+ max_completion_tokens: isQwen38ReasoningModel(this.modelId) ? input.maxTokens : undefined,
1361
1395
  stream: true,
1362
1396
  stream_options: { include_usage: true },
1363
1397
  ...mapChatReasoning(this.modelId, input, providerOptions)
@@ -1366,6 +1400,8 @@ class QwenLanguageModel {
1366
1400
  return (async function* () {
1367
1401
  try {
1368
1402
  const toolBuffers = new Map();
1403
+ const seenIds = existingToolCallIds(input.messages);
1404
+ const fallbackGeneration = nextFallbackToolCallGeneration("chat", input, seenIds);
1369
1405
  for await (const event of streamSSE(response)) {
1370
1406
  if (event.data === "[DONE]") {
1371
1407
  return;
@@ -1389,12 +1425,13 @@ class QwenLanguageModel {
1389
1425
  for (const toolCall of delta?.tool_calls ?? []) {
1390
1426
  const index = Number(toolCall.index ?? 0);
1391
1427
  const existing = toolBuffers.get(index) ?? {
1392
- id: toolCall.id ?? `${index}`,
1428
+ id: stableToolCallId(toolCall.id),
1429
+ fallbackId: fallbackToolCallId("chat", fallbackGeneration, index),
1393
1430
  name: toolCall.function?.name ?? "",
1394
1431
  args: "",
1395
1432
  emitted: false
1396
1433
  };
1397
- existing.id = toolCall.id ?? existing.id;
1434
+ existing.id = stableToolCallId(toolCall.id) ?? existing.id;
1398
1435
  existing.name ||= toolCall.function?.name ?? "";
1399
1436
  existing.args += toolCall.function?.arguments ?? "";
1400
1437
  toolBuffers.set(index, existing);
@@ -1409,7 +1446,7 @@ class QwenLanguageModel {
1409
1446
  yield {
1410
1447
  type: "tool-call",
1411
1448
  toolCall: {
1412
- id: toolCall.id,
1449
+ id: resolveToolCallId([toolCall.id], toolCall.fallbackId, seenIds),
1413
1450
  name: toolCall.name,
1414
1451
  input: JSON.parse(toolCall.args || "{}")
1415
1452
  }
@@ -2155,7 +2192,7 @@ class QwenTasksClientImpl {
2155
2192
  }
2156
2193
  }
2157
2194
  }
2158
- const parseRealtimeEvent = (payload) => {
2195
+ const parseRealtimeEvent = (payload, fallbackRealtimeToolCallId, seenToolCallIds) => {
2159
2196
  const type = String(payload.type ?? "");
2160
2197
  const metadata = payload;
2161
2198
  const itemId = typeof payload.item_id === "string" ? payload.item_id : undefined;
@@ -2196,7 +2233,7 @@ const parseRealtimeEvent = (payload) => {
2196
2233
  return [{
2197
2234
  type: "realtime-tool-call",
2198
2235
  toolCall: {
2199
- id: String(payload.call_id ?? payload.item_id ?? "qwen-tool-call"),
2236
+ id: resolveToolCallId([payload.call_id, payload.item_id], fallbackRealtimeToolCallId, seenToolCallIds),
2200
2237
  name: String(payload.name ?? ""),
2201
2238
  input: JSON.parse(typeof payload.arguments === "string" ? payload.arguments : "{}")
2202
2239
  }
@@ -2290,6 +2327,8 @@ class QwenRealtimeModel {
2290
2327
  assertQwenRealtimeConfig(config);
2291
2328
  const url = appendQuery(this.realtimeURL, { model: this.modelId });
2292
2329
  const connection = await (this.connectionFactory ?? openQwenRealtimeConnection)(url, { authorization: `Bearer ${this.apiKey}` }, options);
2330
+ let fallbackRealtimeToolCallIndex = 0;
2331
+ const seenRealtimeToolCallIds = new Set();
2293
2332
  const session = new CallbackRealtimeSession({
2294
2333
  provider: this.provider,
2295
2334
  modelId: this.modelId,
@@ -2297,7 +2336,7 @@ class QwenRealtimeModel {
2297
2336
  config,
2298
2337
  connection,
2299
2338
  callbacks: {
2300
- parseEvent: parseRealtimeEvent,
2339
+ parseEvent: (payload) => parseRealtimeEvent(payload, () => `qwen-realtime-tool-${fallbackRealtimeToolCallIndex++}`, seenRealtimeToolCallIds),
2301
2340
  buildInitialPayloads: (value) => [{ type: "session.update", session: mapRealtimeSession(value) }],
2302
2341
  buildAudioPayloads: (frame, sessionConfig) => [
2303
2342
  { type: "input_audio_buffer.append", audio: encodeRealtimeData(frame.data) },