| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815 |
- import {
- APICallError,
- InvalidResponseDataError,
- type LanguageModelV3,
- type LanguageModelV3CallOptions,
- type LanguageModelV3Content,
- type LanguageModelV3StreamPart,
- type SharedV3ProviderMetadata,
- type SharedV3Warning,
- } from "@ai-sdk/provider"
- import {
- combineHeaders,
- createEventSourceResponseHandler,
- createJsonErrorResponseHandler,
- createJsonResponseHandler,
- type FetchFunction,
- generateId,
- isParsableJson,
- parseProviderOptions,
- type ParseResult,
- postJsonToApi,
- type ResponseHandler,
- } from "@ai-sdk/provider-utils"
- import { z } from "zod/v4"
- import { convertToOpenAICompatibleChatMessages } from "./convert-to-openai-compatible-chat-messages"
- import { getResponseMetadata } from "./get-response-metadata"
- import { mapOpenAICompatibleFinishReason } from "./map-openai-compatible-finish-reason"
- import { type OpenAICompatibleChatModelId, openaiCompatibleProviderOptions } from "./openai-compatible-chat-options"
- import { defaultOpenAICompatibleErrorStructure, type ProviderErrorStructure } from "../openai-compatible-error"
- import type { MetadataExtractor } from "./openai-compatible-metadata-extractor"
- import { prepareTools } from "./openai-compatible-prepare-tools"
- export type OpenAICompatibleChatConfig = {
- provider: string
- headers: () => Record<string, string | undefined>
- url: (options: { modelId: string; path: string }) => string
- fetch?: FetchFunction
- includeUsage?: boolean
- errorStructure?: ProviderErrorStructure<any>
- metadataExtractor?: MetadataExtractor
- /**
- * Whether the model supports structured outputs.
- */
- supportsStructuredOutputs?: boolean
- /**
- * The supported URLs for the model.
- */
- supportedUrls?: () => LanguageModelV3["supportedUrls"]
- }
- export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
- readonly specificationVersion = "v3"
- readonly supportsStructuredOutputs: boolean
- readonly modelId: OpenAICompatibleChatModelId
- private readonly config: OpenAICompatibleChatConfig
- private readonly failedResponseHandler: ResponseHandler<APICallError>
- private readonly chunkSchema // type inferred via constructor
- constructor(modelId: OpenAICompatibleChatModelId, config: OpenAICompatibleChatConfig) {
- this.modelId = modelId
- this.config = config
- // initialize error handling:
- const errorStructure = config.errorStructure ?? defaultOpenAICompatibleErrorStructure
- this.chunkSchema = createOpenAICompatibleChatChunkSchema(errorStructure.errorSchema)
- this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure)
- this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false
- }
- get provider(): string {
- return this.config.provider
- }
- private get providerOptionsName(): string {
- return this.config.provider.split(".")[0].trim()
- }
- get supportedUrls() {
- return this.config.supportedUrls?.() ?? {}
- }
- private async getArgs({
- prompt,
- maxOutputTokens,
- temperature,
- topP,
- topK,
- frequencyPenalty,
- presencePenalty,
- providerOptions,
- stopSequences,
- responseFormat,
- seed,
- toolChoice,
- tools,
- }: LanguageModelV3CallOptions) {
- const warnings: SharedV3Warning[] = []
- // Parse provider options
- const compatibleOptions = Object.assign(
- (await parseProviderOptions({
- provider: "copilot",
- providerOptions,
- schema: openaiCompatibleProviderOptions,
- })) ?? {},
- (await parseProviderOptions({
- provider: this.providerOptionsName,
- providerOptions,
- schema: openaiCompatibleProviderOptions,
- })) ?? {},
- )
- if (topK != null) {
- warnings.push({ type: "unsupported", feature: "topK" })
- }
- if (responseFormat?.type === "json" && responseFormat.schema != null && !this.supportsStructuredOutputs) {
- warnings.push({
- type: "unsupported",
- feature: "responseFormat",
- details: "JSON response format schema is only supported with structuredOutputs",
- })
- }
- const {
- tools: openaiTools,
- toolChoice: openaiToolChoice,
- toolWarnings,
- } = prepareTools({
- tools,
- toolChoice,
- })
- return {
- args: {
- // model id:
- model: this.modelId,
- // model specific settings:
- user: compatibleOptions.user,
- // standardized settings:
- max_tokens: maxOutputTokens,
- temperature,
- top_p: topP,
- frequency_penalty: frequencyPenalty,
- presence_penalty: presencePenalty,
- response_format:
- responseFormat?.type === "json"
- ? this.supportsStructuredOutputs === true && responseFormat.schema != null
- ? {
- type: "json_schema",
- json_schema: {
- schema: responseFormat.schema,
- name: responseFormat.name ?? "response",
- description: responseFormat.description,
- },
- }
- : { type: "json_object" }
- : undefined,
- stop: stopSequences,
- seed,
- ...Object.fromEntries(
- Object.entries(providerOptions?.[this.providerOptionsName] ?? {}).filter(
- ([key]) => !Object.keys(openaiCompatibleProviderOptions.shape).includes(key),
- ),
- ),
- reasoning_effort: compatibleOptions.reasoningEffort,
- verbosity: compatibleOptions.textVerbosity,
- // messages:
- messages: convertToOpenAICompatibleChatMessages(prompt),
- // tools:
- tools: openaiTools,
- tool_choice: openaiToolChoice,
- // thinking_budget
- thinking_budget: compatibleOptions.thinking_budget,
- },
- warnings: [...warnings, ...toolWarnings],
- }
- }
- async doGenerate(options: LanguageModelV3CallOptions) {
- const { args, warnings } = await this.getArgs({ ...options })
- const body = JSON.stringify(args)
- const {
- responseHeaders,
- value: responseBody,
- rawValue: rawResponse,
- } = await postJsonToApi({
- url: this.config.url({
- path: "/chat/completions",
- modelId: this.modelId,
- }),
- headers: combineHeaders(this.config.headers(), options.headers),
- body: args,
- failedResponseHandler: this.failedResponseHandler,
- successfulResponseHandler: createJsonResponseHandler(OpenAICompatibleChatResponseSchema),
- abortSignal: options.abortSignal,
- fetch: this.config.fetch,
- })
- const choice = responseBody.choices[0]
- const content: Array<LanguageModelV3Content> = []
- // text content:
- const text = choice.message.content
- if (text != null && text.length > 0) {
- content.push({
- type: "text",
- text,
- providerMetadata: choice.message.reasoning_opaque
- ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
- : undefined,
- })
- }
- // reasoning content (Copilot uses reasoning_text):
- const reasoning = choice.message.reasoning_text
- if (reasoning != null && reasoning.length > 0) {
- content.push({
- type: "reasoning",
- text: reasoning,
- // Include reasoning_opaque for Copilot multi-turn reasoning
- providerMetadata: choice.message.reasoning_opaque
- ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
- : undefined,
- })
- }
- // tool calls:
- if (choice.message.tool_calls != null) {
- for (const toolCall of choice.message.tool_calls) {
- content.push({
- type: "tool-call",
- toolCallId: toolCall.id ?? generateId(),
- toolName: toolCall.function.name,
- input: toolCall.function.arguments!,
- providerMetadata: choice.message.reasoning_opaque
- ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
- : undefined,
- })
- }
- }
- // provider metadata:
- const providerMetadata: SharedV3ProviderMetadata = {
- [this.providerOptionsName]: {},
- ...(await this.config.metadataExtractor?.extractMetadata?.({
- parsedBody: rawResponse,
- })),
- }
- const completionTokenDetails = responseBody.usage?.completion_tokens_details
- if (completionTokenDetails?.accepted_prediction_tokens != null) {
- providerMetadata[this.providerOptionsName].acceptedPredictionTokens =
- completionTokenDetails?.accepted_prediction_tokens
- }
- if (completionTokenDetails?.rejected_prediction_tokens != null) {
- providerMetadata[this.providerOptionsName].rejectedPredictionTokens =
- completionTokenDetails?.rejected_prediction_tokens
- }
- return {
- content,
- finishReason: {
- unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
- raw: choice.finish_reason ?? undefined,
- },
- usage: {
- inputTokens: {
- total: responseBody.usage?.prompt_tokens ?? undefined,
- noCache: undefined,
- cacheRead: responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined,
- cacheWrite: undefined,
- },
- outputTokens: {
- total: responseBody.usage?.completion_tokens ?? undefined,
- text: undefined,
- reasoning: responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? undefined,
- },
- raw: responseBody.usage ?? undefined,
- },
- providerMetadata,
- request: { body },
- response: {
- ...getResponseMetadata(responseBody),
- headers: responseHeaders,
- body: rawResponse,
- },
- warnings,
- }
- }
- async doStream(options: LanguageModelV3CallOptions) {
- const { args, warnings } = await this.getArgs({ ...options })
- const body = {
- ...args,
- stream: true,
- // only include stream_options when in strict compatibility mode:
- stream_options: this.config.includeUsage ? { include_usage: true } : undefined,
- }
- const metadataExtractor = this.config.metadataExtractor?.createStreamExtractor()
- const { responseHeaders, value: response } = await postJsonToApi({
- url: this.config.url({
- path: "/chat/completions",
- modelId: this.modelId,
- }),
- headers: combineHeaders(this.config.headers(), options.headers),
- body,
- failedResponseHandler: this.failedResponseHandler,
- successfulResponseHandler: createEventSourceResponseHandler(this.chunkSchema),
- abortSignal: options.abortSignal,
- fetch: this.config.fetch,
- })
- const toolCalls: Array<{
- id: string
- type: "function"
- function: {
- name: string
- arguments: string
- }
- hasFinished: boolean
- }> = []
- let finishReason: {
- unified: ReturnType<typeof mapOpenAICompatibleFinishReason>
- raw: string | undefined
- } = {
- unified: "other",
- raw: undefined,
- }
- const usage: {
- completionTokens: number | undefined
- completionTokensDetails: {
- reasoningTokens: number | undefined
- acceptedPredictionTokens: number | undefined
- rejectedPredictionTokens: number | undefined
- }
- promptTokens: number | undefined
- promptTokensDetails: {
- cachedTokens: number | undefined
- }
- totalTokens: number | undefined
- } = {
- completionTokens: undefined,
- completionTokensDetails: {
- reasoningTokens: undefined,
- acceptedPredictionTokens: undefined,
- rejectedPredictionTokens: undefined,
- },
- promptTokens: undefined,
- promptTokensDetails: {
- cachedTokens: undefined,
- },
- totalTokens: undefined,
- }
- let isFirstChunk = true
- const providerOptionsName = this.providerOptionsName
- let isActiveReasoning = false
- let isActiveText = false
- let reasoningOpaque: string | undefined
- return {
- stream: response.pipeThrough(
- new TransformStream<ParseResult<z.infer<typeof this.chunkSchema>>, LanguageModelV3StreamPart>({
- start(controller) {
- controller.enqueue({ type: "stream-start", warnings })
- },
- // TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX
- transform(chunk, controller) {
- // Emit raw chunk if requested (before anything else)
- if (options.includeRawChunks) {
- controller.enqueue({ type: "raw", rawValue: chunk.rawValue })
- }
- // handle failed chunk parsing / validation:
- if (!chunk.success) {
- finishReason = {
- unified: "error",
- raw: undefined,
- }
- controller.enqueue({ type: "error", error: chunk.error })
- return
- }
- const value = chunk.value
- metadataExtractor?.processChunk(chunk.rawValue)
- // handle error chunks:
- if ("error" in value) {
- finishReason = {
- unified: "error",
- raw: undefined,
- }
- controller.enqueue({ type: "error", error: value.error.message })
- return
- }
- if (isFirstChunk) {
- isFirstChunk = false
- controller.enqueue({
- type: "response-metadata",
- ...getResponseMetadata(value),
- })
- }
- if (value.usage != null) {
- const {
- prompt_tokens,
- completion_tokens,
- total_tokens,
- prompt_tokens_details,
- completion_tokens_details,
- } = value.usage
- usage.promptTokens = prompt_tokens ?? undefined
- usage.completionTokens = completion_tokens ?? undefined
- usage.totalTokens = total_tokens ?? undefined
- if (completion_tokens_details?.reasoning_tokens != null) {
- usage.completionTokensDetails.reasoningTokens = completion_tokens_details?.reasoning_tokens
- }
- if (completion_tokens_details?.accepted_prediction_tokens != null) {
- usage.completionTokensDetails.acceptedPredictionTokens =
- completion_tokens_details?.accepted_prediction_tokens
- }
- if (completion_tokens_details?.rejected_prediction_tokens != null) {
- usage.completionTokensDetails.rejectedPredictionTokens =
- completion_tokens_details?.rejected_prediction_tokens
- }
- if (prompt_tokens_details?.cached_tokens != null) {
- usage.promptTokensDetails.cachedTokens = prompt_tokens_details?.cached_tokens
- }
- }
- const choice = value.choices[0]
- if (choice?.finish_reason != null) {
- finishReason = {
- unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
- raw: choice.finish_reason ?? undefined,
- }
- }
- if (choice?.delta == null) {
- return
- }
- const delta = choice.delta
- // Capture reasoning_opaque for Copilot multi-turn reasoning
- if (delta.reasoning_opaque) {
- if (reasoningOpaque != null) {
- throw new InvalidResponseDataError({
- data: delta,
- message:
- "Multiple reasoning_opaque values received in a single response. Only one thinking part per response is supported.",
- })
- }
- reasoningOpaque = delta.reasoning_opaque
- }
- // enqueue reasoning before text deltas (Copilot uses reasoning_text):
- const reasoningContent = delta.reasoning_text
- if (reasoningContent) {
- if (!isActiveReasoning) {
- controller.enqueue({
- type: "reasoning-start",
- id: "reasoning-0",
- })
- isActiveReasoning = true
- }
- controller.enqueue({
- type: "reasoning-delta",
- id: "reasoning-0",
- delta: reasoningContent,
- })
- }
- if (delta.content) {
- // If reasoning was active and we're starting text, end reasoning first
- // This handles the case where reasoning_opaque and content come in the same chunk
- if (isActiveReasoning && !isActiveText) {
- controller.enqueue({
- type: "reasoning-end",
- id: "reasoning-0",
- providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
- })
- isActiveReasoning = false
- }
- if (!isActiveText) {
- controller.enqueue({
- type: "text-start",
- id: "txt-0",
- providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
- })
- isActiveText = true
- }
- controller.enqueue({
- type: "text-delta",
- id: "txt-0",
- delta: delta.content,
- })
- }
- if (delta.tool_calls != null) {
- // If reasoning was active and we're starting tool calls, end reasoning first
- // This handles the case where reasoning goes directly to tool calls with no content
- if (isActiveReasoning) {
- controller.enqueue({
- type: "reasoning-end",
- id: "reasoning-0",
- providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
- })
- isActiveReasoning = false
- }
- for (const toolCallDelta of delta.tool_calls) {
- const index = toolCallDelta.index
- if (toolCalls[index] == null) {
- if (toolCallDelta.id == null) {
- throw new InvalidResponseDataError({
- data: toolCallDelta,
- message: `Expected 'id' to be a string.`,
- })
- }
- if (toolCallDelta.function?.name == null) {
- throw new InvalidResponseDataError({
- data: toolCallDelta,
- message: `Expected 'function.name' to be a string.`,
- })
- }
- controller.enqueue({
- type: "tool-input-start",
- id: toolCallDelta.id,
- toolName: toolCallDelta.function.name,
- })
- toolCalls[index] = {
- id: toolCallDelta.id,
- type: "function",
- function: {
- name: toolCallDelta.function.name,
- arguments: toolCallDelta.function.arguments ?? "",
- },
- hasFinished: false,
- }
- const toolCall = toolCalls[index]
- if (toolCall.function?.name != null && toolCall.function?.arguments != null) {
- // send delta if the argument text has already started:
- if (toolCall.function.arguments.length > 0) {
- controller.enqueue({
- type: "tool-input-delta",
- id: toolCall.id,
- delta: toolCall.function.arguments,
- })
- }
- // check if tool call is complete
- // (some providers send the full tool call in one chunk):
- if (isParsableJson(toolCall.function.arguments)) {
- controller.enqueue({
- type: "tool-input-end",
- id: toolCall.id,
- })
- controller.enqueue({
- type: "tool-call",
- toolCallId: toolCall.id ?? generateId(),
- toolName: toolCall.function.name,
- input: toolCall.function.arguments,
- providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
- })
- toolCall.hasFinished = true
- }
- }
- continue
- }
- // existing tool call, merge if not finished
- const toolCall = toolCalls[index]
- if (toolCall.hasFinished) {
- continue
- }
- if (toolCallDelta.function?.arguments != null) {
- toolCall.function!.arguments += toolCallDelta.function?.arguments ?? ""
- }
- // send delta
- controller.enqueue({
- type: "tool-input-delta",
- id: toolCall.id,
- delta: toolCallDelta.function.arguments ?? "",
- })
- // check if tool call is complete
- if (
- toolCall.function?.name != null &&
- toolCall.function?.arguments != null &&
- isParsableJson(toolCall.function.arguments)
- ) {
- controller.enqueue({
- type: "tool-input-end",
- id: toolCall.id,
- })
- controller.enqueue({
- type: "tool-call",
- toolCallId: toolCall.id ?? generateId(),
- toolName: toolCall.function.name,
- input: toolCall.function.arguments,
- providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
- })
- toolCall.hasFinished = true
- }
- }
- }
- },
- flush(controller) {
- if (isActiveReasoning) {
- controller.enqueue({
- type: "reasoning-end",
- id: "reasoning-0",
- // Include reasoning_opaque for Copilot multi-turn reasoning
- providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
- })
- }
- if (isActiveText) {
- controller.enqueue({ type: "text-end", id: "txt-0" })
- }
- // go through all tool calls and send the ones that are not finished
- for (const toolCall of toolCalls.filter((toolCall) => !toolCall.hasFinished)) {
- controller.enqueue({
- type: "tool-input-end",
- id: toolCall.id,
- })
- controller.enqueue({
- type: "tool-call",
- toolCallId: toolCall.id ?? generateId(),
- toolName: toolCall.function.name,
- input: toolCall.function.arguments,
- })
- }
- const providerMetadata: SharedV3ProviderMetadata = {
- [providerOptionsName]: {},
- // Include reasoning_opaque for Copilot multi-turn reasoning
- ...(reasoningOpaque ? { copilot: { reasoningOpaque } } : {}),
- ...metadataExtractor?.buildMetadata(),
- }
- if (usage.completionTokensDetails.acceptedPredictionTokens != null) {
- providerMetadata[providerOptionsName].acceptedPredictionTokens =
- usage.completionTokensDetails.acceptedPredictionTokens
- }
- if (usage.completionTokensDetails.rejectedPredictionTokens != null) {
- providerMetadata[providerOptionsName].rejectedPredictionTokens =
- usage.completionTokensDetails.rejectedPredictionTokens
- }
- controller.enqueue({
- type: "finish",
- finishReason,
- usage: {
- inputTokens: {
- total: usage.promptTokens,
- noCache:
- usage.promptTokens != undefined && usage.promptTokensDetails.cachedTokens != undefined
- ? usage.promptTokens - usage.promptTokensDetails.cachedTokens
- : undefined,
- cacheRead: usage.promptTokensDetails.cachedTokens,
- cacheWrite: undefined,
- },
- outputTokens: {
- total: usage.completionTokens,
- text: undefined,
- reasoning: usage.completionTokensDetails.reasoningTokens,
- },
- raw: {
- prompt_tokens: usage.promptTokens ?? null,
- completion_tokens: usage.completionTokens ?? null,
- total_tokens: usage.totalTokens ?? null,
- },
- },
- providerMetadata,
- })
- },
- }),
- ),
- request: { body },
- response: { headers: responseHeaders },
- }
- }
- }
- const openaiCompatibleTokenUsageSchema = z
- .object({
- prompt_tokens: z.number().nullish(),
- completion_tokens: z.number().nullish(),
- total_tokens: z.number().nullish(),
- prompt_tokens_details: z
- .object({
- cached_tokens: z.number().nullish(),
- })
- .nullish(),
- completion_tokens_details: z
- .object({
- reasoning_tokens: z.number().nullish(),
- accepted_prediction_tokens: z.number().nullish(),
- rejected_prediction_tokens: z.number().nullish(),
- })
- .nullish(),
- })
- .nullish()
- // limited version of the schema, focussed on what is needed for the implementation
- // this approach limits breakages when the API changes and increases efficiency
- const OpenAICompatibleChatResponseSchema = z.object({
- id: z.string().nullish(),
- created: z.number().nullish(),
- model: z.string().nullish(),
- choices: z.array(
- z.object({
- message: z.object({
- role: z.literal("assistant").nullish(),
- content: z.string().nullish(),
- // Copilot-specific reasoning fields
- reasoning_text: z.string().nullish(),
- reasoning_opaque: z.string().nullish(),
- tool_calls: z
- .array(
- z.object({
- id: z.string().nullish(),
- function: z.object({
- name: z.string(),
- arguments: z.string(),
- }),
- }),
- )
- .nullish(),
- }),
- finish_reason: z.string().nullish(),
- }),
- ),
- usage: openaiCompatibleTokenUsageSchema,
- })
- // limited version of the schema, focussed on what is needed for the implementation
- // this approach limits breakages when the API changes and increases efficiency
- const createOpenAICompatibleChatChunkSchema = <ERROR_SCHEMA extends z.core.$ZodType>(errorSchema: ERROR_SCHEMA) =>
- z.union([
- z.object({
- id: z.string().nullish(),
- created: z.number().nullish(),
- model: z.string().nullish(),
- choices: z.array(
- z.object({
- delta: z
- .object({
- role: z.enum(["assistant"]).nullish(),
- content: z.string().nullish(),
- // Copilot-specific reasoning fields
- reasoning_text: z.string().nullish(),
- reasoning_opaque: z.string().nullish(),
- tool_calls: z
- .array(
- z.object({
- index: z.number(),
- id: z.string().nullish(),
- function: z.object({
- name: z.string().nullish(),
- arguments: z.string().nullish(),
- }),
- }),
- )
- .nullish(),
- })
- .nullish(),
- finish_reason: z.string().nullish(),
- }),
- ),
- usage: openaiCompatibleTokenUsageSchema,
- }),
- errorSchema,
- ])
|