UNPKG

@friendliai/ai-provider

Version:

<!-- header start --> <p align="center"> <img src="https://huggingface.co/datasets/FriendliAI/documentation-images/resolve/main/model-card-assets/friendliai.png" width="100%" alt="FriendliAI Logo"> </p> <!-- header end -->

1 lines 77.1 kB
{"version":3,"sources":["../src/friendli-provider.ts","../src/friendli-chat-language-model.ts","../src/friendli-error.ts","../src/friendli-prepare-tools.ts","../src/friendli-settings.ts","../src/friendli-tools.ts","../src/get-available-models.ts"],"sourcesContent":["import { OpenAICompatibleCompletionLanguageModel } from '@ai-sdk/openai-compatible';\nimport {\n EmbeddingModelV3,\n ImageModelV3,\n LanguageModelV3,\n NoSuchModelError,\n ProviderV3,\n SpeechModelV3,\n TranscriptionModelV3,\n} from '@ai-sdk/provider';\nimport { FetchFunction, loadApiKey, withoutTrailingSlash } from '@ai-sdk/provider-utils';\n\nimport { FriendliAIChatLanguageModel } from './friendli-chat-language-model';\nimport { friendliaiErrorStructure } from './friendli-error';\nimport {\n FriendliAILanguageModelId,\n FriendliAIServerlessModelId,\n FriendliAIServerlessModelIds,\n} from './friendli-settings';\nimport { friendliTools } from './friendli-tools';\nimport type { FriendliAvailableModelsResponse } from './get-available-models';\nimport { getAvailableModelsImpl } from './get-available-models';\n\nexport interface FriendliAIProviderSettings {\n /**\n * FriendliAI API key. (FRIENDLI_TOKEN)\n */\n apiKey?: string;\n /**\n * Base URL for the API calls.\n */\n baseURL?: string | 'auto' | 'dedicated' | 'serverless' | 'serverless-tools';\n /**\n * Custom headers to include in the requests.\n */\n headers?: Record<string, string>;\n /**\n * FriendliAI Team ID.\n */\n teamId?: string;\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 * Whether to include usage information in the response.\n */\n includeUsage?: boolean;\n}\n\nexport interface FriendliAIProvider extends ProviderV3 {\n /**\n * Creates a model for text generation.\n */\n (modelId: FriendliAILanguageModelId): LanguageModelV3;\n /**\n * Creates a chat model for text generation.\n */\n languageModel(modelId: FriendliAILanguageModelId): LanguageModelV3;\n /**\n * Creates a chat model for text generation.\n */\n chat(modelId: FriendliAILanguageModelId): LanguageModelV3;\n /**\n * Creates a completion model for text generation.\n */\n completion(modelId: FriendliAILanguageModelId): LanguageModelV3;\n /**\n * Creates an embedding model for text generation.\n * TODO: Implement for Dedicated users\n */\n embedding(modelId: string & {}): LanguageModelV3;\n embeddingModel(modelId: string & {}): LanguageModelV3;\n /**\n * Returns the available models and their metadata.\n */\n getAvailableModels(options?: { graphqlURL?: string }): Promise<FriendliAvailableModelsResponse>;\n embedding(modelId: string & {}): EmbeddingModelV3;\n embeddingModel(modelId: string & {}): EmbeddingModelV3;\n /**\n * Creates a model for image generation.\n * TODO: Implement for Dedicated users\n */\n imageModel(modelId: string & {}): ImageModelV3;\n\n /**\n * Creates a model for transcription.\n * TODO: Implement for Dedicated users\n */\n transcription(modelId: string & {}): TranscriptionModelV3;\n\n /**\n * Creates a model for speech generation.\n * TODO: Implement for Dedicated users\n */\n speech(modelId: string & {}): SpeechModelV3;\n\n /**\n * Friendli-specific tools.\n */\n tools: typeof friendliTools;\n}\n\n/**\nCreate an FriendliAI provider instance.\n */\nexport function createFriendli(options: FriendliAIProviderSettings = {}): FriendliAIProvider {\n const getHeaders = () => ({\n Authorization: `Bearer ${loadApiKey({\n apiKey: options.apiKey,\n environmentVariableName: 'FRIENDLI_TOKEN',\n description: 'FRIENDLI_TOKEN',\n })}`,\n 'X-Friendli-Team': options.teamId,\n ...options.headers,\n });\n\n const baseURLAutoSelect = (\n modelId: string,\n baseURL: string | 'dedicated' | 'serverless' | 'serverless-tools' | undefined\n ): {\n baseURL: string;\n type: 'dedicated' | 'serverless' | 'serverless-tools' | 'custom';\n } => {\n const FriendliBaseURL = {\n serverless: 'https://api.friendli.ai/serverless/v1',\n serverless_tools: 'https://api.friendli.ai/serverless/tools/v1',\n dedicated: 'https://api.friendli.ai/dedicated/v1',\n };\n\n // Ignore options if baseURL is specified\n const customBaseURL = withoutTrailingSlash(baseURL);\n if (\n typeof customBaseURL === 'string' &&\n customBaseURL !== 'dedicated' &&\n customBaseURL !== 'serverless' &&\n customBaseURL !== 'serverless-tools'\n ) {\n return { baseURL: customBaseURL, type: 'custom' };\n }\n\n switch (baseURL) {\n case 'dedicated':\n return {\n baseURL: FriendliBaseURL.dedicated,\n type: 'dedicated',\n };\n case 'serverless':\n return {\n baseURL: FriendliBaseURL.serverless,\n type: 'serverless',\n };\n case 'serverless-tools':\n return {\n baseURL: FriendliBaseURL.serverless_tools,\n type: 'serverless-tools',\n };\n default:\n if (FriendliAIServerlessModelIds.includes(modelId as FriendliAIServerlessModelId)) {\n return {\n baseURL: FriendliBaseURL.serverless,\n type: 'serverless',\n };\n } else {\n return {\n baseURL: FriendliBaseURL.dedicated,\n type: 'dedicated',\n };\n }\n }\n };\n\n const createLanguageModel = (modelId: FriendliAILanguageModelId) => {\n const { baseURL, type } = baseURLAutoSelect(modelId, options.baseURL);\n\n return new FriendliAIChatLanguageModel(modelId, {\n provider: `friendliai.${type}.chat`,\n url: ({ path }: { path: string }) => `${baseURL}${path}`,\n headers: getHeaders,\n fetch: options.fetch,\n includeUsage: options.includeUsage,\n });\n };\n\n const createCompletionModel = (modelId: FriendliAILanguageModelId) => {\n const { baseURL, type } = baseURLAutoSelect(modelId, options.baseURL);\n\n return new OpenAICompatibleCompletionLanguageModel(modelId, {\n provider: `friendliai.${type}.completion`,\n url: ({ path }) => `${baseURL}${path}`,\n headers: getHeaders,\n fetch: options.fetch,\n errorStructure: friendliaiErrorStructure,\n });\n };\n\n const createTextEmbeddingModel = (modelId: string) => {\n throw new NoSuchModelError({ modelId, modelType: 'embeddingModel' });\n };\n const createImageModel = (modelId: string) => {\n throw new NoSuchModelError({ modelId, modelType: 'imageModel' });\n };\n const createTranscriptionModel = (modelId: string) => {\n throw new NoSuchModelError({ modelId, modelType: 'languageModel' });\n };\n const createSpeechModel = (modelId: string) => {\n throw new NoSuchModelError({ modelId, modelType: 'languageModel' });\n };\n\n const provider = (modelId: FriendliAILanguageModelId) => createLanguageModel(modelId);\n\n provider.languageModel = createLanguageModel;\n provider.chat = createLanguageModel;\n provider.completion = createCompletionModel;\n\n // TODO: Implement for Dedicated users\n provider.embedding = createTextEmbeddingModel;\n provider.embeddingModel = createTextEmbeddingModel;\n (provider as unknown as FriendliAIProvider).getAvailableModels = async (opts?: {\n graphqlURL?: string;\n }) => {\n const defaultURL = 'https://api-internal.friendli.ai/api/graphql';\n const graphqlURL = opts?.graphqlURL ?? defaultURL;\n const apiKey = options.apiKey;\n const teamId = options.teamId;\n const headers = options.headers;\n return getAvailableModelsImpl({ apiKey, teamId, headers, graphqlURL });\n };\n provider.imageModel = createImageModel;\n provider.transcription = createTranscriptionModel;\n provider.speech = createSpeechModel;\n\n provider.tools = friendliTools;\n\n // 'getAvailableModels' is declared here.\n return provider as unknown as FriendliAIProvider;\n}\n\n/**\n * Default FriendliAI provider instance.\n */\nexport const friendli = createFriendli();\n","import { MetadataExtractor, ProviderErrorStructure } from '@ai-sdk/openai-compatible';\nimport {\n convertOpenAICompatibleChatUsage,\n convertToOpenAICompatibleChatMessages,\n getResponseMetadata,\n mapOpenAICompatibleFinishReason,\n} from '@ai-sdk/openai-compatible/internal';\nimport {\n APICallError,\n InvalidResponseDataError,\n LanguageModelV3,\n LanguageModelV3Content,\n LanguageModelV3FinishReason,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n SharedV3ProviderMetadata,\n SharedV3Warning,\n} from '@ai-sdk/provider';\nimport {\n combineHeaders,\n createEventSourceResponseHandler,\n createJsonResponseHandler,\n FetchFunction,\n generateId,\n isParsableJson,\n ParseResult,\n parseProviderOptions,\n postJsonToApi,\n ResponseHandler,\n} from '@ai-sdk/provider-utils';\nimport { z } from 'zod/v4';\n\nimport {\n friendliaiErrorSchema,\n friendliaiErrorStructure,\n friendliaiFailedResponseHandler,\n tryWrapFriendliJsonEnvelopeError,\n} from './friendli-error';\nimport { prepareTools } from './friendli-prepare-tools';\nimport { FriendliAILanguageModelId } from './friendli-settings';\n\nexport type OpenAICompatibleChatConfig = {\n provider: string;\n headers: () => Record<string, string | undefined>;\n url: (options: { modelId: string; path: string }) => string;\n fetch?: FetchFunction;\n includeUsage?: boolean;\n errorStructure?: ProviderErrorStructure<z.infer<typeof friendliaiErrorSchema>>;\n metadataExtractor?: MetadataExtractor;\n\n /**\n * Whether the model supports structured outputs.\n */\n supportsStructuredOutputs?: boolean;\n\n /**\n * The supported URLs for the model.\n */\n supportedUrls?: () => LanguageModelV3['supportedUrls'];\n};\n\ntype HostedToolExecutionChunk = {\n name: string;\n status: 'ENDED' | 'STARTED' | 'ERRORED' | 'UPDATING';\n message: null;\n parameters: Array<{ name: string; value: string }>;\n result: string | null;\n error: { type: 'INVALID_PARAMETER' | 'UNKNOWN'; msg: string } | null;\n timestamp: number;\n usage: null;\n tool_call_id: string | null;\n};\n\ntype OpenAIChatUsage = {\n prompt_tokens?: number | null;\n completion_tokens?: number | null;\n total_tokens?: number | null;\n prompt_tokens_details?: { cached_tokens?: number | null } | null;\n completion_tokens_details?: {\n reasoning_tokens?: number | null;\n accepted_prediction_tokens?: number | null;\n rejected_prediction_tokens?: number | null;\n } | null;\n};\n\ntype OpenAIChatToolCallDelta = {\n index: number;\n id?: string | null;\n type?: 'function' | null;\n function?: { name?: string | null; arguments?: string | null } | null;\n};\n\ntype OpenAIChatDelta = {\n role?: 'assistant' | null;\n content?: string | null;\n reasoning_content?: string | null;\n tool_calls?: OpenAIChatToolCallDelta[] | null;\n};\n\ntype OpenAIChatChoice = {\n delta?: OpenAIChatDelta | null;\n finish_reason?: string | null;\n};\n\ntype OpenAIChatChunk = {\n id?: string | null;\n created?: number | null;\n model?: string | null;\n choices: OpenAIChatChoice[];\n usage?: OpenAIChatUsage | null;\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value != null;\n}\n\nfunction isHostedToolExecutionChunk(value: unknown): value is HostedToolExecutionChunk {\n if (!isRecord(value)) return false;\n return (\n typeof value.status === 'string' &&\n typeof value.name === 'string' &&\n Array.isArray(value.parameters)\n );\n}\n\nfunction getChunkErrorMessage(value: unknown): string | undefined {\n if (!isRecord(value)) return undefined;\n\n if (typeof value.message === 'string') {\n return value.message;\n }\n\n const nestedError = value.error;\n if (isRecord(nestedError) && typeof nestedError.message === 'string') {\n return nestedError.message;\n }\n\n return undefined;\n}\n\nfunction isOpenAIChatChunk(value: unknown): value is OpenAIChatChunk {\n if (!isRecord(value)) return false;\n return Array.isArray(value.choices);\n}\n\n/**\n * Adds reasoning_content field to assistant messages for interleaved thinking support.\n * This enables models like MiniMax-M2.5, GLM-5.1 to maintain reasoning context across conversation turns.\n *\n * Note: We use `reasoning_content` which is consistent with FriendliAI's response format.\n */\nfunction addReasoningToMessages<T extends Array<{ role: string; reasoning_content?: string }>>(\n prompt: LanguageModelV3Prompt,\n messages: T\n): T {\n // Track assistant message indices in both arrays\n let promptAssistantIndex = 0;\n\n for (const promptMessage of prompt) {\n if (promptMessage.role === 'assistant') {\n // Extract reasoning from the original prompt\n const reasoningText = promptMessage.content\n .filter((part): part is { type: 'reasoning'; text: string } => part.type === 'reasoning')\n .map((part) => part.text)\n .join('\\n');\n\n if (reasoningText) {\n // Find the corresponding assistant message in converted messages\n let messagesAssistantIndex = 0;\n for (let i = 0; i < messages.length; i++) {\n if (messages[i].role === 'assistant') {\n if (messagesAssistantIndex === promptAssistantIndex) {\n messages[i].reasoning_content = reasoningText;\n break;\n }\n messagesAssistantIndex++;\n }\n }\n }\n promptAssistantIndex++;\n }\n }\n\n return messages;\n}\n\nexport class FriendliAIChatLanguageModel implements LanguageModelV3 {\n readonly specificationVersion = 'v3';\n\n readonly supportsStructuredOutputs: boolean;\n\n readonly modelId: FriendliAILanguageModelId;\n // readonly settings: FriendliAIChatSettings\n\n private readonly config: OpenAICompatibleChatConfig;\n private readonly failedResponseHandler: ResponseHandler<APICallError>;\n private readonly chunkSchema; // type inferred via constructor\n\n constructor(modelId: FriendliAILanguageModelId, config: OpenAICompatibleChatConfig) {\n this.modelId = modelId;\n // this.settings = settings\n this.config = config;\n\n const errorStructure = friendliaiErrorStructure;\n this.chunkSchema = createOpenAICompatibleChatChunkSchema(errorStructure.errorSchema);\n\n this.failedResponseHandler = friendliaiFailedResponseHandler;\n\n this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? true;\n }\n\n get provider(): string {\n return this.config.provider;\n }\n\n get supportedUrls() {\n return this.config.supportedUrls?.() ?? {};\n }\n private async getArgs({\n prompt,\n maxOutputTokens,\n temperature,\n topP,\n topK,\n frequencyPenalty,\n presencePenalty,\n providerOptions,\n stopSequences,\n responseFormat,\n seed,\n toolChoice,\n tools,\n stream,\n }: Parameters<LanguageModelV3['doGenerate']>[0] & {\n stream: boolean;\n }) {\n const warnings: SharedV3Warning[] = [];\n\n const friendliOptions = await parseProviderOptions({\n provider: 'friendliai',\n providerOptions,\n schema: friendliProviderOptionsSchema,\n });\n\n const legacyFriendliOptions = await parseProviderOptions({\n provider: 'friendli',\n providerOptions,\n schema: friendliProviderOptionsSchema,\n });\n\n const options = {\n ...legacyFriendliOptions,\n ...friendliOptions,\n };\n\n if (\n responseFormat?.type === 'json' &&\n responseFormat.schema != null &&\n !this.supportsStructuredOutputs\n ) {\n warnings.push({\n type: 'unsupported',\n feature: 'responseFormat',\n details: 'JSON response format schema is only supported with structuredOutputs',\n });\n }\n\n const {\n tools: openaiTools,\n toolChoice: openaiToolChoice,\n toolWarnings,\n } = prepareTools({\n tools,\n toolChoice,\n });\n\n const isToolsPresent = openaiTools != null && openaiTools.length > 0;\n\n if (isToolsPresent && (responseFormat != null || options?.regex != null)) {\n warnings.push({\n type: 'unsupported',\n feature: 'responseFormat',\n details: 'response_format is not supported when tools are present.',\n });\n }\n\n return {\n args: {\n // >>> hard-coded default options >>>\n parse_reasoning: true,\n // <<< hard-coded default options <<<\n\n model: this.modelId,\n\n // standardized settings:\n stream: stream,\n max_tokens: maxOutputTokens,\n temperature,\n top_p: topP,\n top_k: topK,\n frequency_penalty: frequencyPenalty,\n presence_penalty: presencePenalty,\n response_format:\n isToolsPresent === false\n ? responseFormat?.type === 'json'\n ? this.supportsStructuredOutputs === true && 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 : options?.regex != null\n ? {\n type: 'regex',\n schema: options.regex,\n }\n : undefined\n : undefined,\n\n stop: stopSequences,\n seed,\n\n min_p: options?.minP,\n repetition_penalty: options?.repetitionPenalty,\n xtc_threshold: options?.xtcThreshold,\n xtc_probability: options?.xtcProbability,\n\n ...(options?.chat_template_kwargs\n ? { chat_template_kwargs: options.chat_template_kwargs }\n : {}),\n\n // messages:\n // Use addReasoningToMessages to include reasoning_content in assistant messages\n // for interleaved thinking support\n messages: addReasoningToMessages(prompt, convertToOpenAICompatibleChatMessages(prompt)),\n\n // tools:\n tools: openaiTools,\n tool_choice: openaiToolChoice,\n parallel_tool_calls: options?.parallelToolCalls,\n },\n warnings: [...warnings, ...toolWarnings],\n };\n }\n\n async doGenerate(\n options: Parameters<LanguageModelV3['doGenerate']>[0]\n ): Promise<Awaited<ReturnType<LanguageModelV3['doGenerate']>>> {\n const { args, warnings } = await this.getArgs({ ...options, stream: false });\n\n const body = JSON.stringify(args);\n\n const response = await (async () => {\n try {\n return 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(OpenAICompatibleChatResponseSchema),\n abortSignal: options.abortSignal,\n fetch: this.config.fetch,\n });\n } catch (error) {\n const wrappedError = await tryWrapFriendliJsonEnvelopeError(error);\n\n if (wrappedError != null) {\n throw wrappedError;\n }\n\n throw error;\n }\n })();\n\n const { responseHeaders, value: responseBody, rawValue: rawResponse } = response;\n\n const choice = responseBody.choices[0];\n const content: Array<LanguageModelV3Content> = [];\n\n // text content:\n const text = choice.message.content;\n if (text != null && text.length > 0) {\n content.push({ type: 'text', text });\n }\n\n // reasoning content:\n const reasoning = choice.message.reasoning_content;\n if (reasoning != null && reasoning.length > 0) {\n content.push({\n type: 'reasoning',\n text: reasoning,\n });\n }\n\n // tool calls:\n if (choice.message.tool_calls != null) {\n for (const toolCall of choice.message.tool_calls) {\n content.push({\n type: 'tool-call',\n toolCallId: toolCall.id ?? generateId(),\n toolName: toolCall.function.name,\n input: toolCall.function.arguments!,\n });\n }\n }\n\n return {\n content,\n finishReason: {\n unified: mapOpenAICompatibleFinishReason(choice.finish_reason),\n raw: choice.finish_reason ?? undefined,\n },\n usage: convertOpenAICompatibleChatUsage(responseBody.usage),\n // providerMetadata,\n request: { body },\n response: {\n ...getResponseMetadata(responseBody),\n headers: responseHeaders,\n body: rawResponse,\n },\n warnings,\n };\n }\n\n async doStream(\n options: Parameters<LanguageModelV3['doStream']>[0]\n ): Promise<Awaited<ReturnType<LanguageModelV3['doStream']>>> {\n const { args, warnings } = await this.getArgs({ ...options, stream: true });\n\n const body = {\n ...args,\n stream: true,\n\n // only include stream_options when in strict compatibility mode:\n stream_options: this.config.includeUsage ? { include_usage: true } : undefined,\n };\n\n const metadataExtractor = 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 failedResponseHandler: this.failedResponseHandler,\n successfulResponseHandler: createEventSourceResponseHandler(this.chunkSchema),\n abortSignal: options.abortSignal,\n fetch: this.config.fetch,\n });\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: LanguageModelV3FinishReason = {\n unified: 'other',\n raw: undefined,\n };\n let usage: z.infer<typeof openaiCompatibleTokenUsageSchema> | undefined = undefined;\n let isFirstChunk = true;\n const providerOptionsName = 'friendliai';\n\n // Track IDs for text and reasoning events\n let currentTextId: string | null = null;\n let currentReasoningId: string | null = null;\n\n return {\n stream: response.pipeThrough(\n new TransformStream<\n ParseResult<z.infer<typeof this.chunkSchema>>,\n LanguageModelV3StreamPart\n >({\n start(controller) {\n controller.enqueue({ type: 'stream-start', warnings });\n },\n\n // NOTE: Chunk values can contain OpenAI-compatible deltas, hosted tool events, and error events.\n // We narrow with type guards for safe handling.\n transform(chunk, controller) {\n if (options.includeRawChunks) {\n controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });\n }\n\n // handle failed chunk parsing / validation:\n if (!chunk.success) {\n finishReason = { unified: 'error', raw: undefined };\n controller.enqueue({ type: 'error', error: chunk.error });\n return;\n }\n const value: unknown = chunk.value;\n\n metadataExtractor?.processChunk(chunk.rawValue);\n\n // hosted tool execution case\n if (isHostedToolExecutionChunk(value)) {\n const toolCallId = value.tool_call_id ?? generateId();\n switch (value.status) {\n case 'STARTED':\n controller.enqueue({\n type: 'tool-call',\n toolCallId,\n toolName: value.name,\n input: JSON.stringify(\n Object.fromEntries(value.parameters.map((p) => [p.name, p.value]))\n ),\n providerExecuted: true,\n });\n break;\n\n case 'UPDATING':\n // Optionally handle progress if needed, but LanguageModelV3StreamPart doesn't have a direct \"progress\" type for tools\n break;\n\n case 'ENDED':\n controller.enqueue({\n type: 'tool-result',\n toolCallId,\n toolName: value.name,\n result: value.result ?? '',\n });\n break;\n\n case 'ERRORED':\n finishReason = { unified: 'error', raw: undefined };\n controller.enqueue({\n type: 'tool-result',\n toolCallId,\n toolName: value.name,\n result: value.error?.msg ?? 'Unknown error',\n isError: true,\n });\n break;\n\n default:\n finishReason = { unified: 'error', raw: undefined };\n controller.enqueue({\n type: 'error',\n error: new Error(`Unsupported tool call status: ${value.status}`),\n });\n }\n return;\n }\n\n const chunkErrorMessage = getChunkErrorMessage(value);\n if (chunkErrorMessage != null) {\n finishReason = { unified: 'error', raw: undefined };\n controller.enqueue({ type: 'error', error: chunkErrorMessage });\n return;\n }\n\n if (!isOpenAIChatChunk(value)) {\n finishReason = { unified: 'error', raw: undefined };\n controller.enqueue({\n type: 'error',\n error: new Error('Unsupported chunk shape'),\n });\n return;\n }\n\n const chunkValue = value;\n\n if (isFirstChunk) {\n isFirstChunk = false;\n\n controller.enqueue({\n type: 'response-metadata',\n ...getResponseMetadata(chunkValue),\n });\n }\n\n if (chunkValue.usage != null) {\n usage = chunkValue.usage;\n }\n\n const choice = chunkValue.choices[0];\n\n if (choice?.finish_reason != null) {\n finishReason = {\n unified: mapOpenAICompatibleFinishReason(choice.finish_reason),\n raw: choice.finish_reason,\n };\n }\n\n if (choice?.delta == null) {\n return;\n }\n\n const delta = choice.delta;\n\n // enqueue reasoning before text deltas:\n if (delta.reasoning_content != null) {\n if (currentReasoningId == null) {\n currentReasoningId = generateId();\n // Enqueue reasoning-start event for the first reasoning delta\n controller.enqueue({\n type: 'reasoning-start',\n id: currentReasoningId,\n });\n }\n controller.enqueue({\n type: 'reasoning-delta',\n id: currentReasoningId,\n delta: delta.reasoning_content,\n });\n }\n\n if (delta.content != null) {\n if (currentTextId == null) {\n currentTextId = generateId();\n // Enqueue text-start event for the first text delta\n controller.enqueue({\n type: 'text-start',\n id: currentTextId,\n });\n }\n controller.enqueue({\n type: 'text-delta',\n id: currentTextId,\n delta: delta.content,\n });\n }\n\n if (delta.tool_calls != null) {\n for (const toolCallDelta of delta.tool_calls) {\n const index = toolCallDelta.index;\n // Tool call start. FriendliAI returns all information except the arguments in the first chunk.\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 controller.enqueue({\n type: 'tool-input-start',\n id: toolCallDelta.id,\n toolName: toolCallDelta.function.name,\n });\n\n const toolCall = toolCalls[index];\n\n if (toolCall.function?.name != null && toolCall.function?.arguments != null) {\n // send delta if the argument text has already started:\n if (toolCall.function.arguments.length > 0) {\n controller.enqueue({\n type: 'tool-input-delta',\n id: toolCall.id,\n delta: toolCall.function.arguments,\n });\n }\n\n // check if tool call is complete\n // (some providers send the full tool call in one chunk):\n if (isParsableJson(toolCall.function.arguments)) {\n controller.enqueue({\n type: 'tool-input-end',\n id: toolCall.id,\n });\n\n controller.enqueue({\n type: 'tool-call',\n toolCallId: toolCall.id ?? generateId(),\n toolName: toolCall.function.name,\n input: toolCall.function.arguments,\n });\n toolCall.hasFinished = true;\n }\n }\n\n continue;\n }\n\n // existing tool call, merge if not finished\n const toolCall = toolCalls[index];\n\n if (toolCall.hasFinished) {\n continue;\n }\n\n if (toolCallDelta.function?.arguments != null) {\n toolCall.function!.arguments += toolCallDelta.function?.arguments ?? '';\n }\n\n // send delta\n controller.enqueue({\n type: 'tool-input-delta',\n id: toolCall.id,\n delta: toolCallDelta.function?.arguments ?? '',\n });\n\n // check if tool call is complete\n if (\n toolCall.function?.name != null &&\n toolCall.function?.arguments != null &&\n isParsableJson(toolCall.function.arguments)\n ) {\n controller.enqueue({\n type: 'tool-input-end',\n id: toolCall.id,\n });\n\n controller.enqueue({\n type: 'tool-call',\n toolCallId: toolCall.id ?? generateId(),\n toolName: toolCall.function.name,\n input: toolCall.function.arguments,\n });\n toolCall.hasFinished = true;\n }\n }\n }\n },\n\n flush(controller) {\n // Emit end events before finish\n if (currentReasoningId != null) {\n controller.enqueue({\n type: 'reasoning-end',\n id: currentReasoningId,\n });\n }\n\n if (currentTextId != null) {\n controller.enqueue({\n type: 'text-end',\n id: currentTextId,\n });\n }\n\n for (const toolCall of toolCalls.filter(\n (pendingToolCall) => !pendingToolCall.hasFinished\n )) {\n controller.enqueue({\n type: 'tool-input-end',\n id: toolCall.id,\n });\n\n controller.enqueue({\n type: 'tool-call',\n toolCallId: toolCall.id ?? generateId(),\n toolName: toolCall.function.name,\n input: toolCall.function.arguments,\n });\n }\n\n const providerMetadata: SharedV3ProviderMetadata = {\n [providerOptionsName]: {},\n ...metadataExtractor?.buildMetadata(),\n };\n if (usage?.completion_tokens_details?.accepted_prediction_tokens != null) {\n providerMetadata[providerOptionsName].acceptedPredictionTokens =\n usage.completion_tokens_details.accepted_prediction_tokens;\n }\n if (usage?.completion_tokens_details?.rejected_prediction_tokens != null) {\n providerMetadata[providerOptionsName].rejectedPredictionTokens =\n usage.completion_tokens_details.rejected_prediction_tokens;\n }\n\n controller.enqueue({\n type: 'finish',\n finishReason,\n usage: convertOpenAICompatibleChatUsage(usage),\n providerMetadata,\n });\n },\n })\n ),\n request: { body },\n response: { headers: responseHeaders },\n };\n }\n}\n\nconst openaiCompatibleTokenUsageSchema = z\n .object({\n prompt_tokens: z.number().nullish(),\n completion_tokens: z.number().nullish(),\n total_tokens: z.number().nullish(),\n prompt_tokens_details: z\n .object({\n cached_tokens: z.number().nullish(),\n })\n .nullish(),\n completion_tokens_details: z\n .object({\n reasoning_tokens: z.number().nullish(),\n accepted_prediction_tokens: z.number().nullish(),\n rejected_prediction_tokens: z.number().nullish(),\n })\n .nullish(),\n })\n .nullish();\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 OpenAICompatibleChatResponseSchema = 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: openaiCompatibleTokenUsageSchema,\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 createOpenAICompatibleChatChunkSchema = <ERROR_SCHEMA extends z.ZodType>(\n errorSchema: ERROR_SCHEMA\n) =>\n 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').nullish(),\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: openaiCompatibleTokenUsageSchema,\n }),\n z.object({\n name: z.string(),\n status: z.enum(['ENDED', 'STARTED', 'ERRORED', 'UPDATING']),\n message: z.null(),\n parameters: z.array(\n z.object({\n name: z.string(),\n value: z.string(),\n })\n ),\n result: z.string().nullable(),\n error: z\n .object({\n type: z.enum(['INVALID_PARAMETER', 'UNKNOWN']),\n msg: z.string(),\n })\n .nullable(),\n timestamp: z.number(),\n usage: z.null(),\n tool_call_id: z.string().nullable(),\n }),\n errorSchema,\n ]);\n\nconst friendliProviderOptionsSchema = z.object({\n /**\n * Whether to enable parallel function calling during tool use. Default to true.\n */\n parallelToolCalls: z.boolean().nullish(),\n\n /**\n * BETA FEATURE: You can write a regular expression to force output that satisfies that regular expression.\n */\n // regex: z.instanceof(RegExp).nullish(),\n regex: z.string().nullish(),\n\n chat_template_kwargs: z.record(z.string(), z.any()).nullish(),\n\n /**\n * A scaling factor used to determine the minimum token probability threshold.\n */\n minP: z.number().nullish(),\n\n /**\n * Penalizes tokens that have already appeared in the generated result.\n */\n repetitionPenalty: z.number().nullish(),\n\n /**\n * A probability threshold used to identify “top choice” tokens for exclusion in XTC sampling.\n */\n xtcThreshold: z.number().nullish(),\n\n /**\n * The probability that XTC (Exclude Top Choices) filtering will be applied for each sampling decision.\n */\n xtcProbability: z.number().nullish(),\n});\n\nexport type FriendliProviderOptions = z.infer<typeof friendliProviderOptionsSchema>;\n","import { ProviderErrorStructure } from '@ai-sdk/openai-compatible';\nimport { APICallError } from '@ai-sdk/provider';\nimport { type ResponseHandler, safeParseJSON } from '@ai-sdk/provider-utils';\nimport { z } from 'zod';\n\nconst friendliErrorResponseSchema = z.object({\n message: z.string(),\n error: z.record(z.string(), z.any()).optional(),\n});\n\nconst openAIStyleErrorResponseSchema = z\n .object({\n error: z\n .object({\n message: z.string(),\n })\n .loose(),\n })\n .loose();\n\nexport const friendliaiErrorSchema = z.union([\n // OpenAI/OpenRouter style error: { \"error\": { \"message\": \"...\" } }\n openAIStyleErrorResponseSchema,\n // Friendli style error: { \"message\": \"...\", \"error\": { ... } }\n friendliErrorResponseSchema,\n]);\n\nexport type FriendliAIErrorData = z.infer<typeof friendliaiErrorSchema>;\n\nexport const friendliaiErrorStructure: ProviderErrorStructure<FriendliAIErrorData> = {\n errorSchema: friendliaiErrorSchema,\n errorToMessage: (data) => {\n if (\n typeof data === 'object' &&\n data != null &&\n 'error' in data &&\n typeof data.error === 'object' &&\n data.error != null &&\n 'message' in data.error &&\n typeof data.error.message === 'string'\n ) {\n return data.error.message;\n }\n\n if (\n typeof data === 'object' &&\n data != null &&\n 'message' in data &&\n typeof data.message === 'string'\n ) {\n return data.message;\n }\n\n return 'Unknown error';\n },\n};\n\nexport const friendliaiFailedResponseHandler: ResponseHandler<APICallError> = async ({\n response,\n url,\n requestBodyValues,\n}) => {\n const responseBody = await response.text();\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n const baseErrorOptions = {\n url,\n requestBodyValues,\n statusCode: response.status,\n responseHeaders,\n responseBody,\n } as const;\n\n const trimmedBody = responseBody.trim();\n\n if (trimmedBody === '') {\n const fallback = response.statusText || `Request failed with status ${response.status}`;\n return {\n responseHeaders,\n value: new APICallError({\n message: fallback,\n ...baseErrorOptions,\n }),\n };\n }\n\n const parsedError = await safeParseJSON({\n text: responseBody,\n schema: friendliaiErrorSchema,\n });\n\n if (parsedError.success) {\n return {\n responseHeaders,\n value: new APICallError({\n message: friendliaiErrorStructure.errorToMessage(parsedError.value),\n data: parsedError.value,\n ...baseErrorOptions,\n }),\n };\n }\n\n const fallback =\n trimmedBody || response.statusText || `Request failed with status ${response.status}`;\n\n return {\n responseHeaders,\n value: new APICallError({\n message: fallback,\n cause: parsedError.error,\n ...baseErrorOptions,\n }),\n };\n};\n\nexport const tryWrapFriendliJsonEnvelopeError = async (\n error: unknown\n): Promise<APICallError | undefined> => {\n if (!APICallError.isInstance(error)) {\n return undefined;\n }\n\n const responseBody = error.responseBody;\n\n if (typeof responseBody !== 'string' || responseBody.trim() === '') {\n return undefined;\n }\n\n const parsedError = await safeParseJSON({\n text: responseBody,\n schema: friendliaiErrorSchema,\n });\n\n if (!parsedError.success) {\n return undefined;\n }\n\n return new APICallError({\n message: friendliaiErrorStructure.errorToMessage(parsedError.value),\n url: error.url,\n requestBodyValues: error.requestBodyValues,\n statusCode: error.statusCode,\n responseHeaders: error.responseHeaders,\n responseBody: error.responseBody,\n cause: error,\n isRetryable: error.isRetryable,\n data: parsedError.value,\n });\n};\n","import {\n LanguageModelV3CallOptions,\n SharedV3Warning,\n UnsupportedFunctionalityError,\n} from '@ai-sdk/provider';\n\nexport function prepareTools({\n tools,\n toolChoice,\n}: {\n tools: LanguageModelV3CallOptions['tools'];\n toolChoice?: LanguageModelV3CallOptions['toolChoice'];\n}): {\n tools:\n | undefined\n | Array<{\n type: string;\n files?: string[];\n }>\n | Array<{\n type: 'function';\n function: {\n name: string;\n description: string | undefined;\n parameters: unknown;\n };\n }>;\n toolChoice:\n | { type: 'function'; function: { name: string } }\n | 'auto'\n | 'none'\n | 'required'\n | undefined;\n toolWarnings: SharedV3Warning[];\n} {\n // when the tools array is empty, change it to undefined to prevent errors:\n tools = tools?.length ? tools : undefined;\n\n const toolWarnings: SharedV3Warning[] = [];\n\n if (tools == null) {\n return { tools: undefined, toolChoice: undefined, toolWarnings };\n }\n\n const openaiCompatTools: Array<\n | {\n type: 'function';\n function: {\n name: string;\n description: string | undefined;\n parameters: unknown;\n };\n }\n | {\n type: string;\n }\n > = [];\n\n for (const tool of tools) {\n if (tool.type === 'provider') {\n openaiCompatTools.push({\n // NOTE: Friendli tool-assisted API expects provider tool types like \"web:search\".\n // We derive it from the provider tool id (e.g. \"friendli.web:search\" -> \"web:search\")\n // instead of tool.name (often \"web_search\").\n type: tool.id.split('.')[1] ?? 'unknown',\n });\n } else {\n openaiCompatTools.push({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.inputSchema,\n },\n });\n }\n }\n\n if (toolChoice == null) {\n return { tools: openaiCompatTools, toolChoice: undefined, toolWarnings };\n }\n\n const type = toolChoice.type;\n\n switch (type) {\n case 'auto':\n case 'none':\n case 'required':\n return { tools: openaiCompatTools, toolChoice: type, toolWarnings };\n case 'tool':\n return {\n tools: openaiCompatTools,\n toolChoice: {\n type: 'function',\n function: { name: toolChoice.toolName },\n },\n toolWarnings,\n };\n default: {\n const _exhaustiveCheck: never = type;\n throw new UnsupportedFunctionalityError({\n functionality: `tool choice type: ${_exhaustiveCheck}`,\n });\n }\n }\n}\n","// https://friendli.ai/product/model-apis\n// Below is just a subset of the available models.\nexport const FriendliAIServerlessModelIds = [\n 'google/gemma-4-31B-it',\n 'zai-org/GLM-5.1',\n 'zai-org/GLM-5',\n 'meta-llama/Llama-3.3-70B-Instruct',\n 'meta-llama-3.3-70b-instruct',\n 'meta-llama/Llama-3.1-8B-Instruct',\n 'meta-llama-3.1-8b-instruct',\n 'Qwen/Qwen3-235B-A22B-Instruct-2507',\n 'deepseek-ai/DeepSeek-V3.2',\n 'openai/whisper-large-v3',\n 'MiniMaxAI/MiniMax-M2.5',\n 'LGAI-EXAONE/K-EXAONE-236B-A23B',\n] as const;\n\nexport type FriendliAIServerlessModelId = (typeof FriendliAIServerlessModelIds)[number];\n\nexport type FriendliAILanguageModelId = FriendliAIServerlessModelId | (string & {});\n","import { createProviderToolFactoryWithOutputSchema, type Tool } from '@ai-sdk/provider-utils';\nimport { z } from 'zod';\n\n/**\n * Friendli built-in tools for serverless tool-assisted API.\n *\n * @remarks\n * These tools are currently in **Beta**. While we strive to provide a stable\n * and reliable experience, this feature is still under active development.\n *\n * @see https://friendli.ai/docs/guides/model-apis/tool-assisted-api\n */\n\nconst inputSchema = z.object({}).loose();\nconst outputSchema = z.unknown();\n\nexport const webSearchTool = createProviderToolFactoryWithOutputSchema({\n id: 'friendli.web:search',\n inputSchema,\n outputSchema,\n});\n\nexport const webUrlTool = createProviderToolFactoryWithOutputSchema({\n id: 'friendli.web:url',\n inputSchema,\n outputSchema,\n});\n\nexport const mathCalendarTool = createProviderToolFactoryWithOutputSchema({\n id: 'friendli.math:calendar',\n inputSchema,\n outputSchema,\n});\n\nexport const mathStatisticsTool = createProviderToolFactoryWithOutputSchema({\n id: 'friendli.math:statistics',\n inputSchema,\n outputSchema,\n});\n\nexport const mathCalculatorTool = createProviderToolFactoryWithOutputSchema({\n id: 'friendli.math:calculator',\n inputSchema,\n outputSchema,\n});\n\nexport const codePythonInterpreterTool = createProviderToolFactoryWithOutputSchema({\n id: 'friendli.code:python-interpreter',\n inputSchema,\n outputSchema,\n});\n\nexport const linkupSearchTool = createProviderToolFactoryWithOutputSchema({\n id: 'friendli.linkup:search',\n inputSchema,\n outputSchema,\n});\n\n/**\n * Web search tool - searches the web for information.\n * @beta\n */\nfunction webSearch(): Tool {\n return webSearchTool({});\n}\n\n/**\n * Web URL tool - fetches content from a specific URL.\n * @beta\n */\nfunction webUrl(): Tool {\n return webUrlTool({});\n}\n\n/**\n * Math calendar tool - performs calendar-related calculations.\n * @beta\n */\nfunction mathCalendar(): Tool {\n return mathCalendarTool({});\n}\n\n/**\n * Math statistics tool - performs statistical calculations.\n * @beta\n */\nfunction mathStatistics(): Tool {\n return mathStatisticsTool({});\n}\n\n/**\n * Math calculator tool - performs arithmetic calculations.\n * @beta\n */\nfunction mathCalculator(): Tool {\n return mathCalculatorTool({});\n}\n\n/**\n * Python interpreter tool - executes Python code.\n * @beta\n */\nfunction codePythonInterpreter(): Tool {\n return codePythonInterpreterTool({});\n}\n\n/**\n * Linkup search tool - searches the web for real-time information with citations.\n * @see https://www.linkup.so\n */\nfunction linkupSearch(): Tool {\n return linkupSearchTool({});\n}\n\nexport const friendliTools = {\n webSearch,\n webUrl,\n mathCalendar,\n mathStatistics,\n mathCalculator,\n codePythonInterpreter,\n linkupSearch,\n};\n","import { loadApiKey } from '@ai-sdk/provider-utils';\n\ntype Pricing = {\n inputToken?: number;\n cachedInputToken?: number;\n outputToken?: number;\n responseTime?: number;\n audioMinute?: number;\n unitType?: 'TOKEN' | 'SECOND' | 'AUDIO_MINUTE';\n currency?: string;\n unit?: string;\n};\n\nexport type FriendliAvailableModel = {\n id: string;\n name?: string | null;\n description?: string | null;\n pricing?: Pricing;\n warm?: boolean;\n col