qwen-ai-provider
Version:
Vercel AI Provider for running Qwen AI models
1 lines • 92 kB
Source Map (JSON)
{"version":3,"sources":["../src/qwen-provider.ts","../src/qwen-chat-language-model.ts","../src/convert-to-qwen-chat-messages.ts","../src/get-response-metadata.ts","../src/map-qwen-finish-reason.ts","../src/qwen-error.ts","../src/qwen-prepare-tools.ts","../src/qwen-completion-language-model.ts","../src/convert-to-qwen-completion-prompt.ts","../src/qwen-embedding-model.ts"],"sourcesContent":["import type {\n EmbeddingModelV1,\n LanguageModelV1,\n ProviderV1,\n} from \"@ai-sdk/provider\"\nimport type { FetchFunction } from \"@ai-sdk/provider-utils\"\nimport type { QwenChatModelId, QwenChatSettings } from \"./qwen-chat-settings\"\nimport type {\n QwenCompletionModelId,\n QwenCompletionSettings,\n} from \"./qwen-completion-settings\"\nimport type {\n QwenEmbeddingModelId,\n QwenEmbeddingSettings,\n} from \"./qwen-embedding-settings\"\nimport { loadApiKey, withoutTrailingSlash } from \"@ai-sdk/provider-utils\"\nimport { QwenChatLanguageModel } from \"./qwen-chat-language-model\"\nimport { QwenCompletionLanguageModel } from \"./qwen-completion-language-model\"\nimport { QwenEmbeddingModel } from \"./qwen-embedding-model\"\n\n/**\n * QwenProvider function type and its properties.\n * Creates various language or embedding models based on the provided settings.\n */\nexport interface QwenProvider extends ProviderV1 {\n (modelId: QwenChatModelId, settings?: QwenChatSettings): LanguageModelV1\n\n /**\n * Create a new chat model for text generation.\n * @param modelId The model ID.\n * @param settings The settings for the model.\n * @returns The chat model.\n */\n chatModel: (\n modelId: QwenChatModelId,\n settings?: QwenChatSettings,\n ) => LanguageModelV1\n\n /**\n Creates a text embedding model for text generation.\n @param modelId The model ID.\n @param settings The settings for the model.\n @returns The text embedding model.\n */\n textEmbeddingModel: (\n modelId: QwenEmbeddingModelId,\n settings?: QwenEmbeddingSettings,\n ) => EmbeddingModelV1<string>\n\n languageModel: (\n modelId: QwenChatModelId,\n settings?: QwenChatSettings,\n ) => LanguageModelV1\n\n completion: (\n modelId: QwenCompletionModelId,\n settings?: QwenCompletionSettings,\n ) => LanguageModelV1\n}\n\n/**\n * QwenProviderSettings interface holds configuration options for Qwen.\n */\nexport interface QwenProviderSettings {\n /**\n Use a different URL prefix for API calls, e.g. to use proxy servers.\n The default prefix is `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`.\n */\n baseURL?: string\n\n /**\n API key that is being send using the `Authorization` header.\n It defaults to the `DASHSCOPE_API_KEY` environment variable.\n */\n apiKey?: string\n\n /**\n Custom headers to include in the requests.\n */\n headers?: Record<string, string>\n\n /**\n Optional custom url query parameters to include in request urls.\n */\n queryParams?: Record<string, string>\n /**\n /**\n Custom fetch implementation. You can use it as a middleware to intercept requests,\n or to provide a custom fetch implementation for e.g. testing.\n */\n fetch?: FetchFunction\n\n // generateId?: () => string\n}\n\n/**\n * Creates a Qwen provider instance with the specified options.\n * @param options Provider configuration options.\n * @returns A QwenProvider instance.\n */\nexport function createQwen(options: QwenProviderSettings = {}): QwenProvider {\n // Remove trailing slash from the base URL.\n const baseURL = withoutTrailingSlash(\n options.baseURL ?? \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\",\n )\n\n // Build headers including the API key.\n const getHeaders = () => ({\n Authorization: `Bearer ${loadApiKey({\n apiKey: options.apiKey,\n environmentVariableName: \"DASHSCOPE_API_KEY\",\n description: \"Qwen API key\",\n })}`,\n ...options.headers,\n })\n\n interface CommonModelConfig {\n provider: string\n url: ({ path }: { path: string }) => string\n headers: () => Record<string, string>\n fetch?: FetchFunction\n }\n\n /**\n * Helper to get common model configuration.\n * @param modelType The type of model (chat, completion, embedding).\n */\n const getCommonModelConfig = (modelType: string): CommonModelConfig => ({\n provider: `qwen.${modelType}`,\n url: ({ path }) => {\n const url = new URL(`${baseURL}${path}`)\n if (options.queryParams) {\n // Append custom query parameters if provided.\n url.search = new URLSearchParams(options.queryParams).toString()\n }\n return url.toString()\n },\n headers: getHeaders,\n fetch: options.fetch,\n })\n\n // Create a chat language model instance.\n const createChatModel = (\n modelId: QwenChatModelId,\n settings: QwenChatSettings = {},\n ) =>\n new QwenChatLanguageModel(modelId, settings, {\n ...getCommonModelConfig(\"chat\"),\n defaultObjectGenerationMode: \"tool\",\n })\n\n // Create a completion model instance.\n const createCompletionModel = (\n modelId: QwenCompletionModelId,\n settings: QwenCompletionSettings = {},\n ) =>\n new QwenCompletionLanguageModel(\n modelId,\n settings,\n getCommonModelConfig(\"completion\"),\n )\n\n // Create a text embedding model instance.\n const createTextEmbeddingModel = (\n modelId: QwenEmbeddingModelId,\n settings: QwenEmbeddingSettings = {},\n ) =>\n new QwenEmbeddingModel(\n modelId,\n settings,\n getCommonModelConfig(\"embedding\"),\n )\n\n // Default provider returns a chat model.\n const provider = (modelId: QwenChatModelId, settings?: QwenChatSettings) =>\n createChatModel(modelId, settings)\n\n provider.chatModel = createChatModel\n provider.completion = createCompletionModel\n provider.textEmbeddingModel = createTextEmbeddingModel\n provider.languageModel = createChatModel\n return provider as QwenProvider\n}\n\nexport const qwen = createQwen()\n","import type {\n APICallError,\n LanguageModelV1,\n LanguageModelV1CallWarning,\n LanguageModelV1FinishReason,\n LanguageModelV1ObjectGenerationMode,\n LanguageModelV1StreamPart,\n} from \"@ai-sdk/provider\"\nimport type {\n FetchFunction,\n ParseResult,\n ResponseHandler,\n} from \"@ai-sdk/provider-utils\"\nimport type {\n QwenChatModelId,\n QwenChatSettings,\n} from \"./qwen-chat-settings\"\nimport type {\n QwenErrorStructure,\n} from \"./qwen-error\"\nimport type { MetadataExtractor } from \"./qwen-metadata-extractor\"\nimport {\n InvalidResponseDataError,\n} from \"@ai-sdk/provider\"\nimport {\n combineHeaders,\n createEventSourceResponseHandler,\n createJsonErrorResponseHandler,\n createJsonResponseHandler,\n generateId,\n isParsableJson,\n postJsonToApi,\n} from \"@ai-sdk/provider-utils\"\nimport { z } from \"zod\"\nimport { convertToQwenChatMessages } from \"./convert-to-qwen-chat-messages\"\nimport { getResponseMetadata } from \"./get-response-metadata\"\nimport { mapQwenFinishReason } from \"./map-qwen-finish-reason\"\nimport {\n defaultQwenErrorStructure,\n} from \"./qwen-error\"\nimport { prepareTools } from \"./qwen-prepare-tools\"\n\n/**\n * Configuration for the Qwen Chat Language Model.\n * @interface QwenChatConfig\n */\nexport interface QwenChatConfig {\n provider: string\n headers: () => Record<string, string | undefined>\n url: (options: { modelId: string, path: string }) => string\n fetch?: FetchFunction\n errorStructure?: QwenErrorStructure<any>\n metadataExtractor?: MetadataExtractor\n\n /**\n Default object generation mode that should be used with this model when\n no mode is specified. Should be the mode with the best results for this\n model. `undefined` can be specified if object generation is not supported.\n */\n defaultObjectGenerationMode?: LanguageModelV1ObjectGenerationMode\n\n /**\n * Whether the model supports structured outputs.\n */\n supportsStructuredOutputs?: boolean\n}\n\n// limited version of the schema, focussed on what is needed for the implementation\n// this approach limits breakages when the API changes and increases efficiency\nconst QwenChatResponseSchema = z.object({\n id: z.string().nullish(),\n created: z.number().nullish(),\n model: z.string().nullish(),\n choices: z.array(\n z.object({\n message: z.object({\n role: z.literal(\"assistant\").nullish(),\n content: z.string().nullish(),\n reasoning_content: z.string().nullish(),\n tool_calls: z\n .array(\n z.object({\n id: z.string().nullish(),\n type: z.literal(\"function\"),\n function: z.object({\n name: z.string(),\n arguments: z.string(),\n }),\n }),\n )\n .nullish(),\n }),\n finish_reason: z.string().nullish(),\n }),\n ),\n usage: z\n .object({\n prompt_tokens: z.number().nullish(),\n completion_tokens: z.number().nullish(),\n })\n .nullish(),\n})\n\n/**\n * Class representing the Qwen Chat language model.\n * Implements LanguageModelV1 providing text generation and streaming.\n */\n/**\n * A language model implementation for Qwen Chat API that follows the LanguageModelV1 interface.\n * Handles both regular text generation and structured outputs through various modes.\n *\n * @param options.mode - The generation mode configuration that determines how the model generates responses:\n * 'regular' for standard chat completion,\n * 'object-json' for JSON-structured outputs,\n * 'object-tool' for function-calling outputs\n * @param options.prompt - The input prompt messages to send to the model\n * @param options.maxTokens - Maximum number of tokens to generate in the response\n * @param options.temperature - Controls randomness in the model's output (0-2)\n * @param options.topP - Nucleus sampling parameter that controls diversity (0-1)\n * @param options.topK - Not supported by Qwen - will generate a warning if used\n * @param options.frequencyPenalty - Penalizes frequent tokens (-2 to 2)\n * @param options.presencePenalty - Penalizes repeated tokens (-2 to 2)\n * @param options.providerMetadata - Additional provider-specific metadata to include in the request\n * @param options.stopSequences - Array of sequences where the model should stop generating\n * @param options.responseFormat - Specifies the desired format of the response (e.g. JSON)\n * @param options.seed - Random seed for deterministic generation\n */\nexport class QwenChatLanguageModel implements LanguageModelV1 {\n readonly specificationVersion = \"v1\"\n\n readonly supportsStructuredOutputs: boolean\n\n readonly modelId: QwenChatModelId\n readonly settings: QwenChatSettings\n\n private readonly config: QwenChatConfig\n private readonly failedResponseHandler: ResponseHandler<APICallError>\n private readonly chunkSchema // type inferred via constructor\n\n /**\n * Constructs a new QwenChatLanguageModel.\n * @param modelId - The model identifier.\n * @param settings - Settings for the chat.\n * @param config - Model configuration.\n */\n constructor(\n modelId: QwenChatModelId,\n settings: QwenChatSettings,\n config: QwenChatConfig,\n ) {\n this.modelId = modelId\n this.settings = settings\n this.config = config\n\n // Initialize error handling using provided or default error structure.\n const errorStructure\n = config.errorStructure ?? defaultQwenErrorStructure\n this.chunkSchema = createQwenChatChunkSchema(\n errorStructure.errorSchema,\n )\n this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure)\n\n this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false\n }\n\n /**\n * Getter for the default object generation mode.\n */\n get defaultObjectGenerationMode(): \"json\" | \"tool\" | undefined {\n return this.config.defaultObjectGenerationMode\n }\n\n /**\n * Getter for the provider name.\n */\n get provider(): string {\n return this.config.provider\n }\n\n /**\n * Internal getter that extracts the provider options name.\n * @private\n */\n private get providerOptionsName(): string {\n return this.config.provider.split(\".\")[0].trim()\n }\n\n /**\n * Generates the arguments and warnings required for a language model generation call.\n *\n * This function prepares the argument object based on the provided generation options and mode,\n * including any necessary warnings for unsupported settings. It handles different generation modes\n * such as regular, object-json, and object-tool.\n *\n * @param options.mode - The generation mode configuration containing the type and additional settings.\n * @param options.prompt - The prompt input used to generate chat messages.\n * @param options.maxTokens - The maximum number of tokens to generate.\n * @param options.temperature - The temperature setting to control randomness in generation.\n * @param options.topP - The nucleus sampling parameter (top-p) for token selection.\n * @param options.topK - The top-k sampling parameter; if provided, it triggers a warning as it is unsupported.\n * @param options.frequencyPenalty - The penalty applied to frequently occurring tokens.\n * @param options.presencePenalty - The penalty applied based on the presence of tokens.\n * @param options.providerMetadata - Additional metadata customized for the specific provider.\n * @param options.stopSequences - An array of sequences that will signal the generation to stop.\n * @param options.responseFormat - The desired response format; supports JSON schema formatting when structured outputs are enabled.\n * @param options.seed - An optional seed value for randomization.\n *\n * @returns An object containing:\n * - args: The arguments constructed for the language model generation request.\n * - warnings: A list of warnings related to unsupported or deprecated settings.\n */\n private getArgs({\n mode,\n prompt,\n maxTokens,\n temperature,\n topP,\n topK,\n frequencyPenalty,\n presencePenalty,\n providerMetadata,\n stopSequences,\n responseFormat,\n seed,\n }: Parameters<LanguageModelV1[\"doGenerate\"]>[0]) {\n // Determine the type of generation mode.\n const type = mode.type\n\n const warnings: LanguageModelV1CallWarning[] = []\n\n // Warn if unsupported settings are used:\n if (topK != null) {\n warnings.push({\n type: \"unsupported-setting\",\n setting: \"topK\",\n })\n }\n\n if (\n responseFormat?.type === \"json\"\n && responseFormat.schema != null\n && !this.supportsStructuredOutputs\n ) {\n warnings.push({\n type: \"unsupported-setting\",\n setting: \"responseFormat\",\n details:\n \"JSON response format schema is only supported with structuredOutputs\",\n })\n }\n\n const baseArgs = {\n // model id:\n model: this.modelId,\n\n // model specific settings:\n user: this.settings.user,\n\n // standardized settings:\n max_tokens: maxTokens,\n temperature,\n top_p: topP,\n frequency_penalty: frequencyPenalty,\n presence_penalty: presencePenalty,\n response_format:\n responseFormat?.type === \"json\"\n ? this.supportsStructuredOutputs === true\n && responseFormat.schema != null\n ? {\n type: \"json_schema\",\n json_schema: {\n schema: responseFormat.schema,\n name: responseFormat.name ?? \"response\",\n description: responseFormat.description,\n },\n }\n : { type: \"json_object\" }\n : undefined,\n\n stop: stopSequences,\n seed,\n ...providerMetadata?.[this.providerOptionsName],\n\n // messages:\n messages: convertToQwenChatMessages(prompt),\n }\n\n // Handling various generation modes.\n switch (type) {\n case \"regular\": {\n const { tools, tool_choice, toolWarnings } = prepareTools({\n mode,\n structuredOutputs: this.supportsStructuredOutputs,\n })\n\n return {\n args: { ...baseArgs, tools, tool_choice },\n warnings: [...warnings, ...toolWarnings],\n }\n }\n\n case \"object-json\": {\n return {\n args: {\n ...baseArgs,\n response_format:\n this.supportsStructuredOutputs === true && mode.schema != null\n ? {\n type: \"json_schema\",\n json_schema: {\n schema: mode.schema,\n name: mode.name ?? \"response\",\n description: mode.description,\n },\n }\n : { type: \"json_object\" },\n },\n warnings,\n }\n }\n\n case \"object-tool\": {\n return {\n args: {\n ...baseArgs,\n tool_choice: {\n type: \"function\",\n function: { name: mode.tool.name },\n },\n tools: [\n {\n type: \"function\",\n function: {\n name: mode.tool.name,\n description: mode.tool.description,\n parameters: mode.tool.parameters,\n },\n },\n ],\n },\n warnings,\n }\n }\n\n default: {\n const _exhaustiveCheck: never = type\n throw new Error(`Unsupported type: ${_exhaustiveCheck}`)\n }\n }\n }\n\n /**\n * Generates a text response from the model.\n * @param options - Generation options.\n * @returns A promise resolving with the generation result.\n */\n async doGenerate(\n options: Parameters<LanguageModelV1[\"doGenerate\"]>[0],\n ): Promise<Awaited<ReturnType<LanguageModelV1[\"doGenerate\"]>>> {\n const { args, warnings } = this.getArgs({ ...options })\n\n const body = JSON.stringify(args)\n\n // Send request for generation using POST JSON.\n const {\n responseHeaders,\n value: responseBody,\n rawValue: parsedBody,\n } = await postJsonToApi({\n url: this.config.url({\n path: \"/chat/completions\",\n modelId: this.modelId,\n }),\n headers: combineHeaders(this.config.headers(), options.headers),\n body: args,\n failedResponseHandler: this.failedResponseHandler,\n successfulResponseHandler: createJsonResponseHandler(\n QwenChatResponseSchema,\n ),\n abortSignal: options.abortSignal,\n fetch: this.config.fetch,\n })\n\n const { messages: rawPrompt, ...rawSettings } = args\n const choice = responseBody.choices[0]\n const providerMetadata = this.config.metadataExtractor?.extractMetadata?.({\n parsedBody,\n })\n\n // Return structured generation details.\n return {\n text: choice.message.content ?? undefined,\n reasoning: choice.message.reasoning_content ?? undefined,\n toolCalls: choice.message.tool_calls?.map(toolCall => ({\n toolCallType: \"function\",\n toolCallId: toolCall.id ?? generateId(),\n toolName: toolCall.function.name,\n args: toolCall.function.arguments!,\n })),\n finishReason: mapQwenFinishReason(choice.finish_reason),\n usage: {\n promptTokens: responseBody.usage?.prompt_tokens ?? Number.NaN,\n completionTokens: responseBody.usage?.completion_tokens ?? Number.NaN,\n },\n ...(providerMetadata && { providerMetadata }),\n rawCall: { rawPrompt, rawSettings },\n rawResponse: { headers: responseHeaders },\n response: getResponseMetadata(responseBody),\n warnings,\n request: { body },\n }\n }\n\n /**\n * Returns a stream of model responses.\n * @param options - Stream generation options.\n * @returns A promise resolving with the stream and additional metadata.\n */\n async doStream(\n options: Parameters<LanguageModelV1[\"doStream\"]>[0],\n ): Promise<Awaited<ReturnType<LanguageModelV1[\"doStream\"]>>> {\n if (this.settings.simulateStreaming) {\n // Simulate streaming by generating a full response and splitting it.\n const result = await this.doGenerate(options)\n const simulatedStream = new ReadableStream<LanguageModelV1StreamPart>({\n start(controller) {\n // Send metadata then text deltas.\n controller.enqueue({ type: \"response-metadata\", ...result.response })\n if (result.reasoning) {\n controller.enqueue({\n type: \"reasoning\",\n textDelta: result.reasoning,\n })\n }\n if (result.text) {\n controller.enqueue({\n type: \"text-delta\",\n textDelta: result.text,\n })\n }\n if (result.toolCalls) {\n for (const toolCall of result.toolCalls) {\n controller.enqueue({\n type: \"tool-call\",\n ...toolCall,\n })\n }\n }\n controller.enqueue({\n type: \"finish\",\n finishReason: result.finishReason,\n usage: result.usage,\n logprobs: result.logprobs,\n providerMetadata: result.providerMetadata,\n })\n controller.close()\n },\n })\n return {\n stream: simulatedStream,\n rawCall: result.rawCall,\n rawResponse: result.rawResponse,\n warnings: result.warnings,\n }\n }\n\n const { args, warnings } = this.getArgs({ ...options })\n\n // Set stream flag to true for the API.\n const body = JSON.stringify({ ...args, stream: true })\n const metadataExtractor\n = this.config.metadataExtractor?.createStreamExtractor()\n\n const { responseHeaders, value: response } = await postJsonToApi({\n url: this.config.url({\n path: \"/chat/completions\",\n modelId: this.modelId,\n }),\n headers: combineHeaders(this.config.headers(), options.headers),\n body: {\n ...args,\n stream: true,\n },\n failedResponseHandler: this.failedResponseHandler,\n successfulResponseHandler: createEventSourceResponseHandler(\n this.chunkSchema,\n ),\n abortSignal: options.abortSignal,\n fetch: this.config.fetch,\n })\n\n const { messages: rawPrompt, ...rawSettings } = args\n\n const toolCalls: Array<{\n id: string\n type: \"function\"\n function: {\n name: string\n arguments: string\n }\n hasFinished: boolean\n }> = []\n\n let finishReason: LanguageModelV1FinishReason = \"unknown\"\n let usage: {\n promptTokens: number | undefined\n completionTokens: number | undefined\n } = {\n promptTokens: undefined,\n completionTokens: undefined,\n }\n let isFirstChunk = true\n\n return {\n stream: response.pipeThrough(\n new TransformStream<\n ParseResult<z.infer<typeof this.chunkSchema>>,\n LanguageModelV1StreamPart\n >({\n // Transforms incoming chunks and maps them to stream parts.\n transform(chunk, controller) {\n // If validation fails, emit an error.\n if (!chunk.success) {\n finishReason = \"error\"\n controller.enqueue({ type: \"error\", error: chunk.error })\n return\n }\n const value = chunk.value\n\n metadataExtractor?.processChunk(chunk.rawValue)\n\n // If API sends an error field in the value.\n if (\"error\" in value) {\n finishReason = \"error\"\n controller.enqueue({ type: \"error\", error: value.error.message })\n return\n }\n\n if (isFirstChunk) {\n isFirstChunk = false\n\n controller.enqueue({\n type: \"response-metadata\",\n ...getResponseMetadata(value),\n })\n }\n\n if (value.usage != null) {\n usage = {\n promptTokens: value.usage.prompt_tokens ?? undefined,\n completionTokens: value.usage.completion_tokens ?? undefined,\n }\n }\n\n const choice = value.choices[0]\n\n if (choice?.finish_reason != null) {\n finishReason = mapQwenFinishReason(\n choice.finish_reason,\n )\n }\n\n if (choice?.delta == null) {\n return\n }\n\n const delta = choice.delta\n\n // Emit reasoning before the main content.\n if (delta.reasoning_content != null) {\n controller.enqueue({\n type: \"reasoning\",\n textDelta: delta.reasoning_content,\n })\n }\n\n if (delta.content != null) {\n controller.enqueue({\n type: \"text-delta\",\n textDelta: delta.content,\n })\n }\n\n // Process and merge tool call deltas.\n if (delta.tool_calls != null) {\n for (const toolCallDelta of delta.tool_calls) {\n const index = toolCallDelta.index\n\n if (toolCalls[index] == null) {\n if (toolCallDelta.type !== \"function\") {\n throw new InvalidResponseDataError({\n data: toolCallDelta,\n message: `Expected 'function' type.`,\n })\n }\n\n if (toolCallDelta.id == null) {\n throw new InvalidResponseDataError({\n data: toolCallDelta,\n message: `Expected 'id' to be a string.`,\n })\n }\n\n if (toolCallDelta.function?.name == null) {\n throw new InvalidResponseDataError({\n data: toolCallDelta,\n message: `Expected 'function.name' to be a string.`,\n })\n }\n\n toolCalls[index] = {\n id: toolCallDelta.id,\n type: \"function\",\n function: {\n name: toolCallDelta.function.name,\n arguments: toolCallDelta.function.arguments ?? \"\",\n },\n hasFinished: false,\n }\n\n const toolCall = toolCalls[index]\n\n if (\n toolCall.function?.name != null\n && toolCall.function?.arguments != null\n ) {\n if (toolCall.function.arguments.length > 0) {\n controller.enqueue({\n type: \"tool-call-delta\",\n toolCallType: \"function\",\n toolCallId: toolCall.id,\n toolName: toolCall.function.name,\n argsTextDelta: toolCall.function.arguments,\n })\n }\n\n // If the accumulated arguments are valid JSON, finish the tool call.\n if (isParsableJson(toolCall.function.arguments)) {\n controller.enqueue({\n type: \"tool-call\",\n toolCallType: \"function\",\n toolCallId: toolCall.id ?? generateId(),\n toolName: toolCall.function.name,\n args: toolCall.function.arguments,\n })\n toolCall.hasFinished = true\n }\n }\n\n continue\n }\n\n // Merge new delta with ongoing tool call.\n const toolCall = toolCalls[index]\n\n if (toolCall.hasFinished) {\n continue\n }\n\n if (toolCallDelta.function?.arguments != null) {\n toolCall.function!.arguments\n += toolCallDelta.function?.arguments ?? \"\"\n }\n\n controller.enqueue({\n type: \"tool-call-delta\",\n toolCallType: \"function\",\n toolCallId: toolCall.id,\n toolName: toolCall.function.name,\n argsTextDelta: toolCallDelta.function.arguments ?? \"\",\n })\n\n if (\n toolCall.function?.name != null\n && toolCall.function?.arguments != null\n && isParsableJson(toolCall.function.arguments)\n ) {\n controller.enqueue({\n type: \"tool-call\",\n toolCallType: \"function\",\n toolCallId: toolCall.id ?? generateId(),\n toolName: toolCall.function.name,\n args: toolCall.function.arguments,\n })\n toolCall.hasFinished = true\n }\n }\n }\n },\n\n flush(controller) {\n // Build final metadata and finish streaming.\n const metadata = metadataExtractor?.buildMetadata()\n controller.enqueue({\n type: \"finish\",\n finishReason,\n usage: {\n promptTokens: usage.promptTokens ?? Number.NaN,\n completionTokens: usage.completionTokens ?? Number.NaN,\n },\n ...(metadata && { providerMetadata: metadata }),\n })\n },\n }),\n ),\n rawCall: { rawPrompt, rawSettings },\n rawResponse: { headers: responseHeaders },\n warnings,\n request: { body },\n }\n }\n}\n\n// limited version of the schema, focussed on what is needed for the implementation\n// this approach limits breakages when the API changes and increases efficiency\nfunction createQwenChatChunkSchema<ERROR_SCHEMA extends z.ZodType>(errorSchema: ERROR_SCHEMA) {\n return z.union([\n z.object({\n id: z.string().nullish(),\n created: z.number().nullish(),\n model: z.string().nullish(),\n choices: z.array(\n z.object({\n delta: z\n .object({\n role: z.enum([\"assistant\"]).nullish(),\n content: z.string().nullish(),\n reasoning_content: z.string().nullish(),\n tool_calls: z\n .array(\n z.object({\n index: z.number(),\n id: z.string().nullish(),\n type: z.literal(\"function\").optional(),\n function: z.object({\n name: z.string().nullish(),\n arguments: z.string().nullish(),\n }),\n }),\n )\n .nullish(),\n })\n .nullish(),\n finish_reason: z.string().nullish(),\n }),\n ),\n usage: z\n .object({\n prompt_tokens: z.number().nullish(),\n completion_tokens: z.number().nullish(),\n })\n .nullish(),\n }),\n errorSchema,\n ])\n}\n","import type {\n LanguageModelV1Prompt,\n LanguageModelV1ProviderMetadata,\n} from \"@ai-sdk/provider\"\nimport type { QwenChatPrompt } from \"./qwen-api-types\"\nimport {\n UnsupportedFunctionalityError,\n} from \"@ai-sdk/provider\"\nimport { convertUint8ArrayToBase64 } from \"@ai-sdk/provider-utils\"\n\n// JSDoc for helper function to extract Qwen metadata.\n/**\n * Extracts Qwen-specific metadata from a message.\n *\n * @param message - An object that may contain providerMetadata\n * @param message.providerMetadata - Provider-specific metadata containing Qwen configuration\n * @returns The Qwen metadata object or an empty object if none exists\n */\n\nfunction getQwenMetadata(message: {\n providerMetadata?: LanguageModelV1ProviderMetadata\n}) {\n return message?.providerMetadata?.qwen ?? {}\n}\n\n/**\n * Converts a generic language model prompt to Qwen chat messages.\n *\n * @param prompt The language model prompt to convert.\n * @returns An array of Qwen chat messages.\n */\nexport function convertToQwenChatMessages(\n prompt: LanguageModelV1Prompt,\n): QwenChatPrompt {\n const messages: QwenChatPrompt = []\n // Iterate over each prompt message.\n for (const { role, content, ...message } of prompt) {\n const metadata = getQwenMetadata({ ...message })\n switch (role) {\n case \"system\": {\n // System messages are sent directly with metadata.\n messages.push({ role: \"system\", content, ...metadata })\n break\n }\n\n case \"user\": {\n if (content.length === 1 && content[0].type === \"text\") {\n // For a single text element, simplify the conversion.\n messages.push({\n role: \"user\",\n content: content[0].text,\n ...getQwenMetadata(content[0]),\n })\n break\n }\n // For multiple content parts, process each part.\n messages.push({\n role: \"user\",\n content: content.map((part) => {\n const partMetadata = getQwenMetadata(part)\n switch (part.type) {\n case \"text\": {\n // Plain text conversion.\n return { type: \"text\", text: part.text, ...partMetadata }\n }\n case \"image\": {\n // Convert images and encode if necessary.\n return {\n type: \"image_url\",\n image_url: {\n url:\n part.image instanceof URL\n ? part.image.toString()\n : `data:${\n part.mimeType ?? \"image/jpeg\"\n };base64,${convertUint8ArrayToBase64(part.image)}`,\n },\n ...partMetadata,\n }\n }\n default: {\n // Unsupported file content parts trigger an error.\n throw new UnsupportedFunctionalityError({\n functionality: \"File content parts in user messages\",\n })\n }\n }\n }),\n ...metadata,\n })\n\n break\n }\n\n case \"assistant\": {\n // Build text response and accumulate function/tool calls.\n let text = \"\"\n const toolCalls: Array<{\n id: string\n type: \"function\"\n function: { name: string, arguments: string }\n }> = []\n\n for (const part of content) {\n const partMetadata = getQwenMetadata(part)\n switch (part.type) {\n case \"text\": {\n // Append each text part.\n text += part.text\n break\n }\n case \"tool-call\": {\n // Convert tool calls to function calls with serialized arguments.\n toolCalls.push({\n id: part.toolCallId,\n type: \"function\",\n function: {\n name: part.toolName,\n arguments: JSON.stringify(part.args),\n },\n ...partMetadata,\n })\n break\n }\n default: {\n // This branch should never occur.\n const _exhaustiveCheck: never = part\n throw new Error(`Unsupported part: ${_exhaustiveCheck}`)\n }\n }\n }\n\n messages.push({\n role: \"assistant\",\n content: text,\n tool_calls: toolCalls.length > 0 ? toolCalls : undefined,\n ...metadata,\n })\n\n break\n }\n\n case \"tool\": {\n // Process tool responses by converting result to JSON string.\n for (const toolResponse of content) {\n const toolResponseMetadata = getQwenMetadata(toolResponse)\n messages.push({\n role: \"tool\",\n tool_call_id: toolResponse.toolCallId,\n content: JSON.stringify(toolResponse.result),\n ...toolResponseMetadata,\n })\n }\n break\n }\n\n default: {\n // Ensure all roles are handled.\n const _exhaustiveCheck: never = role\n throw new Error(`Unsupported role: ${_exhaustiveCheck}`)\n }\n }\n }\n\n return messages\n}\n","/**\n * Generates metadata for a response object.\n *\n * @param {object} params - Input parameters.\n * @param {string | undefined | null} params.id - A unique identifier for the response.\n * @param {string | undefined | null} params.model - Identifier for the model used.\n * @param {number | undefined | null} params.created - Unix timestamp (in seconds) of when the response was created.\n * @returns {object} An object containing normalized metadata.\n */\nexport function getResponseMetadata({\n id,\n model,\n created,\n}: {\n id?: string | undefined | null\n created?: number | undefined | null\n model?: string | undefined | null\n}) {\n // Normalize and construct the response metadata object.\n return {\n // Assign 'id' if provided; otherwise, leave as undefined.\n id: id ?? undefined,\n // Map 'model' to 'modelId' for improved clarity; assign if provided.\n modelId: model ?? undefined,\n // If 'created' is provided, convert the Unix timestamp (seconds) to a JavaScript Date object.\n timestamp: created != null ? new Date(created * 1000) : undefined,\n }\n}\n","import type { LanguageModelV1FinishReason } from \"@ai-sdk/provider\"\n\n/**\n * Maps the finish reason from the backend response to a standardized format.\n *\n * @param finishReason - The original finish reason string.\n * @returns The mapped LanguageModelV1FinishReason.\n */\nexport function mapQwenFinishReason(\n finishReason: string | null | undefined,\n): LanguageModelV1FinishReason {\n switch (finishReason) {\n case \"stop\":\n return \"stop\"\n case \"length\":\n return \"length\"\n case \"tool_calls\":\n return \"tool-calls\"\n default:\n return \"unknown\"\n }\n}\n","import type { ZodSchema } from \"zod\"\nimport { createJsonErrorResponseHandler } from \"@ai-sdk/provider-utils\"\nimport { z } from \"zod\"\n\n/**\n * Schema defining the structure of a Qwen error response.\n */\nconst qwenErrorDataSchema = z.object({\n object: z.literal(\"error\"),\n message: z.string(),\n type: z.string(),\n param: z.string().nullable(),\n code: z.string().nullable(),\n})\n\nexport type QwenErrorData = z.infer<typeof qwenErrorDataSchema>\n\n/**\n * Interface for defining error structures for Qwen.\n */\nexport interface QwenErrorStructure<T> {\n /**\n * Zod schema to validate error data.\n */\n errorSchema: ZodSchema<T>\n /**\n * Maps error details to a human-readable message.\n */\n errorToMessage: (error: T) => string\n /**\n * Determines if an error is retryable.\n */\n isRetryable?: (response: Response, error?: T) => boolean\n}\n\n// Create a handler for failed responses using the defined schema.\nexport const qwenFailedResponseHandler = createJsonErrorResponseHandler({\n errorSchema: qwenErrorDataSchema,\n errorToMessage: error => error.message,\n})\n\nexport const defaultQwenErrorStructure: QwenErrorStructure<QwenErrorData> = {\n errorSchema: qwenErrorDataSchema,\n errorToMessage: data => data.message,\n}\n","import type {\n LanguageModelV1,\n LanguageModelV1CallWarning,\n} from \"@ai-sdk/provider\"\nimport {\n UnsupportedFunctionalityError,\n} from \"@ai-sdk/provider\"\n\n/**\n * Prepares the tool configuration for language model generation.\n * @param param0 Object containing mode details and structured output flag.\n * @returns An object with tools, tool choice and any warnings.\n */\nexport function prepareTools({\n mode,\n}: {\n mode: Parameters<LanguageModelV1[\"doGenerate\"]>[0][\"mode\"] & {\n type: \"regular\"\n }\n structuredOutputs: boolean\n}): {\n tools:\n | undefined\n | Array<{\n type: \"function\"\n function: {\n name: string\n description: string | undefined\n parameters: unknown\n }\n }>\n tool_choice:\n | { type: \"function\", function: { name: string } }\n | \"auto\"\n | \"none\"\n | \"required\"\n | undefined\n toolWarnings: LanguageModelV1CallWarning[]\n } {\n // Normalize tools array by converting empty array to undefined.\n const tools = mode.tools?.length ? mode.tools : undefined\n const toolWarnings: LanguageModelV1CallWarning[] = []\n\n if (tools == null) {\n return { tools: undefined, tool_choice: undefined, toolWarnings }\n }\n\n const toolChoice = mode.toolChoice\n const qwenCompatTools: Array<{\n type: \"function\"\n function: {\n name: string\n description: string | undefined\n parameters: unknown\n }\n }> = []\n\n // Process each tool and format for compatibility.\n for (const tool of tools) {\n if (tool.type === \"provider-defined\") {\n // Warn if the tool is provider-defined.\n toolWarnings.push({ type: \"unsupported-tool\", tool })\n }\n else {\n qwenCompatTools.push({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters,\n },\n })\n }\n }\n\n if (toolChoice == null) {\n return { tools: qwenCompatTools, tool_choice: undefined, toolWarnings }\n }\n\n const type = toolChoice.type\n\n // Determine tool choice strategy.\n switch (type) {\n case \"auto\":\n case \"none\":\n case \"required\":\n return { tools: qwenCompatTools, tool_choice: type, toolWarnings }\n case \"tool\":\n return {\n tools: qwenCompatTools,\n tool_choice: {\n type: \"function\",\n function: {\n name: toolChoice.toolName,\n },\n },\n toolWarnings,\n }\n default: {\n // Exhaustive check to ensure all cases are handled.\n const _exhaustiveCheck: never = type\n throw new UnsupportedFunctionalityError({\n functionality: `Unsupported tool choice type: ${_exhaustiveCheck}`,\n })\n }\n }\n}\n","import type {\n APICallError,\n LanguageModelV1,\n LanguageModelV1CallWarning,\n LanguageModelV1FinishReason,\n LanguageModelV1StreamPart,\n} from \"@ai-sdk/provider\"\nimport type {\n FetchFunction,\n ParseResult,\n ResponseHandler,\n} from \"@ai-sdk/provider-utils\"\nimport type {\n QwenCompletionModelId,\n QwenCompletionSettings,\n} from \"./qwen-completion-settings\"\nimport type {\n QwenErrorStructure,\n} from \"./qwen-error\"\nimport {\n UnsupportedFunctionalityError,\n} from \"@ai-sdk/provider\"\nimport {\n combineHeaders,\n createEventSourceResponseHandler,\n createJsonErrorResponseHandler,\n createJsonResponseHandler,\n postJsonToApi,\n} from \"@ai-sdk/provider-utils\"\nimport { z } from \"zod\"\nimport { convertToQwenCompletionPrompt } from \"./convert-to-qwen-completion-prompt\"\nimport { getResponseMetadata } from \"./get-response-metadata\"\nimport { mapQwenFinishReason } from \"./map-qwen-finish-reason\"\nimport {\n defaultQwenErrorStructure,\n} from \"./qwen-error\"\n\ninterface QwenCompletionConfig {\n provider: string\n headers: () => Record<string, string | undefined>\n url: (options: { modelId: string, path: string }) => string\n fetch?: FetchFunction\n errorStructure?: QwenErrorStructure<any>\n}\n// limited version of the schema, focussed on what is needed for the implementation\n// this approach limits breakages when the API changes and increases efficiency\nconst QwenCompletionResponseSchema = z.object({\n id: z.string().nullish(),\n created: z.number().nullish(),\n model: z.string().nullish(),\n choices: z.array(\n z.object({\n text: z.string(),\n finish_reason: z.string(),\n }),\n ),\n usage: z\n .object({\n prompt_tokens: z.number(),\n completion_tokens: z.number(),\n })\n .nullish(),\n})\n\n/**\n * A language model implementation for Qwen completions.\n *\n * @remarks\n * Implements the LanguageModelV1 interface and handles regular, streaming completions.\n */\nexport class QwenCompletionLanguageModel\nimplements LanguageModelV1 {\n readonly specificationVersion = \"v1\"\n readonly defaultObjectGenerationMode = undefined\n\n readonly modelId: QwenCompletionModelId\n readonly settings: QwenCompletionSettings\n\n private readonly config: QwenCompletionConfig\n private readonly failedResponseHandler: ResponseHandler<APICallError>\n private readonly chunkSchema // type inferred via constructor\n\n /**\n * Creates an instance of QwenCompletionLanguageModel.\n *\n * @param modelId - The model identifier.\n * @param settings - The settings specific for Qwen completions.\n * @param config - The configuration object which includes provider options and error handling.\n */\n constructor(\n modelId: QwenCompletionModelId,\n settings: QwenCompletionSettings,\n config: QwenCompletionConfig,\n ) {\n this.modelId = modelId\n this.settings = settings\n this.config = config\n\n // Initialize error handling schema and response handler.\n const errorStructure\n = config.errorStructure ?? defaultQwenErrorStructure\n this.chunkSchema = createQwenCompletionChunkSchema(\n errorStructure.errorSchema,\n )\n this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure)\n }\n\n get provider(): string {\n return this.config.provider\n }\n\n private get providerOptionsName(): string {\n return this.config.provider.split(\".\")[0].trim()\n }\n\n /**\n * Generates the arguments for invoking the LanguageModelV1 doGenerate method.\n *\n * This function processes the given options to build a configuration object for the request. It converts the\n * input prompt to a Qwen-specific format, merges stop sequences from both the user and the prompt conversion,\n * and applies standardized settings for model generation. Additionally, it emits warnings for any unsupported\n * settings (e.g., topK and non-text response formats) and throws errors if unsupported functionalities\n * (such as tools, toolChoice, or specific modes) are detected.\n *\n * @param options - The configuration options for generating completion arguments.\n * @param options.mode - The mode for generation, specifying the type and any additional functionalities.\n * @param options.inputFormat - The format of the input prompt.\n * @param options.prompt - The prompt text to be processed and used for generating a completion.\n * @param options.maxTokens - The maximum number of tokens to generate.\n * @param options.temperature - The sampling temperature for generation randomness.\n * @param options.topP - The nucleus sampling probability threshold.\n * @param options.topK - The Top-K sampling parameter (unsupported; will trigger a warning if provided).\n * @param options.frequencyPenalty - The frequency penalty to reduce token repetition.\n * @param options.presencePenalty - The presence penalty to encourage novel token generation.\n * @param options.stopSequences - Additional stop sequences provided by the user.\n * @param options.responseFormat - The desired response format (non-text formats will trigger a warning).\n * @param options.seed - The seed for random number generation, ensuring deterministic outputs.\n * @param options.providerMetadata - Additional metadata to be merged into the provider-specific settings.\n *\n * @returns An object containing:\n * - args: The built arguments object ready to be passed to the generation method.\n * - warnings: A list of warnings for unsupported settings that were detected.\n *\n * @throws UnsupportedFunctionalityError If unsupported functionalities (tools, toolChoice, object-json mode,\n * or object-tool mode) are specified in the mode configuration.\n */\n private getArgs({\n mode,\n inputFormat,\n prompt,\n maxTokens,\n temperature,\n topP,\n topK,\n frequencyPenalty,\n presencePenalty,\n stopSequences: userStopSequences,\n responseFormat,\n seed,\n providerMetadata,\n }: Parameters<LanguageModelV1[\"doGenerate\"]>[0]) {\n const type = mode.type\n\n const warnings: LanguageModelV1CallWarning[] = []\n\n // Warn if unsupported settings are used.\n if (topK != null) {\n warnings.push({\n type: \"unsupported-setting\",\n setting: \"topK\",\n })\n }\n\n if (responseFormat != null && responseFormat.type !== \"text\") {\n warnings.push({\n type: \"unsupported-setting\",\n setting: \"responseFormat\",\n details: \"JSON response format is not supported.\",\n })\n }\n\n // Convert prompt to Qwen-specific prompt info.\n const { prompt: completionPrompt, stopSequences }\n = convertToQwenCompletionPrompt({ prompt, inputFormat })\n\n const stop = [...(stopSequences ?? []), ...(userStopSequences ?? [])]\n\n const baseArgs = {\n // Model id and settings:\n model: this.modelId,\n echo: this.