@zhivex-ai/qwen 0.16.1 → 0.16.3

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,3 +1,5 @@
1
+ import { qwenSearchUsage } from "./search-usage.js";
2
+ import { createAnnotationCollector } from "./annotations.js";
1
3
  export { createQwenSDPExchange } from "./sdp-exchange.js";
2
4
  export { connectQwenWebRTC, connectQwenAOQ } from "./browser-realtime.js";
3
5
  import { createQwenTemporaryKeysClient } from "./temporary-keys.js";
@@ -683,7 +685,37 @@ const hasHostedTools = (tools) => Object.values(tools ?? {}).some((tool) => !isC
683
685
  const hasMessagePart = (messages, type) => messages.some((message) => message.parts.some((part) => part.type === type));
684
686
  const hasVideoInput = (messages) => messages.some((message) => message.parts.some((part) => part.type === "file" && part.mediaType.toLowerCase().startsWith("video/")));
685
687
  const hasNonVideoFileInput = (messages) => messages.some((message) => message.parts.some((part) => part.type === "file" && !part.mediaType.toLowerCase().startsWith("video/")));
688
+ // Only documented Responses families may send a bounded output request.
689
+ const responsesOutputLimitFamilies = new Set([
690
+ "qwen3.8-max", "qwen3.8-flash", "qwen3.8-omni-flash", "qwen3.8-2.4t-a95b", "qwen3.8-27b",
691
+ "qwen3.7-max", "qwen3.7-max-preview", "qwen3.7-plus", "qwen3.7-flash",
692
+ "qwen3.6-plus", "qwen3.6-flash", "qwen3.6-35b-a3b", "qwen3-max",
693
+ "qwen3.5-plus", "qwen3.5-flash", "qwen3.5-397b-a17b", "qwen3.5-122b-a10b", "qwen3.5-27b", "qwen3.5-35b-a3b"
694
+ ]);
695
+ const supportsResponsesOutputLimit = (modelId) => responsesOutputLimitFamilies.has(modelId.toLowerCase().replace(/-\d{4}(?:-\d{2}-\d{2})?$/, "")) ||
696
+ thirdPartyProfile(modelId)?.responses === true;
697
+ const responsesOutputLimit = (modelId, input, options) => {
698
+ const value = input.maxTokens ?? options.max_output_tokens;
699
+ if (input.maxTokens !== undefined && options.max_output_tokens !== undefined && input.maxTokens !== options.max_output_tokens) {
700
+ throw new ConfigurationError("Qwen maxTokens and max_output_tokens must agree.");
701
+ }
702
+ if (value === undefined)
703
+ return undefined;
704
+ if (!supportsResponsesOutputLimit(modelId))
705
+ throw new UnsupportedFeatureError(`Qwen Responses output limits are not supported for model "${modelId}".`);
706
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 16) {
707
+ throw new ConfigurationError("Qwen Responses maxTokens/max_output_tokens must be a safe integer of at least 16.");
708
+ }
709
+ return value;
710
+ };
686
711
  const resolveApiMode = (modelId, requestedMode, input, providerOptions) => {
712
+ if (providerOptions.max_tool_calls !== undefined)
713
+ throw new UnsupportedFeatureError("Qwen max_tool_calls is not verified as an effective hosted-tool limit.");
714
+ for (const tool of Object.values(input.tools ?? {})) {
715
+ if (!isCallableToolDefinition(tool) && tool.type === "web_search" && tool.config && typeof tool.config === "object" &&
716
+ ("max_tool_calls" in tool.config || "max_uses" in tool.config))
717
+ throw new UnsupportedFeatureError("Qwen web_search tool-call limits are not verified on Responses.");
718
+ }
687
719
  if (isQwenTranslation(modelId) || isQwenCharacter(modelId))
688
720
  return "chat";
689
721
  const videoInput = hasVideoInput(input.messages);
@@ -700,15 +732,16 @@ const resolveApiMode = (modelId, requestedMode, input, providerOptions) => {
700
732
  throw new UnsupportedFeatureError('QwenCloud glm-5.2 callable tools require apiMode "chat" or "auto" because Responses function continuation is unavailable.');
701
733
  }
702
734
  const needsChat = glmCallableTools ||
703
- input.maxTokens !== undefined ||
735
+ (input.maxTokens !== undefined && (!supportsResponsesOutputLimit(modelId) ||
736
+ (requestedMode === "auto" && !hasHostedTools(input.tools) && !hasNonVideoFileInput(input.messages) && providerOptions.max_output_tokens === undefined))) ||
704
737
  input.reasoning?.budgetTokens !== undefined ||
705
738
  providerOptions.thinking_budget !== undefined ||
706
739
  providerOptions.tool_stream === true ||
707
740
  input.structuredOutput?.mode === "native" ||
708
741
  (!omni38 && (hasMessagePart(input.messages, "audio") || videoInput));
709
- const needsResponses = hasHostedTools(input.tools) || hasNonVideoFileInput(input.messages);
742
+ const needsResponses = hasHostedTools(input.tools) || hasNonVideoFileInput(input.messages) || providerOptions.max_output_tokens !== undefined;
710
743
  if (needsChat && needsResponses) {
711
- throw new UnsupportedFeatureError("Qwen cannot combine Responses-only hosted/file inputs with Chat-only maxTokens, audio, video, reasoning budgets, structured output, or tool streaming.");
744
+ throw new UnsupportedFeatureError("Qwen cannot combine Responses-only hosted/file inputs with Chat-only options (including maxTokens on unsupported models), audio, video, reasoning budgets, structured output, or tool streaming.");
712
745
  }
713
746
  if (isQwenOmniLanguageModel(modelId)) {
714
747
  if (requestedMode === "responses" || needsResponses) {
@@ -895,7 +928,8 @@ const toResponsesInput = (messages, format, modelId) => {
895
928
  part.provider === "qwen" &&
896
929
  part.data &&
897
930
  typeof part.data === "object" &&
898
- typeof part.data.type === "string") {
931
+ typeof part.data.type === "string" &&
932
+ !["response.annotations", "hosted-tool-usage"].includes(String(part.data.type))) {
899
933
  input.push(part.data);
900
934
  }
901
935
  break;
@@ -927,15 +961,72 @@ class QwenResponseError extends Error {
927
961
  this.name = "QwenResponseError";
928
962
  }
929
963
  }
964
+ // Never retain provider arguments or parser causes in diagnostics.
965
+ const qwenResponsesToolCallError = (reason, usage, effectsPossible = false) => new ProviderToolCallError({
966
+ provider: "qwen", transport: "responses", diagnosticCode: "QWEN_RESPONSES_TOOL_CALL_INVALID",
967
+ reason, usage, effectsPossible, retryable: false
968
+ });
969
+ class QwenStreamEventError extends Error {
970
+ transport;
971
+ reason;
972
+ provider = "qwen";
973
+ diagnosticCode = "QWEN_SSE_EVENT_INVALID";
974
+ retryable = false;
975
+ constructor(transport, reason) {
976
+ super("Qwen stream event could not be parsed safely.");
977
+ this.transport = transport;
978
+ this.reason = reason;
979
+ this.name = "QwenStreamEventError";
980
+ }
981
+ }
982
+ const parseQwenStreamEvent = (data, transport) => {
983
+ let event;
984
+ try {
985
+ event = JSON.parse(data);
986
+ }
987
+ catch {
988
+ throw new QwenStreamEventError(transport, "invalid_json");
989
+ }
990
+ if (!event || typeof event !== "object" || Array.isArray(event) || (transport === "responses" && typeof event.type !== "string")) {
991
+ throw new QwenStreamEventError(transport, "invalid_event");
992
+ }
993
+ return event;
994
+ };
995
+ const parseQwenResponsesArguments = (raw, usage, effectsPossible = false) => {
996
+ const failure = (reason) => qwenResponsesToolCallError(reason, usage, effectsPossible);
997
+ if (typeof raw !== "string")
998
+ throw failure("incomplete_arguments");
999
+ if (!raw.trim())
1000
+ throw failure("empty_arguments");
1001
+ let value;
1002
+ try {
1003
+ value = JSON.parse(raw);
1004
+ }
1005
+ catch {
1006
+ throw failure("invalid_json");
1007
+ }
1008
+ if (!value || typeof value !== "object" || Array.isArray(value))
1009
+ throw failure("invalid_json");
1010
+ return value;
1011
+ };
930
1012
  const parseResponsesAssistantMessage = (json, input) => {
931
1013
  if (json.status === "failed" || json.error)
932
1014
  throw new QwenResponseError();
933
1015
  const parts = [];
934
1016
  const seenIds = existingToolCallIds(input.messages);
935
1017
  const fallbackGeneration = nextFallbackToolCallGeneration("responses", input, seenIds);
1018
+ const usage = mapResponsesUsage(json.usage);
1019
+ const effectsPossible = Object.values(input.tools ?? {}).some(tool => !isCallableToolDefinition(tool)) || (json.output ?? []).some((item) => parseResponsesProviderData(item) !== undefined);
1020
+ if (json.status === "incomplete" && (json.output ?? []).some((item) => item?.type === "function_call")) {
1021
+ throw qwenResponsesToolCallError("response_incomplete", usage, effectsPossible);
1022
+ }
1023
+ const collectAnnotations = createAnnotationCollector();
936
1024
  for (const [index, item] of (json.output ?? []).entries()) {
937
1025
  if (item?.type === "message") {
938
- for (const content of item.content ?? []) {
1026
+ for (const [contentIndex, content] of (item.content ?? []).entries()) {
1027
+ const data = collectAnnotations(content, index, contentIndex, item.id);
1028
+ if (data)
1029
+ parts.push(providerDataPart("qwen", data));
939
1030
  if (typeof content?.text === "string" && content.text) {
940
1031
  parts.push({ type: "text", text: content.text });
941
1032
  }
@@ -943,6 +1034,9 @@ const parseResponsesAssistantMessage = (json, input) => {
943
1034
  continue;
944
1035
  }
945
1036
  if (item?.type === "function_call") {
1037
+ if (typeof item.name !== "string" || !item.name.trim()) {
1038
+ throw qwenResponsesToolCallError("inconsistent_metadata", usage, effectsPossible);
1039
+ }
946
1040
  const providerCallId = qwenResponsesProviderCallId(item.call_id);
947
1041
  const id = resolveToolCallId([item.call_id, item.id], fallbackToolCallId("responses", fallbackGeneration, index), seenIds);
948
1042
  parts.push({
@@ -950,7 +1044,7 @@ const parseResponsesAssistantMessage = (json, input) => {
950
1044
  toolCall: {
951
1045
  id,
952
1046
  name: item.name,
953
- input: JSON.parse(item.arguments ?? "{}"),
1047
+ input: parseQwenResponsesArguments(item.arguments, usage, effectsPossible),
954
1048
  ...(providerCallId !== undefined && providerCallId !== id
955
1049
  ? { providerMetadata: { qwenResponsesCallId: providerCallId } }
956
1050
  : {})
@@ -981,17 +1075,41 @@ const normalizeResponsesFinishReason = (status, hasToolCalls) => {
981
1075
  if (status === "completed") {
982
1076
  return "stop";
983
1077
  }
1078
+ if (status === "incomplete")
1079
+ return "length";
984
1080
  if (status === "failed") {
985
1081
  return "error";
986
1082
  }
987
1083
  return normalizeFinishReason(status);
988
1084
  };
989
1085
  const streamResponses = async function* (response, input) {
1086
+ const attemptId = globalThis.crypto.randomUUID();
1087
+ const searchEnabled = Object.values(input.tools ?? {}).some(t => !isCallableToolDefinition(t) && t.type === "web_search");
1088
+ if (searchEnabled)
1089
+ yield { type: "provider-data", provider: "qwen", data: qwenSearchUsage(attemptId) };
1090
+ const collectAnnotations = createAnnotationCollector();
990
1091
  const toolBuffers = new Map();
991
1092
  const toolBufferAliases = new Map();
992
1093
  const seenIds = existingToolCallIds(input.messages);
993
1094
  const fallbackGeneration = nextFallbackToolCallGeneration("responses", input, seenIds);
994
- let sawToolCalls = false;
1095
+ let terminal;
1096
+ let effectsPossible = Object.values(input.tools ?? {}).some(tool => !isCallableToolDefinition(tool));
1097
+ let invalidMetadata = false;
1098
+ const mergeMetadata = (buffer, itemId, callId, name) => {
1099
+ const nextItemId = qwenResponsesProviderCallId(itemId);
1100
+ const nextCallId = qwenResponsesProviderCallId(callId);
1101
+ const nextName = typeof name === "string" && name.trim() ? name : undefined;
1102
+ if ((buffer.itemId !== undefined && nextItemId !== undefined && buffer.itemId !== nextItemId) ||
1103
+ (buffer.providerCallId !== undefined && nextCallId !== undefined && buffer.providerCallId !== nextCallId) ||
1104
+ (buffer.name && nextName !== undefined && buffer.name !== nextName) ||
1105
+ (name !== undefined && typeof name !== "string"))
1106
+ invalidMetadata = true;
1107
+ buffer.itemId ??= nextItemId;
1108
+ buffer.providerCallId ??= nextCallId;
1109
+ buffer.name ||= nextName ?? "";
1110
+ // A receipt ID may arrive after the item ID used as the initial fallback.
1111
+ buffer.callId = stableToolCallId(callId) ?? buffer.callId;
1112
+ };
995
1113
  const bufferKey = (outputIndex, candidates, fallbackId) => {
996
1114
  const stableIds = candidates
997
1115
  .map(stableToolCallId)
@@ -1010,31 +1128,11 @@ const streamResponses = async function* (response, input) {
1010
1128
  }
1011
1129
  return `fallback:${fallbackId}`;
1012
1130
  };
1013
- const emitToolCall = (key) => {
1014
- const toolCall = toolBuffers.get(key);
1015
- if (!toolCall || toolCall.emitted || !toolCall.name) {
1016
- return undefined;
1017
- }
1018
- toolCall.emitted = true;
1019
- sawToolCalls = true;
1020
- const id = resolveToolCallId([toolCall.callId], toolCall.fallbackId, seenIds);
1021
- return {
1022
- type: "tool-call",
1023
- toolCall: {
1024
- id,
1025
- name: toolCall.name,
1026
- input: JSON.parse(toolCall.args || "{}"),
1027
- ...(toolCall.providerCallId !== undefined && toolCall.providerCallId !== id
1028
- ? { providerMetadata: { qwenResponsesCallId: toolCall.providerCallId } }
1029
- : {})
1030
- }
1031
- };
1032
- };
1033
1131
  for await (const event of streamSSE(response)) {
1034
1132
  if (event.data === "[DONE]") {
1035
- return;
1133
+ break;
1036
1134
  }
1037
- const json = JSON.parse(event.data);
1135
+ const json = parseQwenStreamEvent(event.data, "responses");
1038
1136
  const type = json.type;
1039
1137
  if (type === "error" || type === "response.failed" || json.response?.status === "failed" || json.error) {
1040
1138
  throw new QwenResponseError();
@@ -1055,8 +1153,23 @@ const streamResponses = async function* (response, input) {
1055
1153
  };
1056
1154
  continue;
1057
1155
  }
1156
+ input.abortSignal?.throwIfAborted();
1157
+ if (type === "response.content_part.done") {
1158
+ const data = collectAnnotations(json.part, json.output_index, json.content_index, json.item_id);
1159
+ if (data)
1160
+ yield { type: "provider-data", provider: "qwen", data };
1161
+ continue;
1162
+ }
1058
1163
  if (type === "response.output_item.added" || type === "response.output_item.done") {
1059
1164
  const item = json.item;
1165
+ if (type === "response.output_item.done" && item?.type === "message" && Array.isArray(item.content)) {
1166
+ for (const [contentIndex, part] of item.content.entries()) {
1167
+ input.abortSignal?.throwIfAborted();
1168
+ const data = collectAnnotations(part, json.output_index, contentIndex, item.id);
1169
+ if (data)
1170
+ yield { type: "provider-data", provider: "qwen", data };
1171
+ }
1172
+ }
1060
1173
  if (item?.type === "function_call") {
1061
1174
  const outputIndex = json.output_index ?? toolBuffers.size;
1062
1175
  const fallbackId = fallbackToolCallId("responses", fallbackGeneration, outputIndex);
@@ -1067,23 +1180,24 @@ const streamResponses = async function* (response, input) {
1067
1180
  fallbackId,
1068
1181
  name: item.name ?? "",
1069
1182
  args: "",
1070
- emitted: false
1183
+ done: false
1071
1184
  };
1072
- existing.callId = stableToolCallId(item.call_id) ?? existing.callId;
1073
- existing.providerCallId = qwenResponsesProviderCallId(item.call_id) ?? existing.providerCallId;
1074
- existing.name ||= item.name ?? "";
1185
+ if (item.arguments !== undefined && typeof item.arguments !== "string")
1186
+ invalidMetadata = true;
1187
+ if (existing.done && typeof item.arguments === "string" && item.arguments !== existing.args)
1188
+ invalidMetadata = true;
1189
+ mergeMetadata(existing, item.id ?? json.item_id, item.call_id, item.name);
1075
1190
  if (typeof item.arguments === "string") {
1076
1191
  existing.args = item.arguments;
1077
1192
  }
1078
1193
  toolBuffers.set(key, existing);
1079
1194
  if (type === "response.output_item.done") {
1080
- const emitted = emitToolCall(key);
1081
- if (emitted) {
1082
- yield emitted;
1083
- }
1195
+ existing.done = true;
1084
1196
  }
1085
1197
  }
1086
1198
  const providerData = parseResponsesProviderData(item);
1199
+ if (providerData)
1200
+ effectsPossible = true;
1087
1201
  if (providerData && type === "response.output_item.done") {
1088
1202
  yield {
1089
1203
  type: "provider-data",
@@ -1103,9 +1217,13 @@ const streamResponses = async function* (response, input) {
1103
1217
  fallbackId,
1104
1218
  name: "",
1105
1219
  args: "",
1106
- emitted: false
1220
+ done: false
1107
1221
  };
1108
- existing.providerCallId = qwenResponsesProviderCallId(json.call_id) ?? existing.providerCallId;
1222
+ mergeMetadata(existing, json.item_id, json.call_id);
1223
+ if (existing.done)
1224
+ invalidMetadata = true;
1225
+ if (typeof json.delta !== "string")
1226
+ invalidMetadata = true;
1109
1227
  existing.args += typeof json.delta === "string" ? json.delta : "";
1110
1228
  toolBuffers.set(key, existing);
1111
1229
  continue;
@@ -1120,36 +1238,115 @@ const streamResponses = async function* (response, input) {
1120
1238
  fallbackId,
1121
1239
  name: "",
1122
1240
  args: "",
1123
- emitted: false
1241
+ done: false
1124
1242
  };
1125
- existing.providerCallId = qwenResponsesProviderCallId(json.call_id) ?? existing.providerCallId;
1243
+ mergeMetadata(existing, json.item_id, json.call_id);
1244
+ if (json.arguments !== undefined && typeof json.arguments !== "string")
1245
+ invalidMetadata = true;
1246
+ if (existing.done && typeof json.arguments === "string" && json.arguments !== existing.args)
1247
+ invalidMetadata = true;
1126
1248
  if (typeof json.arguments === "string") {
1127
1249
  existing.args = json.arguments;
1128
1250
  }
1251
+ existing.done = true;
1129
1252
  toolBuffers.set(key, existing);
1130
- const emitted = emitToolCall(key);
1131
- if (emitted) {
1132
- yield emitted;
1133
- }
1134
1253
  continue;
1135
1254
  }
1136
1255
  if (type === "response.completed" || type === "response.incomplete") {
1137
- const responseData = json.response ?? {};
1138
- if (typeof responseData.id === "string") {
1139
- yield {
1140
- type: "provider-data",
1141
- provider: "qwen",
1142
- data: { responseId: responseData.id }
1143
- };
1256
+ const next = { ...(json.response ?? {}), status: type === "response.incomplete" ? "incomplete" : json.response?.status };
1257
+ if (terminal && JSON.stringify(terminal) !== JSON.stringify(next))
1258
+ invalidMetadata = true;
1259
+ terminal = next;
1260
+ }
1261
+ }
1262
+ // Some servers supply the final items only in the terminal snapshot. When
1263
+ // both forms exist, neither may silently overwrite a contradictory call.
1264
+ if (Array.isArray(terminal?.output)) {
1265
+ const terminalKeys = new Set();
1266
+ for (const [index, item] of terminal.output.entries()) {
1267
+ input.abortSignal?.throwIfAborted();
1268
+ if (item?.type === "message" && Array.isArray(item.content)) {
1269
+ for (const [contentIndex, part] of item.content.entries()) {
1270
+ input.abortSignal?.throwIfAborted();
1271
+ const data = collectAnnotations(part, index, contentIndex, item.id);
1272
+ if (data)
1273
+ yield { type: "provider-data", provider: "qwen", data };
1274
+ }
1144
1275
  }
1145
- yield {
1146
- type: "finish",
1147
- finishReason: normalizeResponsesFinishReason(responseData.status, sawToolCalls),
1148
- providerFinishReason: responseData.status,
1149
- usage: mapResponsesUsage(responseData.usage)
1150
- };
1276
+ if (parseResponsesProviderData(item))
1277
+ effectsPossible = true;
1278
+ if (item?.type !== "function_call")
1279
+ continue;
1280
+ const fallbackId = fallbackToolCallId("responses", fallbackGeneration, index);
1281
+ const priorKey = [item.id, item.call_id]
1282
+ .map(stableToolCallId)
1283
+ .filter((id) => id !== undefined)
1284
+ .map(id => toolBufferAliases.get(`id:${id}`) ?? `id:${id}`)
1285
+ .find(key => toolBuffers.has(key));
1286
+ const positionalKey = `output:${index}`;
1287
+ const key = toolBuffers.has(positionalKey) ? positionalKey : priorKey ?? bufferKey(index, [item.id, item.call_id], fallbackId);
1288
+ terminalKeys.add(key);
1289
+ const existing = toolBuffers.get(key);
1290
+ if (existing) {
1291
+ if (existing.done && existing.args !== item.arguments)
1292
+ invalidMetadata = true;
1293
+ mergeMetadata(existing, item.id, item.call_id, item.name);
1294
+ }
1295
+ if (item.arguments !== undefined && typeof item.arguments !== "string")
1296
+ invalidMetadata = true;
1297
+ toolBuffers.set(key, {
1298
+ itemId: existing?.itemId ?? qwenResponsesProviderCallId(item.id),
1299
+ callId: stableToolCallId(item.call_id) ?? existing?.callId ?? stableToolCallId(item.id),
1300
+ providerCallId: qwenResponsesProviderCallId(item.call_id) ?? existing?.providerCallId,
1301
+ fallbackId: existing?.fallbackId ?? fallbackId,
1302
+ name: item.name,
1303
+ args: typeof item.arguments === "string" ? item.arguments : "",
1304
+ done: true
1305
+ });
1151
1306
  }
1152
- }
1307
+ if ([...toolBuffers.keys()].some(key => !terminalKeys.has(key)))
1308
+ invalidMetadata = true;
1309
+ }
1310
+ // No callable event escapes until the full response batch has been checked.
1311
+ // Text/provider events may already have been observed; hosted effects cannot be undone.
1312
+ input.abortSignal?.throwIfAborted();
1313
+ const usage = mapResponsesUsage(terminal?.usage);
1314
+ const failure = (reason) => qwenResponsesToolCallError(reason, usage, effectsPossible);
1315
+ if (!terminal)
1316
+ throw failure("stream_truncated");
1317
+ if (invalidMetadata)
1318
+ throw failure("inconsistent_metadata");
1319
+ if (searchEnabled || terminal.usage?.x_tools?.web_search !== undefined)
1320
+ yield { type: "provider-data", provider: "qwen", data: qwenSearchUsage(attemptId, terminal) };
1321
+ if (toolBuffers.size && terminal.status !== "completed")
1322
+ throw failure("response_incomplete");
1323
+ const materialized = [];
1324
+ for (const call of toolBuffers.values()) {
1325
+ if (!call.done)
1326
+ throw failure("incomplete_arguments");
1327
+ if (typeof call.name !== "string" || !call.name.trim())
1328
+ throw failure("inconsistent_metadata");
1329
+ const args = parseQwenResponsesArguments(call.args, usage, effectsPossible);
1330
+ const id = resolveToolCallId([call.callId], call.fallbackId, seenIds);
1331
+ materialized.push({ type: "tool-call", toolCall: {
1332
+ id, name: call.name, input: args,
1333
+ ...(call.providerCallId !== undefined && call.providerCallId !== id
1334
+ ? { providerMetadata: { qwenResponsesCallId: call.providerCallId } } : {})
1335
+ } });
1336
+ }
1337
+ for (const event of materialized) {
1338
+ input.abortSignal?.throwIfAborted();
1339
+ yield event;
1340
+ }
1341
+ if (typeof terminal.id === "string") {
1342
+ yield { type: "provider-data", provider: "qwen", data: { responseId: terminal.id } };
1343
+ }
1344
+ yield {
1345
+ type: "finish",
1346
+ finishReason: normalizeResponsesFinishReason(terminal.status, materialized.length > 0),
1347
+ providerFinishReason: terminal.status,
1348
+ usage
1349
+ };
1153
1350
  };
1154
1351
  class QwenLanguageModel {
1155
1352
  modelId;
@@ -1163,7 +1360,7 @@ class QwenLanguageModel {
1163
1360
  this.apiKey = apiKey;
1164
1361
  this.baseURL = baseURL;
1165
1362
  this.fetcher = fetcher;
1166
- this.capabilities = { ...qwenLanguageCapabilities(modelId), toolHistory: thirdPartyProfile(modelId)?.thinkingOnly || isQwen38MaxPreview(modelId) || /thinking|qwq/i.test(modelId) ? undefined : "json" };
1363
+ this.capabilities = { ...qwenLanguageCapabilities(modelId), hostedTools: qwenLanguageCapabilities(modelId).webSearch ? [{ route: "responses", tool: "web_search", limit: "unverified", metering: "provider-counter" }] : [], toolHistory: thirdPartyProfile(modelId)?.thinkingOnly || isQwen38MaxPreview(modelId) || /thinking|qwq/i.test(modelId) ? undefined : "json" };
1167
1364
  }
1168
1365
  async generate(input) {
1169
1366
  if (isQwenOmniLanguageModel(this.modelId)) {
@@ -1184,6 +1381,7 @@ class QwenLanguageModel {
1184
1381
  }
1185
1382
  const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input, providerOptions);
1186
1383
  delete providerOptions.apiMode;
1384
+ const outputLimit = apiMode === "responses" ? responsesOutputLimit(this.modelId, input, providerOptions) : undefined;
1187
1385
  const { signal, cleanup } = withTimeoutSignal(input);
1188
1386
  try {
1189
1387
  if (apiMode === "responses") {
@@ -1201,6 +1399,7 @@ class QwenLanguageModel {
1201
1399
  signal,
1202
1400
  body: JSON.stringify({
1203
1401
  ...responseProviderOptions,
1402
+ max_output_tokens: outputLimit,
1204
1403
  model: this.modelId,
1205
1404
  ...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
1206
1405
  ...(messages.length ? { input: toResponsesInput(messages, input.toolResultFormat, this.modelId) } : {}),
@@ -1213,6 +1412,9 @@ class QwenLanguageModel {
1213
1412
  }), { ...input, abortSignal: signal }, "Qwen");
1214
1413
  const json = await parseJson(response);
1215
1414
  const assistantMessage = parseResponsesAssistantMessage(json, input);
1415
+ if (Object.values(input.tools ?? {}).some(t => !isCallableToolDefinition(t) && t.type === "web_search") || json.usage?.x_tools?.web_search !== undefined) {
1416
+ assistantMessage.parts.push(providerDataPart("qwen", qwenSearchUsage(globalThis.crypto.randomUUID(), json)));
1417
+ }
1216
1418
  const hasToolCalls = assistantMessage.parts.some((part) => part.type === "tool-call");
1217
1419
  return {
1218
1420
  messages: [assistantMessage],
@@ -1285,6 +1487,7 @@ class QwenLanguageModel {
1285
1487
  }
1286
1488
  const apiMode = resolveApiMode(this.modelId, providerOptions.apiMode ?? "auto", input, providerOptions);
1287
1489
  delete providerOptions.apiMode;
1490
+ const outputLimit = apiMode === "responses" ? responsesOutputLimit(this.modelId, input, providerOptions) : undefined;
1288
1491
  const { signal, cleanup } = withTimeoutSignal(input);
1289
1492
  if (apiMode === "responses") {
1290
1493
  const previousResponse = getProviderResponseId(input.messages);
@@ -1301,6 +1504,7 @@ class QwenLanguageModel {
1301
1504
  signal,
1302
1505
  body: JSON.stringify({
1303
1506
  ...responseProviderOptions,
1507
+ max_output_tokens: outputLimit,
1304
1508
  model: this.modelId,
1305
1509
  ...(previousResponse ? { previous_response_id: previousResponse.responseId } : {}),
1306
1510
  ...(messages.length ? { input: toResponsesInput(messages, input.toolResultFormat, this.modelId) } : {}),
@@ -1313,7 +1517,7 @@ class QwenLanguageModel {
1313
1517
  }), { ...input, abortSignal: signal }, "Qwen").catch((error) => { cleanup(); throw error; });
1314
1518
  return (async function* () {
1315
1519
  try {
1316
- yield* streamResponses(response, input);
1520
+ yield* streamResponses(response, { ...input, abortSignal: signal });
1317
1521
  }
1318
1522
  finally {
1319
1523
  cleanup();
@@ -1359,7 +1563,7 @@ class QwenLanguageModel {
1359
1563
  if (event.data === "[DONE]") {
1360
1564
  break;
1361
1565
  }
1362
- const json = JSON.parse(event.data);
1566
+ const json = parseQwenStreamEvent(event.data, "chat");
1363
1567
  if (json.error) {
1364
1568
  if (json.usage)
1365
1569
  lastUsage = json.usage;