@elizaos/plugin-openai 2.0.3-beta.4 → 2.0.3-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"import type { IAgentRuntime, ModelTypeName } from \"@elizaos/core\";\nimport { EventType } from \"@elizaos/core\";\nimport type { TokenUsage } from \"../types\";\n\nconst MAX_PROMPT_LENGTH = 200;\n\ninterface ModelUsageEventPayload {\n runtime: IAgentRuntime;\n source: \"openai\";\n provider: \"openai\";\n type: ModelTypeName;\n prompt: string;\n tokens: {\n prompt: number;\n completion: number;\n total: number;\n cached?: number;\n };\n}\n\ninterface AISDKUsage {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n cachedInputTokens?: number;\n}\n\ninterface OpenAIAPIUsage {\n promptTokens?: number;\n completionTokens?: number;\n totalTokens?: number;\n cachedPromptTokens?: number;\n promptTokensDetails?: {\n cachedTokens?: number;\n };\n}\n\ntype ModelUsage = TokenUsage | AISDKUsage | OpenAIAPIUsage;\n\nfunction truncatePrompt(prompt: string): string {\n if (prompt.length <= MAX_PROMPT_LENGTH) {\n return prompt;\n }\n return `${prompt.slice(0, MAX_PROMPT_LENGTH)}…`;\n}\n\nfunction normalizeUsage(usage: ModelUsage): TokenUsage {\n if (\"promptTokens\" in usage) {\n const promptTokensDetails =\n \"promptTokensDetails\" in usage ? usage.promptTokensDetails : undefined;\n const cachedPromptTokens = usage.cachedPromptTokens ?? promptTokensDetails?.cachedTokens;\n return {\n promptTokens: usage.promptTokens ?? 0,\n completionTokens: usage.completionTokens ?? 0,\n totalTokens: usage.totalTokens ?? (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0),\n cachedPromptTokens,\n };\n }\n if (\"inputTokens\" in usage || \"outputTokens\" in usage) {\n const input = (usage as AISDKUsage).inputTokens ?? 0;\n const output = (usage as AISDKUsage).outputTokens ?? 0;\n const total = (usage as AISDKUsage).totalTokens ?? input + output;\n return {\n promptTokens: input,\n completionTokens: output,\n totalTokens: total,\n cachedPromptTokens: (usage as AISDKUsage).cachedInputTokens,\n };\n }\n return {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n}\n\nexport function emitModelUsageEvent(\n runtime: IAgentRuntime,\n type: ModelTypeName,\n prompt: string,\n usage: ModelUsage\n): void {\n const normalized = normalizeUsage(usage);\n\n const payload: ModelUsageEventPayload = {\n runtime,\n source: \"openai\",\n provider: \"openai\",\n type,\n prompt: truncatePrompt(prompt),\n tokens: {\n prompt: normalized.promptTokens,\n completion: normalized.completionTokens,\n total: normalized.totalTokens,\n ...(normalized.cachedPromptTokens !== undefined\n ? { cached: normalized.cachedPromptTokens }\n : {}),\n },\n };\n\n runtime.emitEvent(EventType.MODEL_USED, payload);\n}\n",
|
|
12
12
|
"import type {\n IAgentRuntime,\n ImageDescriptionParams,\n ImageGenerationParams,\n RecordLlmCallDetails,\n} from \"@elizaos/core\";\nimport { logger, ModelType, recordLlmCall } from \"@elizaos/core\";\nimport type {\n ImageDescriptionResult,\n ImageGenerationResult,\n ImageQuality,\n ImageSize,\n ImageStyle,\n OpenAIChatCompletionResponse,\n OpenAIImageGenerationResponse,\n} from \"../types\";\nimport {\n getAuthHeader,\n getBaseURL,\n getImageDescriptionAuthHeader,\n getImageDescriptionBaseURL,\n getImageDescriptionMaxTokens,\n getImageDescriptionModel,\n getImageModel,\n} from \"../utils/config\";\nimport { emitModelUsageEvent } from \"../utils/events\";\n\ninterface ExtendedImageGenerationParams extends ImageGenerationParams {\n quality?: ImageQuality;\n style?: ImageStyle;\n}\n\nconst DEFAULT_IMAGE_DESCRIPTION_PROMPT =\n \"Please analyze this image and provide a title and detailed description.\";\n\nexport async function handleImageGeneration(\n runtime: IAgentRuntime,\n params: ImageGenerationParams\n): Promise<ImageGenerationResult[]> {\n const modelName = getImageModel(runtime);\n const count = params.count ?? 1;\n const size: ImageSize = (params.size as ImageSize) ?? \"1024x1024\";\n const extendedParams = params as ExtendedImageGenerationParams;\n\n logger.debug(`[OpenAI] Using IMAGE model: ${modelName}`);\n\n if (typeof params.prompt !== \"string\" || params.prompt.trim().length === 0) {\n throw new Error(\"IMAGE generation requires a non-empty prompt\");\n }\n\n if (count < 1 || count > 10) {\n throw new Error(\"IMAGE count must be between 1 and 10\");\n }\n\n const baseURL = getBaseURL(runtime);\n\n const requestBody: Record<string, string | number> = {\n model: modelName,\n prompt: params.prompt,\n n: count,\n size,\n };\n\n if (extendedParams.quality) {\n requestBody.quality = extendedParams.quality;\n }\n if (extendedParams.style) {\n requestBody.style = extendedParams.style;\n }\n\n const details: RecordLlmCallDetails = {\n model: modelName,\n systemPrompt: \"\",\n userPrompt: params.prompt,\n temperature: 0,\n maxTokens: 0,\n purpose: \"external_llm\",\n actionType: \"openai.images.generate\",\n };\n const data = await recordLlmCall(runtime, details, async () => {\n const response = await fetch(`${baseURL}/images/generations`, {\n method: \"POST\",\n headers: {\n ...getAuthHeader(runtime),\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestBody),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n throw new Error(\n `OpenAI image generation failed: ${response.status} ${response.statusText} - ${errorText}`\n );\n }\n\n const responseData = (await response.json()) as OpenAIImageGenerationResponse;\n details.response = JSON.stringify(responseData.data);\n return responseData;\n });\n\n if (data.data.length === 0) {\n throw new Error(\"OpenAI API returned no images\");\n }\n\n return data.data.map((item) => ({\n url: item.url,\n revisedPrompt: item.revised_prompt,\n }));\n}\n\nfunction parseTitleFromResponse(content: string): string {\n const titleMatch = content.match(/title[:\\s]+(.+?)(?:\\n|$)/i);\n return titleMatch?.[1]?.trim() ?? \"Image Analysis\";\n}\n\nfunction parseDescriptionFromResponse(content: string): string {\n return content.replace(/title[:\\s]+(.+?)(?:\\n|$)/i, \"\").trim();\n}\n\nexport async function handleImageDescription(\n runtime: IAgentRuntime,\n params: ImageDescriptionParams | string\n): Promise<ImageDescriptionResult> {\n const modelName = getImageDescriptionModel(runtime);\n const paramsWithMaxTokens = params as ImageDescriptionParams & { maxTokens?: number };\n const maxTokens =\n typeof params === \"object\" && typeof paramsWithMaxTokens.maxTokens === \"number\"\n ? paramsWithMaxTokens.maxTokens\n : getImageDescriptionMaxTokens(runtime);\n\n logger.debug(`[OpenAI] Using IMAGE_DESCRIPTION model: ${modelName}`);\n\n let imageUrl: string;\n let promptText: string;\n\n if (typeof params === \"string\") {\n imageUrl = params;\n promptText = DEFAULT_IMAGE_DESCRIPTION_PROMPT;\n } else {\n imageUrl = params.imageUrl;\n promptText = params.prompt ?? DEFAULT_IMAGE_DESCRIPTION_PROMPT;\n }\n\n if (!imageUrl || imageUrl.trim().length === 0) {\n throw new Error(\"IMAGE_DESCRIPTION requires a valid image URL\");\n }\n\n const baseURL = getImageDescriptionBaseURL(runtime);\n\n const requestBody = {\n model: modelName,\n messages: [\n {\n role: \"user\",\n content: [\n { type: \"text\", text: promptText },\n { type: \"image_url\", image_url: { url: imageUrl } },\n ],\n },\n ],\n max_tokens: maxTokens,\n };\n\n const details: RecordLlmCallDetails = {\n model: modelName,\n systemPrompt: \"\",\n userPrompt: promptText,\n temperature: 0,\n maxTokens,\n purpose: \"external_llm\",\n actionType: \"openai.chat.completions.create\",\n };\n const data = await recordLlmCall(runtime, details, async () => {\n const response = await fetch(`${baseURL}/chat/completions`, {\n method: \"POST\",\n headers: {\n ...getImageDescriptionAuthHeader(runtime),\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestBody),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n throw new Error(\n `OpenAI image description failed: ${response.status} ${response.statusText} - ${errorText}`\n );\n }\n\n const responseData = (await response.json()) as OpenAIChatCompletionResponse;\n const responseContent = responseData.choices[0]?.message.content;\n if (!responseContent) {\n throw new Error(\"OpenAI API returned empty image description\");\n }\n details.response = responseContent;\n if (responseData.usage) {\n details.promptTokens = responseData.usage.prompt_tokens;\n details.completionTokens = responseData.usage.completion_tokens;\n }\n return responseData;\n });\n\n if (data.usage) {\n emitModelUsageEvent(\n runtime,\n ModelType.IMAGE_DESCRIPTION,\n typeof params === \"string\" ? params : (params.prompt ?? \"\"),\n {\n promptTokens: data.usage.prompt_tokens,\n completionTokens: data.usage.completion_tokens,\n totalTokens: data.usage.total_tokens,\n }\n );\n }\n\n const firstChoice = data.choices[0];\n const content = firstChoice?.message.content;\n\n if (!content) {\n throw new Error(\"OpenAI API returned empty image description\");\n }\n\n return {\n title: parseTitleFromResponse(content),\n description: parseDescriptionFromResponse(content),\n };\n}\n",
|
|
13
13
|
"/**\n * Deep Research model handler\n *\n * Provides deep research capabilities using OpenAI's o3-deep-research and o4-mini-deep-research models.\n * These models can find, analyze, and synthesize hundreds of sources to create comprehensive reports.\n *\n * @see https://platform.openai.com/docs/guides/deep-research\n */\n\nimport type {\n IAgentRuntime,\n JsonValue,\n RecordLlmCallDetails,\n ResearchAnnotation,\n ResearchCodeInterpreterCall,\n ResearchFileSearchCall,\n ResearchMcpToolCall,\n ResearchMessageOutput,\n ResearchOutputItem,\n ResearchParams,\n ResearchResult,\n ResearchTool,\n ResearchWebSearchCall,\n} from \"@elizaos/core\";\nimport { logger, recordLlmCall } from \"@elizaos/core\";\nimport { getApiKey, getBaseURL, getResearchModel, getResearchTimeout } from \"../utils/config\";\n\n// ============================================================================\n// Types for OpenAI Responses API\n// ============================================================================\n\n/**\n * Tool configuration for the Responses API\n */\ninterface ResponsesApiTool {\n type: \"web_search_preview\" | \"file_search\" | \"code_interpreter\" | \"mcp\";\n vector_store_ids?: string[];\n container?: { type: \"auto\" };\n server_label?: string;\n server_url?: string;\n require_approval?: \"never\";\n}\n\n/**\n * Raw response from the OpenAI Responses API\n */\ninterface ResponsesApiResponse {\n id: string;\n object: string;\n status?: \"queued\" | \"in_progress\" | \"completed\" | \"failed\";\n output?: ResponsesApiOutputItem[];\n output_text?: string;\n error?: {\n message: string;\n code: string;\n };\n}\n\n/**\n * Raw output item from the Responses API\n */\ninterface ResponsesApiOutputItem {\n id?: string;\n type: string;\n status?: string;\n action?: {\n type: string;\n query?: string;\n url?: string;\n };\n query?: string;\n results?: Array<{\n file_id: string;\n file_name: string;\n score: number;\n }>;\n code?: string;\n output?: string;\n server_label?: string;\n tool_name?: string;\n arguments?: Record<string, unknown>;\n result?: unknown;\n content?: Array<{\n type: string;\n text: string;\n annotations?: Array<{\n url: string;\n title: string;\n start_index: number;\n end_index: number;\n }>;\n }>;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Converts ResearchTool params to Responses API tool format\n */\nfunction convertToolToApi(tool: ResearchTool): ResponsesApiTool {\n switch (tool.type) {\n case \"web_search_preview\":\n return { type: \"web_search_preview\" };\n case \"file_search\":\n return {\n type: \"file_search\",\n vector_store_ids: tool.vectorStoreIds,\n };\n case \"code_interpreter\":\n return {\n type: \"code_interpreter\",\n container: tool.container ?? { type: \"auto\" },\n };\n case \"mcp\":\n return {\n type: \"mcp\",\n server_label: tool.serverLabel,\n server_url: tool.serverUrl,\n require_approval: tool.requireApproval ?? \"never\",\n };\n default:\n throw new Error(`Unknown research tool type: ${(tool as ResearchTool).type}`);\n }\n}\n\n/**\n * Converts raw API output items to typed ResearchOutputItem\n */\nfunction convertOutputItem(item: ResponsesApiOutputItem): ResearchOutputItem | null {\n switch (item.type) {\n case \"web_search_call\":\n return {\n id: item.id ?? \"\",\n type: \"web_search_call\",\n status: (item.status as \"completed\" | \"failed\") ?? \"completed\",\n action: {\n type: (item.action?.type as \"search\" | \"open_page\" | \"find_in_page\") ?? \"search\",\n query: item.action?.query,\n url: item.action?.url,\n },\n } satisfies ResearchWebSearchCall;\n\n case \"file_search_call\":\n return {\n id: item.id ?? \"\",\n type: \"file_search_call\",\n status: (item.status as \"completed\" | \"failed\") ?? \"completed\",\n query: item.query ?? \"\",\n results: item.results?.map((r) => ({\n fileId: r.file_id,\n fileName: r.file_name,\n score: r.score,\n })),\n } satisfies ResearchFileSearchCall;\n\n case \"code_interpreter_call\":\n return {\n id: item.id ?? \"\",\n type: \"code_interpreter_call\",\n status: (item.status as \"completed\" | \"failed\") ?? \"completed\",\n code: item.code ?? \"\",\n output: item.output,\n } satisfies ResearchCodeInterpreterCall;\n\n case \"mcp_tool_call\":\n return {\n id: item.id ?? \"\",\n type: \"mcp_tool_call\",\n status: (item.status as \"completed\" | \"failed\") ?? \"completed\",\n serverLabel: item.server_label ?? \"\",\n toolName: item.tool_name ?? \"\",\n arguments: (item.arguments ?? {}) as Record<string, JsonValue>,\n result: item.result as JsonValue,\n } satisfies ResearchMcpToolCall;\n\n case \"message\":\n return {\n type: \"message\",\n content:\n item.content?.map((c) => ({\n type: \"output_text\" as const,\n text: c.text,\n annotations:\n c.annotations?.map((a) => ({\n url: a.url,\n title: a.title,\n startIndex: a.start_index,\n endIndex: a.end_index,\n })) ?? [],\n })) ?? [],\n } satisfies ResearchMessageOutput;\n\n default:\n // Unknown output type, skip\n return null;\n }\n}\n\n/**\n * Extracts text and annotations from the response\n */\nfunction extractTextAndAnnotations(response: ResponsesApiResponse): {\n text: string;\n annotations: ResearchAnnotation[];\n} {\n // Try output_text first (convenience field)\n if (response.output_text) {\n // Find annotations from message output items\n const annotations: ResearchAnnotation[] = [];\n if (response.output) {\n for (const item of response.output) {\n if (item.type === \"message\" && item.content) {\n for (const content of item.content) {\n if (content.annotations) {\n for (const ann of content.annotations) {\n annotations.push({\n url: ann.url,\n title: ann.title,\n startIndex: ann.start_index,\n endIndex: ann.end_index,\n });\n }\n }\n }\n }\n }\n }\n return { text: response.output_text, annotations };\n }\n\n // Fall back to extracting from message output items\n let text = \"\";\n const annotations: ResearchAnnotation[] = [];\n\n if (response.output) {\n for (const item of response.output) {\n if (item.type === \"message\" && item.content) {\n for (const content of item.content) {\n text += content.text;\n if (content.annotations) {\n for (const ann of content.annotations) {\n annotations.push({\n url: ann.url,\n title: ann.title,\n startIndex: ann.start_index,\n endIndex: ann.end_index,\n });\n }\n }\n }\n }\n }\n }\n\n return { text, annotations };\n}\n\n// ============================================================================\n// Main Handler\n// ============================================================================\n\n/**\n * Handles RESEARCH model requests using OpenAI's deep research models.\n *\n * Deep research models can take tens of minutes to complete tasks.\n * Use background mode for long-running tasks.\n *\n * @param runtime - The agent runtime\n * @param params - Research parameters\n * @returns Research result with text, annotations, and output items\n *\n * @example\n * ```typescript\n * const result = await handleResearch(runtime, {\n * input: \"Research the economic impact of AI on global labor markets\",\n * tools: [\n * { type: \"web_search_preview\" },\n * { type: \"code_interpreter\", container: { type: \"auto\" } }\n * ],\n * background: true,\n * });\n * console.log(result.text);\n * ```\n */\nexport async function handleResearch(\n runtime: IAgentRuntime,\n params: ResearchParams\n): Promise<ResearchResult> {\n const apiKey = getApiKey(runtime);\n if (!apiKey) {\n throw new Error(\n \"OPENAI_API_KEY is required for deep research. Set it in your environment variables or runtime settings.\"\n );\n }\n\n const baseURL = getBaseURL(runtime);\n const modelName = params.model ?? getResearchModel(runtime);\n const timeout = getResearchTimeout(runtime);\n\n logger.debug(`[OpenAI] Starting deep research with model: ${modelName}`);\n logger.debug(`[OpenAI] Research input: ${params.input.substring(0, 100)}...`);\n\n // Validate that at least one data source tool is provided\n const dataSourceTools = params.tools?.filter(\n (t) => t.type === \"web_search_preview\" || t.type === \"file_search\" || t.type === \"mcp\"\n );\n\n if (!dataSourceTools || dataSourceTools.length === 0) {\n // Default to web search if no tools specified\n logger.debug(\"[OpenAI] No data source tools specified, defaulting to web_search_preview\");\n params.tools = [{ type: \"web_search_preview\" }, ...(params.tools ?? [])];\n }\n\n // Build the request body for the Responses API\n const requestBody: Record<string, unknown> = {\n model: modelName,\n input: params.input,\n };\n\n if (params.instructions) {\n requestBody.instructions = params.instructions;\n }\n\n if (params.background !== undefined) {\n requestBody.background = params.background;\n }\n\n if (params.tools && params.tools.length > 0) {\n requestBody.tools = params.tools.map(convertToolToApi);\n }\n\n if (params.maxToolCalls !== undefined) {\n requestBody.max_tool_calls = params.maxToolCalls;\n }\n\n if (params.reasoningSummary) {\n requestBody.reasoning = { summary: params.reasoningSummary };\n }\n\n logger.debug(`[OpenAI] Research request body: ${JSON.stringify(requestBody, null, 2)}`);\n\n const details: RecordLlmCallDetails = {\n model: modelName,\n systemPrompt: params.instructions ?? \"\",\n userPrompt: params.input,\n temperature: 0,\n maxTokens: 0,\n purpose: \"external_llm\",\n actionType: \"openai.responses.create\",\n };\n const data = await recordLlmCall(runtime, details, async () => {\n // Make the API request\n const response = await fetch(`${baseURL}/responses`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestBody),\n signal: AbortSignal.timeout(timeout),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(`[OpenAI] Research request failed: ${response.status} ${errorText}`);\n throw new Error(`Deep research request failed: ${response.status} ${response.statusText}`);\n }\n\n const responseData = (await response.json()) as ResponsesApiResponse;\n details.response = responseData.output_text ?? \"\";\n return responseData;\n });\n\n if (data.error) {\n logger.error(`[OpenAI] Research API error: ${data.error.message}`);\n throw new Error(`Deep research error: ${data.error.message}`);\n }\n\n logger.debug(`[OpenAI] Research response received. Status: ${data.status ?? \"completed\"}`);\n\n // Extract text and annotations\n const { text, annotations } = extractTextAndAnnotations(data);\n\n // Convert output items\n const outputItems: ResearchOutputItem[] = [];\n if (data.output) {\n for (const item of data.output) {\n const converted = convertOutputItem(item);\n if (converted) {\n outputItems.push(converted);\n }\n }\n }\n\n const result: ResearchResult = {\n id: data.id,\n text,\n annotations,\n outputItems,\n status: data.status,\n };\n\n logger.info(\n `[OpenAI] Research completed. Text length: ${text.length}, Annotations: ${annotations.length}, Output items: ${outputItems.length}`\n );\n\n return result;\n}\n",
|
|
14
|
-
"/**\n * Text generation model handlers\n *\n * Provides text generation using OpenAI's language models.\n */\n\nimport type {\n GenerateTextParams,\n IAgentRuntime,\n JsonValue,\n ModelTypeName,\n RecordLlmCallDetails,\n} from \"@elizaos/core\";\nimport {\n buildCanonicalSystemPrompt,\n dropDuplicateLeadingSystemMessage,\n logger,\n ModelType,\n normalizeSchemaForCerebras,\n recordLlmCall,\n resolveEffectiveSystemPrompt,\n sanitizeFunctionNameForCerebras,\n} from \"@elizaos/core\";\nimport {\n generateText,\n type JSONSchema7,\n jsonSchema,\n type LanguageModelUsage,\n type ModelMessage,\n Output,\n streamText,\n type ToolChoice,\n type ToolSet,\n type UserContent,\n} from \"ai\";\nimport { createOpenAIClient } from \"../providers\";\nimport type { TextStreamResult, TokenUsage } from \"../types\";\nimport {\n getActionPlannerModel,\n getExperimentalTelemetry,\n getLargeModel,\n getMediumModel,\n getMegaModel,\n getNanoModel,\n getResponseHandlerModel,\n getSmallModel,\n isCerebrasMode,\n} from \"../utils/config\";\nimport { emitModelUsageEvent } from \"../utils/events\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Function to get model name from runtime\n */\ntype ModelNameGetter = (runtime: IAgentRuntime) => string;\n\ntype PromptCacheRetention = \"in_memory\" | \"24h\";\ntype ChatAttachment = {\n data: string | Uint8Array | URL;\n mediaType: string;\n filename?: string;\n};\n\ninterface OpenAIPromptCacheOptions {\n promptCacheKey?: string;\n promptCacheRetention?: PromptCacheRetention;\n}\n\ninterface GenerateTextParamsWithOpenAIOptions\n extends Omit<\n GenerateTextParams,\n \"messages\" | \"tools\" | \"toolChoice\" | \"responseSchema\" | \"providerOptions\"\n > {\n model?: string;\n attachments?: ChatAttachment[];\n messages?: unknown[];\n tools?: unknown;\n toolChoice?: unknown;\n responseSchema?: unknown;\n providerOptions?: Record<string, object | JsonValue> & {\n agentName?: string;\n openai?: OpenAIPromptCacheOptions;\n };\n}\n\ntype NativeOutput = NonNullable<Parameters<typeof generateText<ToolSet>>[0][\"output\"]>;\ntype NativeGenerateTextParams = Parameters<typeof generateText<ToolSet, NativeOutput>>[0];\ntype NativeStreamTextParams = Parameters<typeof streamText<ToolSet, NativeOutput>>[0];\ntype NativePrompt =\n | { prompt: string; messages?: never }\n | { messages: ModelMessage[]; prompt?: never };\ntype NativeTextParams = Omit<NativeGenerateTextParams, \"messages\" | \"prompt\"> &\n Omit<NativeStreamTextParams, \"messages\" | \"prompt\"> &\n NativePrompt & {\n // Re-declared explicitly: TypeScript's `Parameters<typeof generateText>`\n // inference produces an overload-union that drops this field, but the\n // ai SDK's runtime signature accepts it (see ai@6 `CallSettings & Prompt`).\n allowSystemInMessages?: boolean;\n };\ntype NativeProviderOptions = NativeTextParams[\"providerOptions\"];\ntype NativeTelemetrySettings = NativeTextParams[\"experimental_telemetry\"];\n\ntype LanguageModelUsageWithCache = Omit<LanguageModelUsage, \"inputTokenDetails\"> & {\n inputTokenDetails?: LanguageModelUsage[\"inputTokenDetails\"] & {\n cachedInputTokens?: number;\n cacheCreationInputTokens?: number;\n cacheCreationTokens?: number;\n };\n cachedInputTokens?: number;\n cacheReadInputTokens?: number;\n cacheCreationInputTokens?: number;\n cacheWriteInputTokens?: number;\n input_tokens_details?: {\n cached_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n prompt_tokens_details?: {\n cached_tokens?: number;\n };\n};\n\ninterface NativeGenerateTextResult {\n text: string;\n toolCalls?: unknown[];\n finishReason?: string;\n usage?: TokenUsage;\n providerMetadata?: unknown;\n}\n\ntype NativeTextModelResult = string & NativeGenerateTextResult;\n\nconst TEXT_NANO_MODEL_TYPE = ModelType.TEXT_NANO as ModelTypeName;\nconst TEXT_MEDIUM_MODEL_TYPE = ModelType.TEXT_MEDIUM as ModelTypeName;\nconst TEXT_MEGA_MODEL_TYPE = ModelType.TEXT_MEGA as ModelTypeName;\nconst RESPONSE_HANDLER_MODEL_TYPE = ModelType.RESPONSE_HANDLER as ModelTypeName;\nconst ACTION_PLANNER_MODEL_TYPE = ModelType.ACTION_PLANNER as ModelTypeName;\n\nfunction resolveRequestedModelName(\n params: GenerateTextParamsWithOpenAIOptions,\n runtime: IAgentRuntime,\n getModelFn: ModelNameGetter\n): string {\n return typeof params.model === \"string\" && params.model.trim().length > 0\n ? params.model.trim()\n : getModelFn(runtime);\n}\n\nfunction buildUserContent(params: GenerateTextParamsWithOpenAIOptions): UserContent {\n const content: Array<\n | { type: \"text\"; text: string }\n | {\n type: \"file\";\n data: string | Uint8Array | URL;\n mediaType: string;\n filename?: string;\n }\n > = [{ type: \"text\", text: params.prompt ?? \"\" }];\n\n for (const attachment of params.attachments ?? []) {\n content.push({\n type: \"file\",\n data: attachment.data,\n mediaType: attachment.mediaType,\n ...(attachment.filename ? { filename: attachment.filename } : {}),\n });\n }\n\n return content;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Converts AI SDK usage to our token usage format.\n *\n * Emits both the legacy `cachedPromptTokens` (kept for back-compat with\n * existing OpenAI consumers) and the canonical v5 `cacheReadInputTokens`\n * (consumed by the trajectory recorder + cost table). They always carry the\n * same value when the AI SDK reports cached input.\n */\nfunction convertUsage(usage: LanguageModelUsage | undefined): TokenUsage | undefined {\n if (!usage) {\n return undefined;\n }\n\n // The AI SDK uses inputTokens/outputTokens\n const promptTokens = usage.inputTokens ?? 0;\n const completionTokens = usage.outputTokens ?? 0;\n const usageWithCache: LanguageModelUsageWithCache = usage;\n const cachedInput =\n firstNumber(\n usageWithCache.cacheReadInputTokens,\n usageWithCache.cachedInputTokens,\n usageWithCache.inputTokenDetails?.cacheReadTokens,\n usageWithCache.inputTokenDetails?.cachedInputTokens,\n usageWithCache.input_tokens_details?.cache_read_input_tokens,\n usageWithCache.input_tokens_details?.cached_tokens,\n usageWithCache.prompt_tokens_details?.cached_tokens\n ) ?? undefined;\n const cacheCreationInput = firstNumber(\n usageWithCache.cacheCreationInputTokens,\n usageWithCache.cacheWriteInputTokens,\n usageWithCache.inputTokenDetails?.cacheCreationInputTokens,\n usageWithCache.inputTokenDetails?.cacheCreationTokens,\n usageWithCache.inputTokenDetails?.cacheWriteTokens,\n usageWithCache.input_tokens_details?.cache_creation_input_tokens\n );\n\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n cachedPromptTokens: cachedInput,\n cacheReadInputTokens: cachedInput,\n cacheCreationInputTokens: cacheCreationInput,\n };\n}\n\nfunction firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"string\" && value.trim().length > 0) {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) {\n return parsed;\n }\n }\n }\n return undefined;\n}\n\nfunction resolvePromptCacheOptions(params: GenerateTextParams): OpenAIPromptCacheOptions {\n const withOpenAIOptions = params as GenerateTextParamsWithOpenAIOptions;\n return {\n promptCacheKey: withOpenAIOptions.providerOptions?.openai?.promptCacheKey,\n promptCacheRetention: withOpenAIOptions.providerOptions?.openai?.promptCacheRetention,\n };\n}\n\n/**\n * Forward `OPENAI_REASONING_EFFORT` (runtime setting / process.env) as\n * `reasoning_effort` on the outbound chat completions request. This is\n * the OpenAI-spec knob for reasoning-capable models (`o1-*`, `o3-*`,\n * `gpt-oss-*`, `deepseek-r1`, `qwen-3-thinking`, etc.) — including\n * Cerebras and OpenRouter, which honor the same field. `\"low\"` keeps\n * reasoning short enough that visible content always fits inside\n * `max_tokens`, which is the failure mode on Cerebras gpt-oss-120b when\n * left unset.\n *\n * In Cerebras mode the field defaults to `\"low\"` when unset, but ONLY for\n * reasoning-capable models (e.g. gpt-oss-*, deepseek-r1, qwen-3-thinking):\n * gpt-oss-120b emits a separate reasoning channel and, left unbounded, spends\n * the whole token budget reasoning — returning empty visible content, which\n * makes the agent fall back to \"I don't have a reply for that\". `\"low\"` keeps\n * reasoning short so a reply always materializes. Non-reasoning Cerebras models\n * (Llama, etc.) reject `reasoning_effort`, so they must never receive the\n * default. For all other models an unset/invalid value yields `undefined`, so\n * they pay no overhead and the wire stays clean. An explicit valid\n * `OPENAI_REASONING_EFFORT` always wins.\n *\n * Valid values follow the OpenAI spec exactly: `minimal`, `low`,\n * `medium`, `high`. Anything else is logged and ignored.\n */\ntype ReasoningEffort = \"minimal\" | \"low\" | \"medium\" | \"high\";\n\nconst VALID_REASONING_EFFORTS: readonly ReasoningEffort[] = [\"minimal\", \"low\", \"medium\", \"high\"];\n\n/**\n * Reasoning-capable model families that emit a separate reasoning channel and\n * honor `reasoning_effort`. Used to gate the Cerebras `\"low\"` default so\n * non-reasoning models (Llama, etc.) are never sent the field.\n */\nfunction isReasoningModel(modelName: string | undefined): boolean {\n if (!modelName) return false;\n const m = modelName.toLowerCase();\n return (\n m.includes(\"gpt-oss\") ||\n m.includes(\"o1\") ||\n m.includes(\"o3\") ||\n m.includes(\"o4\") ||\n m.includes(\"deepseek-r1\") ||\n m.includes(\"thinking\") ||\n m.includes(\"reasoning\") ||\n m.includes(\"qwq\")\n );\n}\n\nfunction resolveReasoningEffort(\n runtime: IAgentRuntime,\n modelName?: string\n): ReasoningEffort | undefined {\n const raw = runtime.getSetting(\"OPENAI_REASONING_EFFORT\");\n const normalized = typeof raw === \"string\" ? raw.trim().toLowerCase() : \"\";\n if (normalized) {\n if ((VALID_REASONING_EFFORTS as readonly string[]).includes(normalized)) {\n return normalized as ReasoningEffort;\n }\n logger.warn(\n `[OpenAI] OPENAI_REASONING_EFFORT=${raw} is not a valid reasoning effort; ignoring. Expected one of: ${VALID_REASONING_EFFORTS.join(\", \")}.`\n );\n }\n // gpt-oss-120b on Cerebras returns empty content when reasoning runs\n // unbounded; default to \"low\" so a visible reply always fits — but only for\n // reasoning-capable models. Non-reasoning Cerebras models (Llama, etc.)\n // reject `reasoning_effort` and would break. An explicit valid value above\n // wins over this default.\n if (isCerebrasMode(runtime) && isReasoningModel(modelName)) {\n return \"low\";\n }\n return undefined;\n}\n\nfunction resolveProviderOptions(\n params: GenerateTextParams,\n runtime: IAgentRuntime,\n modelName?: string\n): Record<string, unknown> | undefined {\n const withOpenAIOptions = params as GenerateTextParamsWithOpenAIOptions;\n const rawProviderOptions = withOpenAIOptions.providerOptions;\n const promptCacheOptions = resolvePromptCacheOptions(params);\n const reasoningEffort = resolveReasoningEffort(runtime, modelName);\n\n if (\n !rawProviderOptions &&\n !promptCacheOptions.promptCacheKey &&\n !promptCacheOptions.promptCacheRetention &&\n !reasoningEffort\n ) {\n return undefined;\n }\n\n // Cerebras supports prompt caching on gpt-oss-120b — 128-token blocks,\n // default-on. The `prompt_cache_key` field IS accepted by Cerebras's\n // OpenAI-compatible endpoint and surfaces hit counts via\n // `usage.prompt_tokens_details.cached_tokens` (same shape as OpenAI), so\n // we keep it in the request body. Only `prompt_cache_retention` is an\n // OpenAI-direct-only field that Cerebras rejects with HTTP 400\n // (`wrong_api_format`), so we strip just that one when in Cerebras mode.\n const skipCacheRetention = isCerebrasMode(runtime);\n\n const { agentName: _agentName, openai: rawOpenAIOptions, ...rest } = rawProviderOptions ?? {};\n // When on Cerebras, scrub OpenAI-direct-only fields (e.g. `promptCacheRetention`)\n // from `rawOpenAIOptions` before they're spread; otherwise they reach the wire\n // and the Cerebras endpoint rejects with HTTP 400 `wrong_api_format`.\n const sanitizedRawOpenAIOptions = (() => {\n if (!rawOpenAIOptions || typeof rawOpenAIOptions !== \"object\") return rawOpenAIOptions;\n if (!skipCacheRetention) return rawOpenAIOptions;\n const { promptCacheRetention: _drop, ...rest2 } = rawOpenAIOptions as Record<string, unknown>;\n return rest2;\n })();\n const openaiOptions = {\n ...(sanitizedRawOpenAIOptions ?? {}),\n ...(promptCacheOptions.promptCacheKey\n ? { promptCacheKey: promptCacheOptions.promptCacheKey }\n : {}),\n ...(!skipCacheRetention && promptCacheOptions.promptCacheRetention\n ? { promptCacheRetention: promptCacheOptions.promptCacheRetention }\n : {}),\n // The caller's explicit `reasoningEffort` wins over the resolved default\n // (env var, or Cerebras \"low\") — same precedence pattern as promptCacheKey.\n ...((sanitizedRawOpenAIOptions as { reasoningEffort?: unknown } | undefined)\n ?.reasoningEffort === undefined && reasoningEffort\n ? { reasoningEffort }\n : {}),\n };\n\n const providerOptions = {\n ...rest,\n ...(Object.keys(openaiOptions).length > 0 ? { openai: openaiOptions } : {}),\n };\n\n return Object.keys(providerOptions).length > 0 ? providerOptions : undefined;\n}\n\nfunction buildStructuredOutput(responseSchema: unknown): NativeOutput {\n if (\n responseSchema &&\n typeof responseSchema === \"object\" &&\n \"responseFormat\" in responseSchema &&\n \"parseCompleteOutput\" in responseSchema\n ) {\n return responseSchema as NativeOutput;\n }\n\n const schemaOptions =\n responseSchema && typeof responseSchema === \"object\" && \"schema\" in responseSchema\n ? (responseSchema as { schema: unknown; name?: string; description?: string })\n : { schema: responseSchema };\n\n return Output.object({\n schema: jsonSchema(sanitizeJsonSchema(schemaOptions.schema, true)),\n ...(schemaOptions.name ? { name: schemaOptions.name } : {}),\n ...(schemaOptions.description ? { description: schemaOptions.description } : {}),\n }) as NativeOutput;\n}\n\nfunction normalizeNativeTools(\n tools: unknown,\n options: { cerebrasMode?: boolean } = {}\n): ToolSet | undefined {\n if (!tools) {\n return undefined;\n }\n\n // Existing AI SDK callers already pass a ToolSet keyed by tool name. Keep it\n // intact so custom tool instances, execute hooks, and dynamic tool metadata\n // are preserved.\n if (!Array.isArray(tools)) {\n return tools as ToolSet;\n }\n\n const toolSet: Record<string, unknown> = {};\n\n for (const rawTool of tools) {\n const tool = asRecord(rawTool);\n const functionTool = asRecord(tool.function);\n const name = firstString(tool.name, functionTool.name);\n\n if (!name) {\n throw new Error(\"[OpenAI] Native tool definition is missing a name.\");\n }\n\n const description = firstString(tool.description, functionTool.description);\n // Default to a permissive object schema. The empty-properties shape\n // (`{ type: \"object\", properties: {}, additionalProperties: false }`) is\n // accepted by OpenAI but rejected by strict-grammar providers like\n // Cerebras with `Object fields require at least one of: 'properties' or\n // 'anyOf' with a list of possible properties`.\n const rawSchema =\n tool.parameters ?? functionTool.parameters ?? ({ type: \"object\" } satisfies JSONSchema7);\n let inputSchema = sanitizeJsonSchema(rawSchema, true);\n if (options.cerebrasMode) {\n // User-supplied schemas may still contain empty-properties subobjects\n // even after sanitizeJsonSchema. Apply Cerebras-specific normalization\n // recursively so deep schemas are accepted by the grammar compiler.\n // Pass isRoot: true so the top-level invariant is enforced (must be\n // type:\"object\" with no root oneOf/anyOf/enum/not).\n inputSchema = normalizeSchemaForCerebras(inputSchema, true) as JSONSchema7;\n }\n\n // Cerebras's grammar compiler rejects function names containing characters\n // outside `[a-zA-Z0-9_-]` (e.g. `math.factorial`). The AI SDK looks up\n // tools by the registered key, so we register under the sanitized name AND\n // surface it to the model under that name. Tool calls come back with the\n // sanitized name, which the runtime resolves through its action registry —\n // any caller relying on dotted action names should pre-sanitize.\n const registeredName = options.cerebrasMode ? sanitizeFunctionNameForCerebras(name) : name;\n\n toolSet[registeredName] = {\n ...(description ? { description } : {}),\n inputSchema: jsonSchema(inputSchema as JSONSchema7),\n };\n }\n\n return Object.keys(toolSet).length > 0 ? (toolSet as ToolSet) : undefined;\n}\n\nfunction normalizeNativeMessages(messages: unknown): ModelMessage[] | undefined {\n if (!Array.isArray(messages)) {\n return undefined;\n }\n\n return messages.map((message) => normalizeNativeMessage(message));\n}\n\nfunction normalizeNativeMessage(message: unknown): ModelMessage {\n const raw = asRecord(message);\n const providerOptions = asOptionalRecord(raw.providerOptions);\n\n if (raw.role === \"system\") {\n return {\n role: \"system\",\n content: stringifyMessageContent(raw.content),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n if (raw.role === \"assistant\") {\n return {\n role: \"assistant\",\n content: normalizeAssistantContent(raw),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n if (raw.role === \"tool\") {\n return {\n role: \"tool\",\n content: normalizeToolContent(raw),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n return {\n role: \"user\",\n content: normalizeUserContent(raw.content),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n}\n\n/**\n * Strip reasoning-only parts from outbound assistant content.\n *\n * OpenAI-spec reasoning models (Cerebras gpt-oss-120b, OpenAI o1/o3,\n * DeepSeek R1, Qwen-3-thinking, etc.) return reasoning in the assistant\n * response — either as a separate `reasoning` / `reasoning_content`\n * field, or as content parts with `type: \"reasoning\"`. Echoing those\n * back to the next turn is wrong on both ends:\n * - Cerebras returns HTTP 400 (`messages.X.assistant.reasoning_content:\n * property is unsupported`).\n * - OpenAI silently drops them, which wastes prompt tokens.\n *\n * The AI SDK upstream of this normalizer surfaces those reasoning blocks\n * as `{ type: \"reasoning\", ... }` content parts. We drop them here so\n * the wire stays spec-clean for the next turn. The reasoning itself\n * remains usable as a single-turn signal (still on the response object);\n * we only refuse to round-trip it.\n */\nfunction stripReasoningParts(content: unknown[]): unknown[] {\n return content.filter((part) => {\n if (!part || typeof part !== \"object\") return true;\n const type = (part as { type?: unknown }).type;\n return type !== \"reasoning\" && type !== \"thinking\";\n });\n}\n\nfunction normalizeAssistantContent(message: Record<string, unknown>): unknown {\n const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];\n\n if (toolCalls.length === 0) {\n if (Array.isArray(message.content)) {\n return stripReasoningParts(message.content);\n }\n if (typeof message.content === \"string\") {\n return message.content;\n }\n return \"\";\n }\n\n const parts: unknown[] = [];\n if (typeof message.content === \"string\" && message.content.length > 0) {\n parts.push({ type: \"text\", text: message.content });\n } else if (Array.isArray(message.content)) {\n parts.push(...stripReasoningParts(message.content));\n }\n\n for (const toolCall of toolCalls) {\n const rawCall = asRecord(toolCall);\n const rawFunction = asRecord(rawCall.function);\n const toolCallId = firstString(rawCall.toolCallId, rawCall.id);\n const toolName = firstString(rawCall.toolName, rawCall.name, rawFunction.name);\n\n if (!toolCallId || !toolName) {\n continue;\n }\n\n parts.push({\n type: \"tool-call\",\n toolCallId,\n toolName,\n input: parseToolCallInput(rawCall, rawFunction),\n });\n }\n\n return parts;\n}\n\nfunction normalizeToolContent(message: Record<string, unknown>): unknown[] {\n if (Array.isArray(message.content)) {\n return message.content;\n }\n\n const toolCallId = firstString(message.toolCallId, message.id) ?? \"tool-call\";\n const toolName = firstString(message.toolName, message.name) ?? \"tool\";\n const parsed = parseJsonIfPossible(message.content);\n\n return [\n {\n type: \"tool-result\",\n toolCallId,\n toolName,\n output:\n typeof parsed === \"string\"\n ? { type: \"text\", value: parsed }\n : { type: \"json\", value: parsed },\n },\n ];\n}\n\nfunction normalizeUserContent(content: unknown): UserContent {\n if (Array.isArray(content)) {\n return content as UserContent;\n }\n return stringifyMessageContent(content);\n}\n\nfunction stringifyMessageContent(content: unknown): string {\n if (typeof content === \"string\") {\n return content;\n }\n if (content == null) {\n return \"\";\n }\n return typeof content === \"object\" ? JSON.stringify(content) : String(content);\n}\n\nfunction parseToolCallInput(\n rawCall: Record<string, unknown>,\n rawFunction: Record<string, unknown>\n): unknown {\n if (\"input\" in rawCall) {\n return rawCall.input;\n }\n return parseJsonIfPossible(rawCall.arguments ?? rawFunction.arguments ?? {});\n}\n\nfunction parseJsonIfPossible(value: unknown): unknown {\n if (typeof value !== \"string\") {\n return value ?? \"\";\n }\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeToolChoice(toolChoice: unknown): ToolChoice<ToolSet> | undefined {\n if (!toolChoice) {\n return undefined;\n }\n\n if (\n typeof toolChoice === \"string\" &&\n (toolChoice === \"auto\" || toolChoice === \"none\" || toolChoice === \"required\")\n ) {\n return toolChoice;\n }\n\n const choice = asRecord(toolChoice);\n if (choice.type === \"tool\") {\n if (typeof choice.toolName === \"string\" && choice.toolName.length > 0) {\n return toolChoice as ToolChoice<ToolSet>;\n }\n const toolName = firstString(choice.toolName, choice.name);\n if (toolName) {\n return { type: \"tool\", toolName };\n }\n }\n\n if (choice.type === \"function\") {\n const fn = asRecord(choice.function);\n const toolName = firstString(fn.name);\n if (toolName) {\n return { type: \"tool\", toolName };\n }\n }\n\n const namedTool = firstString(choice.name);\n if (namedTool) {\n return { type: \"tool\", toolName: namedTool };\n }\n\n return toolChoice as ToolChoice<ToolSet>;\n}\n\nfunction hasIllegalStrictRoot(node: Record<string, unknown>): boolean {\n // Strict-mode JSON schema validators on OpenAI-compatible providers (Groq,\n // Cerebras, OpenAI strict tools) reject tool-parameters whose top level is\n // not `type: \"object\"` or carries `oneOf`/`anyOf`/`enum`/`not` at the root.\n // The error wording varies by provider but the constraint is uniform.\n if (node.type !== \"object\") return true;\n if (Array.isArray(node.oneOf) && node.oneOf.length > 0) return true;\n if (Array.isArray(node.anyOf) && node.anyOf.length > 0) return true;\n if (Array.isArray(node.enum)) return true;\n if (node.not !== undefined) return true;\n return false;\n}\n\nfunction sanitizeJsonSchema(schema: unknown, isRoot = false): JSONSchema7 {\n if (!schema || typeof schema !== \"object\" || Array.isArray(schema)) {\n // Permissive fallback: no `properties: {}`/`additionalProperties: false`\n // pair, which strict-grammar providers reject. See `normalizeSchemaForCerebras`\n // in @elizaos/core for the rationale.\n return { type: \"object\" };\n }\n\n const record = schema as Record<string, unknown>;\n let sanitized: Record<string, unknown> = { ...record };\n\n if (typeof sanitized.type !== \"string\") {\n const inferredType = inferJsonSchemaType(sanitized, isRoot);\n if (inferredType) {\n sanitized.type = inferredType;\n }\n }\n\n if (isRoot && hasIllegalStrictRoot(sanitized)) {\n // Wrap the original schema under properties.value. Strict-tool callers\n // that unwrap arguments will see `{ value: <original> }`. The recursion\n // below normalises the wrapped child like any other property.\n sanitized = {\n type: \"object\",\n properties: { value: { ...record } },\n required: [\"value\"],\n additionalProperties: false,\n };\n }\n\n if (\n sanitized.properties &&\n typeof sanitized.properties === \"object\" &&\n !Array.isArray(sanitized.properties)\n ) {\n const properties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitized.properties as Record<string, unknown>)) {\n properties[key] = sanitizeJsonSchema(value);\n }\n sanitized.properties = properties;\n\n const propertyKeys = Object.keys(properties);\n const existingRequired = Array.isArray(sanitized.required)\n ? sanitized.required.filter((key): key is string => typeof key === \"string\")\n : [];\n sanitized.required = [...new Set([...existingRequired, ...propertyKeys])];\n }\n\n if (sanitized.type === \"object\" && sanitized.additionalProperties !== false) {\n sanitized.additionalProperties = false;\n }\n\n if (sanitized.items) {\n sanitized.items = Array.isArray(sanitized.items)\n ? sanitized.items.map((item) => sanitizeJsonSchema(item))\n : sanitizeJsonSchema(sanitized.items);\n }\n\n for (const unionKey of [\"anyOf\", \"oneOf\", \"allOf\"] as const) {\n const value = sanitized[unionKey];\n if (Array.isArray(value)) {\n sanitized[unionKey] = value.map((item) => sanitizeJsonSchema(item));\n }\n }\n\n return sanitized as JSONSchema7;\n}\n\nfunction inferJsonSchemaType(schema: Record<string, unknown>, isRoot: boolean): string | undefined {\n if (\n \"properties\" in schema ||\n \"required\" in schema ||\n \"additionalProperties\" in schema ||\n isRoot\n ) {\n return \"object\";\n }\n if (\"items\" in schema) {\n return \"array\";\n }\n if (Array.isArray(schema.enum) && schema.enum.length > 0) {\n const types = new Set(schema.enum.map((value) => typeof value));\n if (types.size === 1) {\n const [type] = [...types];\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n return type;\n }\n }\n }\n return undefined;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction asOptionalRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n }\n return undefined;\n}\n\nfunction usesNativeTextResult(params: GenerateTextParamsWithOpenAIOptions): boolean {\n return Boolean(params.messages || params.tools || params.toolChoice || params.responseSchema);\n}\n\nfunction buildNativeTextResult(\n result: {\n text: string;\n toolCalls?: unknown[];\n finishReason?: string;\n usage?: LanguageModelUsage;\n providerMetadata?: unknown;\n },\n modelName?: string\n): NativeGenerateTextResult {\n return {\n text: result.text,\n toolCalls: result.toolCalls ?? [],\n finishReason: result.finishReason,\n usage: convertUsage(result.usage),\n providerMetadata: mergeProviderModelName(result.providerMetadata, modelName),\n };\n}\n\nfunction handledPromise<T>(value: T | PromiseLike<T>): Promise<T> {\n const promise = Promise.resolve(value);\n promise.catch(() => {\n // The streaming path primarily consumes `textStream`. AI SDK companion\n // promises such as `text` can reject later on empty streams even when no\n // caller requested them, which otherwise surfaces as an unhandled rejection.\n });\n return promise;\n}\n\nfunction handledMappedPromise<T, U>(\n value: T | PromiseLike<T>,\n mapper: (resolved: T) => U | PromiseLike<U>\n): Promise<U> {\n return handledPromise(handledPromise(value).then(mapper));\n}\n\nfunction mergeProviderModelName(providerMetadata: unknown, modelName?: string): unknown {\n if (!modelName) {\n return providerMetadata;\n }\n if (\n providerMetadata &&\n typeof providerMetadata === \"object\" &&\n !Array.isArray(providerMetadata)\n ) {\n return {\n ...(providerMetadata as Record<string, unknown>),\n modelName,\n };\n }\n return { modelName };\n}\n\nfunction createLlmCallDetails(\n modelName: string,\n params: GenerateTextParams,\n systemPrompt: string | undefined,\n actionType: string,\n modelType?: ModelTypeName,\n providerOptions?: Record<string, unknown>,\n generateParams?: NativeTextParams\n): RecordLlmCallDetails {\n const originalParams = params as GenerateTextParamsWithOpenAIOptions;\n const nativeParams = generateParams as\n | (NativeTextParams & {\n output?: unknown;\n maxOutputTokens?: unknown;\n })\n | undefined;\n const nativePrompt = nativeParams && \"prompt\" in nativeParams ? nativeParams.prompt : undefined;\n const nativeMessages =\n nativeParams && \"messages\" in nativeParams && Array.isArray(nativeParams.messages)\n ? nativeParams.messages\n : undefined;\n const nativeSystem =\n typeof nativeParams?.system === \"string\" ? nativeParams.system : systemPrompt;\n return {\n model: modelName,\n modelType,\n provider: \"vercel-ai-sdk\",\n systemPrompt: nativeSystem ?? \"\",\n userPrompt:\n typeof nativePrompt === \"string\"\n ? nativePrompt\n : typeof params.prompt === \"string\"\n ? params.prompt\n : \"\",\n prompt: typeof nativePrompt === \"string\" ? nativePrompt : undefined,\n messages: nativeMessages,\n tools: nativeParams?.tools ?? originalParams.tools,\n toolChoice: nativeParams?.toolChoice ?? originalParams.toolChoice,\n output:\n nativeParams?.output !== undefined\n ? buildTrajectoryOutputDescriptor(originalParams.responseSchema, nativeParams.output)\n : undefined,\n responseSchema: originalParams.responseSchema,\n providerOptions:\n providerOptions ?? nativeParams?.providerOptions ?? originalParams.providerOptions,\n temperature: params.temperature ?? 0,\n maxTokens:\n typeof nativeParams?.maxOutputTokens === \"number\"\n ? nativeParams.maxOutputTokens\n : params.omitMaxTokens\n ? 0\n : (params.maxTokens ?? 8192),\n maxTokensOmitted:\n params.omitMaxTokens && typeof nativeParams?.maxOutputTokens !== \"number\" ? true : undefined,\n purpose: \"external_llm\",\n actionType,\n };\n}\n\nfunction buildTrajectoryOutputDescriptor(responseSchema: unknown, output: unknown): unknown {\n if (responseSchema !== undefined) {\n return {\n type: \"object\",\n schema: responseSchema,\n };\n }\n return toTrajectoryJsonSafe(output);\n}\n\nfunction toTrajectoryJsonSafe(value: unknown): unknown {\n try {\n return JSON.parse(\n JSON.stringify(value, (_key, nested) => {\n if (typeof nested === \"function\") return undefined;\n if (typeof nested === \"bigint\") return nested.toString();\n return nested;\n })\n ) as unknown;\n } catch {\n return String(value);\n }\n}\n\nfunction applyUsageToDetails(\n details: RecordLlmCallDetails,\n usage: LanguageModelUsage | undefined\n): void {\n if (!usage) {\n return;\n }\n details.promptTokens = usage.inputTokens ?? 0;\n details.completionTokens = usage.outputTokens ?? 0;\n}\n\n// ============================================================================\n// Core Generation Function\n// ============================================================================\n\n/**\n * Generates text using the specified model type.\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @param modelType - The type of model (TEXT_SMALL or TEXT_LARGE)\n * @param getModelFn - Function to get the model name\n * @returns Generated text or stream result\n */\nasync function generateTextByModelType(\n runtime: IAgentRuntime,\n params: GenerateTextParams,\n modelType: ModelTypeName,\n getModelFn: ModelNameGetter\n): Promise<string | TextStreamResult> {\n const paramsWithAttachments = params as GenerateTextParamsWithOpenAIOptions;\n const openai = createOpenAIClient(runtime);\n const modelName = resolveRequestedModelName(paramsWithAttachments, runtime, getModelFn);\n\n logger.debug(`[OpenAI] Using ${modelType} model: ${modelName}`);\n const providerOptions = resolveProviderOptions(params, runtime, modelName);\n const hasAttachments = (paramsWithAttachments.attachments?.length ?? 0) > 0;\n const userContent = hasAttachments ? buildUserContent(paramsWithAttachments) : undefined;\n const shouldReturnNativeResult = usesNativeTextResult(paramsWithAttachments);\n\n const systemPrompt = resolveEffectiveSystemPrompt({\n params: paramsWithAttachments,\n fallback: buildCanonicalSystemPrompt({ character: runtime.character }),\n });\n const agentName = paramsWithAttachments.providerOptions?.agentName;\n const telemetryConfig: NativeTelemetrySettings = {\n isEnabled: getExperimentalTelemetry(runtime),\n functionId: agentName ? `agent:${agentName}` : undefined,\n metadata: agentName ? { agentName } : undefined,\n };\n\n // Chat Completions is the default: broadest compatibility, and it works\n // against every OpenAI-compatible endpoint (Cerebras, local servers, proxies).\n // gpt-5 / gpt-5-mini reasoning models ignore temperature/penalty/stop params.\n //\n const model = openai.chat(modelName);\n const cerebrasMode = isCerebrasMode(runtime);\n const normalizedTools = normalizeNativeTools(paramsWithAttachments.tools, {\n cerebrasMode,\n });\n const normalizedToolChoice = normalizeToolChoice(paramsWithAttachments.toolChoice);\n const normalizedMessages = normalizeNativeMessages(paramsWithAttachments.messages);\n const wireMessages = dropDuplicateLeadingSystemMessage(normalizedMessages, systemPrompt);\n const effectiveMessages =\n wireMessages && wireMessages.length > 0 ? wireMessages : normalizedMessages;\n const promptText =\n typeof params.prompt === \"string\" && params.prompt.length > 0 ? params.prompt : \"\";\n const promptOrMessages: NativePrompt =\n effectiveMessages && effectiveMessages.length > 0\n ? { messages: effectiveMessages }\n : userContent\n ? { messages: [{ role: \"user\" as const, content: userContent }] }\n : { prompt: promptText };\n // elizaOS callers pass `responseFormat: { type: \"json_object\" | \"text\" }`\n // (see `GenerateTextParams` in @elizaos/core). The AI SDK's equivalent\n // is `responseFormat: { type: \"json\" }` (which translates to\n // `response_format: { type: \"json_object\" }` at the OpenAI wire layer).\n // Translate the shape so the param actually reaches the API call —\n // before this, callers asking for json_object were silently ignored\n // and Cerebras returned plain text, dropping us into the simple-reply\n // fallback every turn.\n const callerResponseFormat = (paramsWithAttachments as { responseFormat?: unknown })\n .responseFormat;\n const responseFormatType =\n typeof callerResponseFormat === \"string\"\n ? callerResponseFormat\n : callerResponseFormat &&\n typeof callerResponseFormat === \"object\" &&\n \"type\" in callerResponseFormat\n ? (callerResponseFormat as { type: string }).type\n : undefined;\n const wireResponseFormat: { type: \"json\" } | { type: \"text\" } | undefined =\n responseFormatType === \"json_object\"\n ? { type: \"json\" }\n : responseFormatType === \"text\"\n ? { type: \"text\" }\n : undefined;\n\n const generateParams: NativeTextParams = {\n model,\n ...promptOrMessages,\n system: systemPrompt,\n allowSystemInMessages: true,\n // Omit the cap when the caller opted out (direct-channel Stage-1) so the\n // model's own max applies — a hardcoded value 400s when it exceeds the\n // model's limit. Other callers keep the 8192 default.\n ...(params.omitMaxTokens ? {} : { maxOutputTokens: params.maxTokens ?? 8192 }),\n experimental_telemetry: telemetryConfig,\n ...(normalizedTools ? { tools: normalizedTools } : {}),\n ...(normalizedToolChoice ? { toolChoice: normalizedToolChoice } : {}),\n // Cerebras's OpenAI-compatible endpoint does not accept the\n // `response_format: { type: \"json_schema\", ... }` payload that the AI SDK\n // emits when `output: Output.object(...)` is set. Fall back to relying on\n // `responseFormat: { type: \"json_object\" }` (already passed by callers)\n // plus the schema embedded in the prompt body.\n ...(paramsWithAttachments.responseSchema && !isCerebrasMode(runtime)\n ? { output: buildStructuredOutput(paramsWithAttachments.responseSchema) }\n : {}),\n ...(wireResponseFormat ? { responseFormat: wireResponseFormat } : {}),\n ...(providerOptions ? { providerOptions: providerOptions as NativeProviderOptions } : {}),\n };\n\n // Handle streaming mode\n if (params.stream) {\n const details = createLlmCallDetails(\n modelName,\n params,\n systemPrompt,\n \"ai.streamText\",\n modelType,\n providerOptions,\n generateParams\n );\n details.response = \"\";\n const result = await recordLlmCall(runtime, details, () => streamText(generateParams));\n\n return {\n textStream: (async function* textStreamWithCallback() {\n for await (const chunk of result.textStream) {\n params.onStreamChunk?.(chunk);\n yield chunk;\n }\n })(),\n text: handledPromise(result.text),\n ...(shouldReturnNativeResult ? { toolCalls: handledPromise(result.toolCalls) } : {}),\n usage: handledMappedPromise(result.usage, convertUsage),\n finishReason: handledMappedPromise(result.finishReason, (r) => r as string | undefined),\n };\n }\n\n // Non-streaming mode\n const details = createLlmCallDetails(\n modelName,\n params,\n systemPrompt,\n \"ai.generateText\",\n modelType,\n providerOptions,\n generateParams\n );\n const result = await recordLlmCall(runtime, details, async () => {\n const result = await generateText(generateParams);\n details.response = result.text;\n details.toolCalls = result.toolCalls;\n details.finishReason = result.finishReason as string | undefined;\n details.providerMetadata = result.providerMetadata;\n applyUsageToDetails(details, result.usage);\n return result;\n });\n\n if (result.usage) {\n emitModelUsageEvent(runtime, modelType, params.prompt ?? \"\", result.usage);\n }\n\n if (shouldReturnNativeResult) {\n return buildNativeTextResult(result, modelName) as NativeTextModelResult;\n }\n\n return result.text;\n}\n\n// ============================================================================\n// Public Handlers\n// ============================================================================\n\n/**\n * Handles TEXT_SMALL model requests.\n *\n * Uses the configured small model (default: gpt-5-mini).\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @returns Generated text or stream result\n */\nexport async function handleTextSmall(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ModelType.TEXT_SMALL, getSmallModel);\n}\n\nexport async function handleTextNano(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_NANO_MODEL_TYPE, getNanoModel);\n}\n\nexport async function handleTextMedium(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_MEDIUM_MODEL_TYPE, getMediumModel);\n}\n\n/**\n * Handles TEXT_LARGE model requests.\n *\n * Uses the configured large model (default: gpt-5).\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @returns Generated text or stream result\n */\nexport async function handleTextLarge(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ModelType.TEXT_LARGE, getLargeModel);\n}\n\nexport async function handleTextMega(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_MEGA_MODEL_TYPE, getMegaModel);\n}\n\nexport async function handleResponseHandler(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(\n runtime,\n params,\n RESPONSE_HANDLER_MODEL_TYPE,\n getResponseHandlerModel\n );\n}\n\nexport async function handleActionPlanner(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ACTION_PLANNER_MODEL_TYPE, getActionPlannerModel);\n}\n\n// ─── Test-only exports ──────────────────────────────────────────────────────\n// These are exported for the shape tests in `__tests__/reasoning-effort.shape.test.ts`.\n// Not part of the public API; do not import outside tests.\n\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_resolveProviderOptions = resolveProviderOptions;\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_normalizeNativeMessages = normalizeNativeMessages;\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_stripReasoningParts = stripReasoningParts;\n",
|
|
14
|
+
"/**\n * Text generation model handlers\n *\n * Provides text generation using OpenAI's language models.\n */\n\nimport type {\n GenerateTextParams,\n IAgentRuntime,\n JsonValue,\n ModelTypeName,\n RecordLlmCallDetails,\n} from \"@elizaos/core\";\nimport {\n buildCanonicalSystemPrompt,\n dropDuplicateLeadingSystemMessage,\n logger,\n ModelType,\n normalizeSchemaForCerebras,\n recordLlmCall,\n resolveEffectiveSystemPrompt,\n sanitizeFunctionNameForCerebras,\n} from \"@elizaos/core\";\nimport {\n generateText,\n type JSONSchema7,\n jsonSchema,\n type LanguageModelUsage,\n type ModelMessage,\n Output,\n streamText,\n type ToolChoice,\n type ToolSet,\n type UserContent,\n} from \"ai\";\nimport { createOpenAIClient } from \"../providers\";\nimport type { TextStreamResult, TokenUsage } from \"../types\";\nimport {\n getActionPlannerModel,\n getExperimentalTelemetry,\n getLargeModel,\n getMediumModel,\n getMegaModel,\n getNanoModel,\n getResponseHandlerModel,\n getSmallModel,\n isCerebrasMode,\n} from \"../utils/config\";\nimport { emitModelUsageEvent } from \"../utils/events\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Function to get model name from runtime\n */\ntype ModelNameGetter = (runtime: IAgentRuntime) => string;\n\ntype PromptCacheRetention = \"in_memory\" | \"24h\";\ntype ChatAttachment = {\n data: string | Uint8Array | URL;\n mediaType: string;\n filename?: string;\n};\n\ninterface OpenAIPromptCacheOptions {\n promptCacheKey?: string;\n promptCacheRetention?: PromptCacheRetention;\n}\n\ninterface GenerateTextParamsWithOpenAIOptions\n extends Omit<\n GenerateTextParams,\n \"messages\" | \"tools\" | \"toolChoice\" | \"responseSchema\" | \"providerOptions\"\n > {\n model?: string;\n attachments?: ChatAttachment[];\n messages?: unknown[];\n tools?: unknown;\n toolChoice?: unknown;\n responseSchema?: unknown;\n providerOptions?: Record<string, object | JsonValue> & {\n agentName?: string;\n openai?: OpenAIPromptCacheOptions;\n };\n}\n\ntype NativeOutput = NonNullable<Parameters<typeof generateText<ToolSet>>[0][\"output\"]>;\ntype NativeGenerateTextParams = Parameters<typeof generateText<ToolSet, NativeOutput>>[0];\ntype NativeStreamTextParams = Parameters<typeof streamText<ToolSet, NativeOutput>>[0];\ntype NativePrompt =\n | { prompt: string; messages?: never }\n | { messages: ModelMessage[]; prompt?: never };\ntype NativeTextParams = Omit<NativeGenerateTextParams, \"messages\" | \"prompt\"> &\n Omit<NativeStreamTextParams, \"messages\" | \"prompt\"> &\n NativePrompt & {\n // Re-declared explicitly: TypeScript's `Parameters<typeof generateText>`\n // inference produces an overload-union that drops this field, but the\n // ai SDK's runtime signature accepts it (see ai@6 `CallSettings & Prompt`).\n allowSystemInMessages?: boolean;\n };\ntype NativeProviderOptions = NativeTextParams[\"providerOptions\"];\ntype NativeTelemetrySettings = NativeTextParams[\"experimental_telemetry\"];\n\ntype LanguageModelUsageWithCache = Omit<LanguageModelUsage, \"inputTokenDetails\"> & {\n inputTokenDetails?: LanguageModelUsage[\"inputTokenDetails\"] & {\n cachedInputTokens?: number;\n cacheCreationInputTokens?: number;\n cacheCreationTokens?: number;\n };\n cachedInputTokens?: number;\n cacheReadInputTokens?: number;\n cacheCreationInputTokens?: number;\n cacheWriteInputTokens?: number;\n input_tokens_details?: {\n cached_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n prompt_tokens_details?: {\n cached_tokens?: number;\n };\n};\n\ninterface NativeGenerateTextResult {\n text: string;\n toolCalls?: unknown[];\n finishReason?: string;\n usage?: TokenUsage;\n providerMetadata?: unknown;\n}\n\ntype NativeTextModelResult = string & NativeGenerateTextResult;\n\nconst TEXT_NANO_MODEL_TYPE = ModelType.TEXT_NANO as ModelTypeName;\nconst TEXT_MEDIUM_MODEL_TYPE = ModelType.TEXT_MEDIUM as ModelTypeName;\nconst TEXT_MEGA_MODEL_TYPE = ModelType.TEXT_MEGA as ModelTypeName;\nconst RESPONSE_HANDLER_MODEL_TYPE = ModelType.RESPONSE_HANDLER as ModelTypeName;\nconst ACTION_PLANNER_MODEL_TYPE = ModelType.ACTION_PLANNER as ModelTypeName;\n\nfunction resolveRequestedModelName(\n params: GenerateTextParamsWithOpenAIOptions,\n runtime: IAgentRuntime,\n getModelFn: ModelNameGetter\n): string {\n return typeof params.model === \"string\" && params.model.trim().length > 0\n ? params.model.trim()\n : getModelFn(runtime);\n}\n\nfunction buildUserContent(params: GenerateTextParamsWithOpenAIOptions): UserContent {\n const content: Array<\n | { type: \"text\"; text: string }\n | {\n type: \"file\";\n data: string | Uint8Array | URL;\n mediaType: string;\n filename?: string;\n }\n > = [{ type: \"text\", text: params.prompt ?? \"\" }];\n\n for (const attachment of params.attachments ?? []) {\n content.push({\n type: \"file\",\n data: attachment.data,\n mediaType: attachment.mediaType,\n ...(attachment.filename ? { filename: attachment.filename } : {}),\n });\n }\n\n return content;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Converts AI SDK usage to our token usage format.\n *\n * Emits both the legacy `cachedPromptTokens` (kept for back-compat with\n * existing OpenAI consumers) and the canonical v5 `cacheReadInputTokens`\n * (consumed by the trajectory recorder + cost table). They always carry the\n * same value when the AI SDK reports cached input.\n */\nfunction convertUsage(usage: LanguageModelUsage | undefined): TokenUsage | undefined {\n if (!usage) {\n return undefined;\n }\n\n // The AI SDK uses inputTokens/outputTokens\n const promptTokens = usage.inputTokens ?? 0;\n const completionTokens = usage.outputTokens ?? 0;\n const usageWithCache: LanguageModelUsageWithCache = usage;\n const cachedInput =\n firstNumber(\n usageWithCache.cacheReadInputTokens,\n usageWithCache.cachedInputTokens,\n usageWithCache.inputTokenDetails?.cacheReadTokens,\n usageWithCache.inputTokenDetails?.cachedInputTokens,\n usageWithCache.input_tokens_details?.cache_read_input_tokens,\n usageWithCache.input_tokens_details?.cached_tokens,\n usageWithCache.prompt_tokens_details?.cached_tokens\n ) ?? undefined;\n const cacheCreationInput = firstNumber(\n usageWithCache.cacheCreationInputTokens,\n usageWithCache.cacheWriteInputTokens,\n usageWithCache.inputTokenDetails?.cacheCreationInputTokens,\n usageWithCache.inputTokenDetails?.cacheCreationTokens,\n usageWithCache.inputTokenDetails?.cacheWriteTokens,\n usageWithCache.input_tokens_details?.cache_creation_input_tokens\n );\n\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n cachedPromptTokens: cachedInput,\n cacheReadInputTokens: cachedInput,\n cacheCreationInputTokens: cacheCreationInput,\n };\n}\n\nfunction firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"string\" && value.trim().length > 0) {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) {\n return parsed;\n }\n }\n }\n return undefined;\n}\n\nfunction resolvePromptCacheOptions(params: GenerateTextParams): OpenAIPromptCacheOptions {\n const withOpenAIOptions = params as GenerateTextParamsWithOpenAIOptions;\n return {\n promptCacheKey: withOpenAIOptions.providerOptions?.openai?.promptCacheKey,\n promptCacheRetention: withOpenAIOptions.providerOptions?.openai?.promptCacheRetention,\n };\n}\n\n/**\n * Forward `OPENAI_REASONING_EFFORT` (runtime setting / process.env) as\n * `reasoning_effort` on the outbound chat completions request. This is\n * the OpenAI-spec knob for reasoning-capable models (`o1-*`, `o3-*`,\n * `gpt-oss-*`, `deepseek-r1`, and similar families) — including\n * Cerebras and OpenRouter, which honor the same field. `\"low\"` keeps\n * reasoning short enough that visible content always fits inside\n * `max_tokens`, which is the failure mode on Cerebras gpt-oss-120b when\n * left unset.\n *\n * In Cerebras mode the field defaults to `\"low\"` when unset, but ONLY for\n * reasoning-capable models (e.g. gpt-oss-* and deepseek-r1):\n * gpt-oss-120b emits a separate reasoning channel and, left unbounded, spends\n * the whole token budget reasoning — returning empty visible content, which\n * makes the agent fall back to \"I don't have a reply for that\". `\"low\"` keeps\n * reasoning short so a reply always materializes. Non-reasoning Cerebras models\n * (Llama, etc.) reject `reasoning_effort`, so they must never receive the\n * default. For all other models an unset/invalid value yields `undefined`, so\n * they pay no overhead and the wire stays clean. An explicit valid\n * `OPENAI_REASONING_EFFORT` always wins.\n *\n * Valid values follow the OpenAI spec exactly: `minimal`, `low`,\n * `medium`, `high`. Anything else is logged and ignored.\n */\ntype ReasoningEffort = \"minimal\" | \"low\" | \"medium\" | \"high\";\n\nconst VALID_REASONING_EFFORTS: readonly ReasoningEffort[] = [\"minimal\", \"low\", \"medium\", \"high\"];\n\n/**\n * Reasoning-capable model families that emit a separate reasoning channel and\n * honor `reasoning_effort`. Used to gate the Cerebras `\"low\"` default so\n * non-reasoning models (Llama, etc.) are never sent the field.\n */\nfunction isReasoningModel(modelName: string | undefined): boolean {\n if (!modelName) return false;\n const m = modelName.toLowerCase();\n return (\n m.includes(\"gpt-oss\") ||\n m.includes(\"o1\") ||\n m.includes(\"o3\") ||\n m.includes(\"o4\") ||\n m.includes(\"deepseek-r1\") ||\n m.includes(\"thinking\") ||\n m.includes(\"reasoning\") ||\n m.includes(\"qwq\")\n );\n}\n\nfunction resolveReasoningEffort(\n runtime: IAgentRuntime,\n modelName?: string\n): ReasoningEffort | undefined {\n const raw = runtime.getSetting(\"OPENAI_REASONING_EFFORT\");\n const normalized = typeof raw === \"string\" ? raw.trim().toLowerCase() : \"\";\n if (normalized) {\n if ((VALID_REASONING_EFFORTS as readonly string[]).includes(normalized)) {\n return normalized as ReasoningEffort;\n }\n logger.warn(\n `[OpenAI] OPENAI_REASONING_EFFORT=${raw} is not a valid reasoning effort; ignoring. Expected one of: ${VALID_REASONING_EFFORTS.join(\", \")}.`\n );\n }\n // gpt-oss-120b on Cerebras returns empty content when reasoning runs\n // unbounded; default to \"low\" so a visible reply always fits — but only for\n // reasoning-capable models. Non-reasoning Cerebras models (Llama, etc.)\n // reject `reasoning_effort` and would break. An explicit valid value above\n // wins over this default.\n if (isCerebrasMode(runtime) && isReasoningModel(modelName)) {\n return \"low\";\n }\n return undefined;\n}\n\nfunction resolveProviderOptions(\n params: GenerateTextParams,\n runtime: IAgentRuntime,\n modelName?: string\n): Record<string, unknown> | undefined {\n const withOpenAIOptions = params as GenerateTextParamsWithOpenAIOptions;\n const rawProviderOptions = withOpenAIOptions.providerOptions;\n const promptCacheOptions = resolvePromptCacheOptions(params);\n const reasoningEffort = resolveReasoningEffort(runtime, modelName);\n\n if (\n !rawProviderOptions &&\n !promptCacheOptions.promptCacheKey &&\n !promptCacheOptions.promptCacheRetention &&\n !reasoningEffort\n ) {\n return undefined;\n }\n\n // Cerebras supports prompt caching on gpt-oss-120b — 128-token blocks,\n // default-on. The `prompt_cache_key` field IS accepted by Cerebras's\n // OpenAI-compatible endpoint and surfaces hit counts via\n // `usage.prompt_tokens_details.cached_tokens` (same shape as OpenAI), so\n // we keep it in the request body. Only `prompt_cache_retention` is an\n // OpenAI-direct-only field that Cerebras rejects with HTTP 400\n // (`wrong_api_format`), so we strip just that one when in Cerebras mode.\n const skipCacheRetention = isCerebrasMode(runtime);\n\n const { agentName: _agentName, openai: rawOpenAIOptions, ...rest } = rawProviderOptions ?? {};\n // When on Cerebras, scrub OpenAI-direct-only fields (e.g. `promptCacheRetention`)\n // from `rawOpenAIOptions` before they're spread; otherwise they reach the wire\n // and the Cerebras endpoint rejects with HTTP 400 `wrong_api_format`.\n const sanitizedRawOpenAIOptions = (() => {\n if (!rawOpenAIOptions || typeof rawOpenAIOptions !== \"object\") return rawOpenAIOptions;\n if (!skipCacheRetention) return rawOpenAIOptions;\n const { promptCacheRetention: _drop, ...rest2 } = rawOpenAIOptions as Record<string, unknown>;\n return rest2;\n })();\n const openaiOptions = {\n ...(sanitizedRawOpenAIOptions ?? {}),\n ...(promptCacheOptions.promptCacheKey\n ? { promptCacheKey: promptCacheOptions.promptCacheKey }\n : {}),\n ...(!skipCacheRetention && promptCacheOptions.promptCacheRetention\n ? { promptCacheRetention: promptCacheOptions.promptCacheRetention }\n : {}),\n // The caller's explicit `reasoningEffort` wins over the resolved default\n // (env var, or Cerebras \"low\") — same precedence pattern as promptCacheKey.\n ...((sanitizedRawOpenAIOptions as { reasoningEffort?: unknown } | undefined)\n ?.reasoningEffort === undefined && reasoningEffort\n ? { reasoningEffort }\n : {}),\n };\n\n const providerOptions = {\n ...rest,\n ...(Object.keys(openaiOptions).length > 0 ? { openai: openaiOptions } : {}),\n };\n\n return Object.keys(providerOptions).length > 0 ? providerOptions : undefined;\n}\n\nfunction buildStructuredOutput(responseSchema: unknown): NativeOutput {\n if (\n responseSchema &&\n typeof responseSchema === \"object\" &&\n \"responseFormat\" in responseSchema &&\n \"parseCompleteOutput\" in responseSchema\n ) {\n return responseSchema as NativeOutput;\n }\n\n const schemaOptions =\n responseSchema && typeof responseSchema === \"object\" && \"schema\" in responseSchema\n ? (responseSchema as { schema: unknown; name?: string; description?: string })\n : { schema: responseSchema };\n\n return Output.object({\n schema: jsonSchema(sanitizeJsonSchema(schemaOptions.schema, true)),\n ...(schemaOptions.name ? { name: schemaOptions.name } : {}),\n ...(schemaOptions.description ? { description: schemaOptions.description } : {}),\n }) as NativeOutput;\n}\n\nfunction normalizeNativeTools(\n tools: unknown,\n options: { cerebrasMode?: boolean } = {}\n): ToolSet | undefined {\n if (!tools) {\n return undefined;\n }\n\n // Existing AI SDK callers already pass a ToolSet keyed by tool name. Keep it\n // intact so custom tool instances, execute hooks, and dynamic tool metadata\n // are preserved.\n if (!Array.isArray(tools)) {\n return tools as ToolSet;\n }\n\n const toolSet: Record<string, unknown> = {};\n\n for (const rawTool of tools) {\n const tool = asRecord(rawTool);\n const functionTool = asRecord(tool.function);\n const name = firstString(tool.name, functionTool.name);\n\n if (!name) {\n throw new Error(\"[OpenAI] Native tool definition is missing a name.\");\n }\n\n const description = firstString(tool.description, functionTool.description);\n // Default to a permissive object schema. The empty-properties shape\n // (`{ type: \"object\", properties: {}, additionalProperties: false }`) is\n // accepted by OpenAI but rejected by strict-grammar providers like\n // Cerebras with `Object fields require at least one of: 'properties' or\n // 'anyOf' with a list of possible properties`.\n const rawSchema =\n tool.parameters ?? functionTool.parameters ?? ({ type: \"object\" } satisfies JSONSchema7);\n let inputSchema = sanitizeJsonSchema(rawSchema, true);\n if (options.cerebrasMode) {\n // User-supplied schemas may still contain empty-properties subobjects\n // even after sanitizeJsonSchema. Apply Cerebras-specific normalization\n // recursively so deep schemas are accepted by the grammar compiler.\n // Pass isRoot: true so the top-level invariant is enforced (must be\n // type:\"object\" with no root oneOf/anyOf/enum/not).\n inputSchema = normalizeSchemaForCerebras(inputSchema, true) as JSONSchema7;\n }\n\n // Cerebras's grammar compiler rejects function names containing characters\n // outside `[a-zA-Z0-9_-]` (e.g. `math.factorial`). The AI SDK looks up\n // tools by the registered key, so we register under the sanitized name AND\n // surface it to the model under that name. Tool calls come back with the\n // sanitized name, which the runtime resolves through its action registry —\n // any caller relying on dotted action names should pre-sanitize.\n const registeredName = options.cerebrasMode ? sanitizeFunctionNameForCerebras(name) : name;\n\n toolSet[registeredName] = {\n ...(description ? { description } : {}),\n inputSchema: jsonSchema(inputSchema as JSONSchema7),\n };\n }\n\n return Object.keys(toolSet).length > 0 ? (toolSet as ToolSet) : undefined;\n}\n\nfunction normalizeNativeMessages(messages: unknown): ModelMessage[] | undefined {\n if (!Array.isArray(messages)) {\n return undefined;\n }\n\n return messages.map((message) => normalizeNativeMessage(message));\n}\n\nfunction normalizeNativeMessage(message: unknown): ModelMessage {\n const raw = asRecord(message);\n const providerOptions = asOptionalRecord(raw.providerOptions);\n\n if (raw.role === \"system\") {\n return {\n role: \"system\",\n content: stringifyMessageContent(raw.content),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n if (raw.role === \"assistant\") {\n return {\n role: \"assistant\",\n content: normalizeAssistantContent(raw),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n if (raw.role === \"tool\") {\n return {\n role: \"tool\",\n content: normalizeToolContent(raw),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n return {\n role: \"user\",\n content: normalizeUserContent(raw.content),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n}\n\n/**\n * Strip reasoning-only parts from outbound assistant content.\n *\n * OpenAI-spec reasoning models (Cerebras gpt-oss-120b, OpenAI o1/o3,\n * DeepSeek R1, and similar families) return reasoning in the assistant\n * response — either as a separate `reasoning` / `reasoning_content`\n * field, or as content parts with `type: \"reasoning\"`. Echoing those\n * back to the next turn is wrong on both ends:\n * - Cerebras returns HTTP 400 (`messages.X.assistant.reasoning_content:\n * property is unsupported`).\n * - OpenAI silently drops them, which wastes prompt tokens.\n *\n * The AI SDK upstream of this normalizer surfaces those reasoning blocks\n * as `{ type: \"reasoning\", ... }` content parts. We drop them here so\n * the wire stays spec-clean for the next turn. The reasoning itself\n * remains usable as a single-turn signal (still on the response object);\n * we only refuse to round-trip it.\n */\nfunction stripReasoningParts(content: unknown[]): unknown[] {\n return content.filter((part) => {\n if (!part || typeof part !== \"object\") return true;\n const type = (part as { type?: unknown }).type;\n return type !== \"reasoning\" && type !== \"thinking\";\n });\n}\n\nfunction normalizeAssistantContent(message: Record<string, unknown>): unknown {\n const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];\n\n if (toolCalls.length === 0) {\n if (Array.isArray(message.content)) {\n return stripReasoningParts(message.content);\n }\n if (typeof message.content === \"string\") {\n return message.content;\n }\n return \"\";\n }\n\n const parts: unknown[] = [];\n if (typeof message.content === \"string\" && message.content.length > 0) {\n parts.push({ type: \"text\", text: message.content });\n } else if (Array.isArray(message.content)) {\n parts.push(...stripReasoningParts(message.content));\n }\n\n for (const toolCall of toolCalls) {\n const rawCall = asRecord(toolCall);\n const rawFunction = asRecord(rawCall.function);\n const toolCallId = firstString(rawCall.toolCallId, rawCall.id);\n const toolName = firstString(rawCall.toolName, rawCall.name, rawFunction.name);\n\n if (!toolCallId || !toolName) {\n continue;\n }\n\n parts.push({\n type: \"tool-call\",\n toolCallId,\n toolName,\n input: parseToolCallInput(rawCall, rawFunction),\n });\n }\n\n return parts;\n}\n\nfunction normalizeToolContent(message: Record<string, unknown>): unknown[] {\n if (Array.isArray(message.content)) {\n return message.content;\n }\n\n const toolCallId = firstString(message.toolCallId, message.id) ?? \"tool-call\";\n const toolName = firstString(message.toolName, message.name) ?? \"tool\";\n const parsed = parseJsonIfPossible(message.content);\n\n return [\n {\n type: \"tool-result\",\n toolCallId,\n toolName,\n output:\n typeof parsed === \"string\"\n ? { type: \"text\", value: parsed }\n : { type: \"json\", value: parsed },\n },\n ];\n}\n\nfunction normalizeUserContent(content: unknown): UserContent {\n if (Array.isArray(content)) {\n return content as UserContent;\n }\n return stringifyMessageContent(content);\n}\n\nfunction stringifyMessageContent(content: unknown): string {\n if (typeof content === \"string\") {\n return content;\n }\n if (content == null) {\n return \"\";\n }\n return typeof content === \"object\" ? JSON.stringify(content) : String(content);\n}\n\nfunction parseToolCallInput(\n rawCall: Record<string, unknown>,\n rawFunction: Record<string, unknown>\n): unknown {\n if (\"input\" in rawCall) {\n return rawCall.input;\n }\n return parseJsonIfPossible(rawCall.arguments ?? rawFunction.arguments ?? {});\n}\n\nfunction parseJsonIfPossible(value: unknown): unknown {\n if (typeof value !== \"string\") {\n return value ?? \"\";\n }\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeToolChoice(toolChoice: unknown): ToolChoice<ToolSet> | undefined {\n if (!toolChoice) {\n return undefined;\n }\n\n if (\n typeof toolChoice === \"string\" &&\n (toolChoice === \"auto\" || toolChoice === \"none\" || toolChoice === \"required\")\n ) {\n return toolChoice;\n }\n\n const choice = asRecord(toolChoice);\n if (choice.type === \"tool\") {\n if (typeof choice.toolName === \"string\" && choice.toolName.length > 0) {\n return toolChoice as ToolChoice<ToolSet>;\n }\n const toolName = firstString(choice.toolName, choice.name);\n if (toolName) {\n return { type: \"tool\", toolName };\n }\n }\n\n if (choice.type === \"function\") {\n const fn = asRecord(choice.function);\n const toolName = firstString(fn.name);\n if (toolName) {\n return { type: \"tool\", toolName };\n }\n }\n\n const namedTool = firstString(choice.name);\n if (namedTool) {\n return { type: \"tool\", toolName: namedTool };\n }\n\n return toolChoice as ToolChoice<ToolSet>;\n}\n\nfunction hasIllegalStrictRoot(node: Record<string, unknown>): boolean {\n // Strict-mode JSON schema validators on OpenAI-compatible providers (Groq,\n // Cerebras, OpenAI strict tools) reject tool-parameters whose top level is\n // not `type: \"object\"` or carries `oneOf`/`anyOf`/`enum`/`not` at the root.\n // The error wording varies by provider but the constraint is uniform.\n if (node.type !== \"object\") return true;\n if (Array.isArray(node.oneOf) && node.oneOf.length > 0) return true;\n if (Array.isArray(node.anyOf) && node.anyOf.length > 0) return true;\n if (Array.isArray(node.enum)) return true;\n if (node.not !== undefined) return true;\n return false;\n}\n\nfunction sanitizeJsonSchema(schema: unknown, isRoot = false): JSONSchema7 {\n if (!schema || typeof schema !== \"object\" || Array.isArray(schema)) {\n // Permissive fallback: no `properties: {}`/`additionalProperties: false`\n // pair, which strict-grammar providers reject. See `normalizeSchemaForCerebras`\n // in @elizaos/core for the rationale.\n return { type: \"object\" };\n }\n\n const record = schema as Record<string, unknown>;\n let sanitized: Record<string, unknown> = { ...record };\n\n if (typeof sanitized.type !== \"string\") {\n const inferredType = inferJsonSchemaType(sanitized, isRoot);\n if (inferredType) {\n sanitized.type = inferredType;\n }\n }\n\n if (isRoot && hasIllegalStrictRoot(sanitized)) {\n // Wrap the original schema under properties.value. Strict-tool callers\n // that unwrap arguments will see `{ value: <original> }`. The recursion\n // below normalises the wrapped child like any other property.\n sanitized = {\n type: \"object\",\n properties: { value: { ...record } },\n required: [\"value\"],\n additionalProperties: false,\n };\n }\n\n if (\n sanitized.properties &&\n typeof sanitized.properties === \"object\" &&\n !Array.isArray(sanitized.properties)\n ) {\n const properties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitized.properties as Record<string, unknown>)) {\n properties[key] = sanitizeJsonSchema(value);\n }\n sanitized.properties = properties;\n\n const propertyKeys = Object.keys(properties);\n const existingRequired = Array.isArray(sanitized.required)\n ? sanitized.required.filter((key): key is string => typeof key === \"string\")\n : [];\n sanitized.required = [...new Set([...existingRequired, ...propertyKeys])];\n }\n\n if (sanitized.type === \"object\" && sanitized.additionalProperties !== false) {\n sanitized.additionalProperties = false;\n }\n\n if (sanitized.items) {\n sanitized.items = Array.isArray(sanitized.items)\n ? sanitized.items.map((item) => sanitizeJsonSchema(item))\n : sanitizeJsonSchema(sanitized.items);\n }\n\n for (const unionKey of [\"anyOf\", \"oneOf\", \"allOf\"] as const) {\n const value = sanitized[unionKey];\n if (Array.isArray(value)) {\n sanitized[unionKey] = value.map((item) => sanitizeJsonSchema(item));\n }\n }\n\n return sanitized as JSONSchema7;\n}\n\nfunction inferJsonSchemaType(schema: Record<string, unknown>, isRoot: boolean): string | undefined {\n if (\n \"properties\" in schema ||\n \"required\" in schema ||\n \"additionalProperties\" in schema ||\n isRoot\n ) {\n return \"object\";\n }\n if (\"items\" in schema) {\n return \"array\";\n }\n if (Array.isArray(schema.enum) && schema.enum.length > 0) {\n const types = new Set(schema.enum.map((value) => typeof value));\n if (types.size === 1) {\n const [type] = [...types];\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n return type;\n }\n }\n }\n return undefined;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction asOptionalRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n }\n return undefined;\n}\n\nfunction usesNativeTextResult(params: GenerateTextParamsWithOpenAIOptions): boolean {\n return Boolean(params.messages || params.tools || params.toolChoice || params.responseSchema);\n}\n\nfunction buildNativeTextResult(\n result: {\n text: string;\n toolCalls?: unknown[];\n finishReason?: string;\n usage?: LanguageModelUsage;\n providerMetadata?: unknown;\n },\n modelName?: string\n): NativeGenerateTextResult {\n return {\n text: result.text,\n toolCalls: result.toolCalls ?? [],\n finishReason: result.finishReason,\n usage: convertUsage(result.usage),\n providerMetadata: mergeProviderModelName(result.providerMetadata, modelName),\n };\n}\n\nfunction handledPromise<T>(value: T | PromiseLike<T>): Promise<T> {\n const promise = Promise.resolve(value);\n promise.catch(() => {\n // The streaming path primarily consumes `textStream`. AI SDK companion\n // promises such as `text` can reject later on empty streams even when no\n // caller requested them, which otherwise surfaces as an unhandled rejection.\n });\n return promise;\n}\n\nfunction handledMappedPromise<T, U>(\n value: T | PromiseLike<T>,\n mapper: (resolved: T) => U | PromiseLike<U>\n): Promise<U> {\n return handledPromise(handledPromise(value).then(mapper));\n}\n\nfunction mergeProviderModelName(providerMetadata: unknown, modelName?: string): unknown {\n if (!modelName) {\n return providerMetadata;\n }\n if (\n providerMetadata &&\n typeof providerMetadata === \"object\" &&\n !Array.isArray(providerMetadata)\n ) {\n return {\n ...(providerMetadata as Record<string, unknown>),\n modelName,\n };\n }\n return { modelName };\n}\n\nfunction createLlmCallDetails(\n modelName: string,\n params: GenerateTextParams,\n systemPrompt: string | undefined,\n actionType: string,\n modelType?: ModelTypeName,\n providerOptions?: Record<string, unknown>,\n generateParams?: NativeTextParams\n): RecordLlmCallDetails {\n const originalParams = params as GenerateTextParamsWithOpenAIOptions;\n const nativeParams = generateParams as\n | (NativeTextParams & {\n output?: unknown;\n maxOutputTokens?: unknown;\n })\n | undefined;\n const nativePrompt = nativeParams && \"prompt\" in nativeParams ? nativeParams.prompt : undefined;\n const nativeMessages =\n nativeParams && \"messages\" in nativeParams && Array.isArray(nativeParams.messages)\n ? nativeParams.messages\n : undefined;\n const nativeSystem =\n typeof nativeParams?.system === \"string\" ? nativeParams.system : systemPrompt;\n return {\n model: modelName,\n modelType,\n provider: \"vercel-ai-sdk\",\n systemPrompt: nativeSystem ?? \"\",\n userPrompt:\n typeof nativePrompt === \"string\"\n ? nativePrompt\n : typeof params.prompt === \"string\"\n ? params.prompt\n : \"\",\n prompt: typeof nativePrompt === \"string\" ? nativePrompt : undefined,\n messages: nativeMessages,\n tools: nativeParams?.tools ?? originalParams.tools,\n toolChoice: nativeParams?.toolChoice ?? originalParams.toolChoice,\n output:\n nativeParams?.output !== undefined\n ? buildTrajectoryOutputDescriptor(originalParams.responseSchema, nativeParams.output)\n : undefined,\n responseSchema: originalParams.responseSchema,\n providerOptions:\n providerOptions ?? nativeParams?.providerOptions ?? originalParams.providerOptions,\n temperature: params.temperature ?? 0,\n maxTokens:\n typeof nativeParams?.maxOutputTokens === \"number\"\n ? nativeParams.maxOutputTokens\n : params.omitMaxTokens\n ? 0\n : (params.maxTokens ?? 8192),\n maxTokensOmitted:\n params.omitMaxTokens && typeof nativeParams?.maxOutputTokens !== \"number\" ? true : undefined,\n purpose: \"external_llm\",\n actionType,\n };\n}\n\nfunction buildTrajectoryOutputDescriptor(responseSchema: unknown, output: unknown): unknown {\n if (responseSchema !== undefined) {\n return {\n type: \"object\",\n schema: responseSchema,\n };\n }\n return toTrajectoryJsonSafe(output);\n}\n\nfunction toTrajectoryJsonSafe(value: unknown): unknown {\n try {\n return JSON.parse(\n JSON.stringify(value, (_key, nested) => {\n if (typeof nested === \"function\") return undefined;\n if (typeof nested === \"bigint\") return nested.toString();\n return nested;\n })\n ) as unknown;\n } catch {\n return String(value);\n }\n}\n\nfunction applyUsageToDetails(\n details: RecordLlmCallDetails,\n usage: LanguageModelUsage | undefined\n): void {\n if (!usage) {\n return;\n }\n details.promptTokens = usage.inputTokens ?? 0;\n details.completionTokens = usage.outputTokens ?? 0;\n}\n\n// ============================================================================\n// Core Generation Function\n// ============================================================================\n\n/**\n * Generates text using the specified model type.\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @param modelType - The type of model (TEXT_SMALL or TEXT_LARGE)\n * @param getModelFn - Function to get the model name\n * @returns Generated text or stream result\n */\nasync function generateTextByModelType(\n runtime: IAgentRuntime,\n params: GenerateTextParams,\n modelType: ModelTypeName,\n getModelFn: ModelNameGetter\n): Promise<string | TextStreamResult> {\n const paramsWithAttachments = params as GenerateTextParamsWithOpenAIOptions;\n const openai = createOpenAIClient(runtime);\n const modelName = resolveRequestedModelName(paramsWithAttachments, runtime, getModelFn);\n\n logger.debug(`[OpenAI] Using ${modelType} model: ${modelName}`);\n const providerOptions = resolveProviderOptions(params, runtime, modelName);\n const hasAttachments = (paramsWithAttachments.attachments?.length ?? 0) > 0;\n const userContent = hasAttachments ? buildUserContent(paramsWithAttachments) : undefined;\n const shouldReturnNativeResult = usesNativeTextResult(paramsWithAttachments);\n\n const systemPrompt = resolveEffectiveSystemPrompt({\n params: paramsWithAttachments,\n fallback: buildCanonicalSystemPrompt({ character: runtime.character }),\n });\n const agentName = paramsWithAttachments.providerOptions?.agentName;\n const telemetryConfig: NativeTelemetrySettings = {\n isEnabled: getExperimentalTelemetry(runtime),\n functionId: agentName ? `agent:${agentName}` : undefined,\n metadata: agentName ? { agentName } : undefined,\n };\n\n // Chat Completions is the default: broadest compatibility, and it works\n // against every OpenAI-compatible endpoint (Cerebras, local servers, proxies).\n // gpt-5 / gpt-5-mini reasoning models ignore temperature/penalty/stop params.\n //\n const model = openai.chat(modelName);\n const cerebrasMode = isCerebrasMode(runtime);\n const normalizedTools = normalizeNativeTools(paramsWithAttachments.tools, {\n cerebrasMode,\n });\n const normalizedToolChoice = normalizeToolChoice(paramsWithAttachments.toolChoice);\n const normalizedMessages = normalizeNativeMessages(paramsWithAttachments.messages);\n const wireMessages = dropDuplicateLeadingSystemMessage(normalizedMessages, systemPrompt);\n const effectiveMessages =\n wireMessages && wireMessages.length > 0 ? wireMessages : normalizedMessages;\n const promptText =\n typeof params.prompt === \"string\" && params.prompt.length > 0 ? params.prompt : \"\";\n const promptOrMessages: NativePrompt =\n effectiveMessages && effectiveMessages.length > 0\n ? { messages: effectiveMessages }\n : userContent\n ? { messages: [{ role: \"user\" as const, content: userContent }] }\n : { prompt: promptText };\n // elizaOS callers pass `responseFormat: { type: \"json_object\" | \"text\" }`\n // (see `GenerateTextParams` in @elizaos/core). The AI SDK's equivalent\n // is `responseFormat: { type: \"json\" }` (which translates to\n // `response_format: { type: \"json_object\" }` at the OpenAI wire layer).\n // Translate the shape so the param actually reaches the API call —\n // before this, callers asking for json_object were silently ignored\n // and Cerebras returned plain text, dropping us into the simple-reply\n // fallback every turn.\n const callerResponseFormat = (paramsWithAttachments as { responseFormat?: unknown })\n .responseFormat;\n const responseFormatType =\n typeof callerResponseFormat === \"string\"\n ? callerResponseFormat\n : callerResponseFormat &&\n typeof callerResponseFormat === \"object\" &&\n \"type\" in callerResponseFormat\n ? (callerResponseFormat as { type: string }).type\n : undefined;\n const wireResponseFormat: { type: \"json\" } | { type: \"text\" } | undefined =\n responseFormatType === \"json_object\"\n ? { type: \"json\" }\n : responseFormatType === \"text\"\n ? { type: \"text\" }\n : undefined;\n\n const generateParams: NativeTextParams = {\n model,\n ...promptOrMessages,\n system: systemPrompt,\n allowSystemInMessages: true,\n // Omit the cap when the caller opted out (direct-channel Stage-1) so the\n // model's own max applies — a hardcoded value 400s when it exceeds the\n // model's limit. Other callers keep the 8192 default.\n ...(params.omitMaxTokens ? {} : { maxOutputTokens: params.maxTokens ?? 8192 }),\n experimental_telemetry: telemetryConfig,\n ...(normalizedTools ? { tools: normalizedTools } : {}),\n ...(normalizedToolChoice ? { toolChoice: normalizedToolChoice } : {}),\n // Cerebras's OpenAI-compatible endpoint does not accept the\n // `response_format: { type: \"json_schema\", ... }` payload that the AI SDK\n // emits when `output: Output.object(...)` is set. Fall back to relying on\n // `responseFormat: { type: \"json_object\" }` (already passed by callers)\n // plus the schema embedded in the prompt body.\n ...(paramsWithAttachments.responseSchema && !isCerebrasMode(runtime)\n ? { output: buildStructuredOutput(paramsWithAttachments.responseSchema) }\n : {}),\n ...(wireResponseFormat ? { responseFormat: wireResponseFormat } : {}),\n ...(providerOptions ? { providerOptions: providerOptions as NativeProviderOptions } : {}),\n };\n\n // Handle streaming mode\n if (params.stream) {\n const details = createLlmCallDetails(\n modelName,\n params,\n systemPrompt,\n \"ai.streamText\",\n modelType,\n providerOptions,\n generateParams\n );\n details.response = \"\";\n const result = await recordLlmCall(runtime, details, () => streamText(generateParams));\n\n return {\n textStream: (async function* textStreamWithCallback() {\n for await (const chunk of result.textStream) {\n params.onStreamChunk?.(chunk);\n yield chunk;\n }\n })(),\n text: handledPromise(result.text),\n ...(shouldReturnNativeResult ? { toolCalls: handledPromise(result.toolCalls) } : {}),\n usage: handledMappedPromise(result.usage, convertUsage),\n finishReason: handledMappedPromise(result.finishReason, (r) => r as string | undefined),\n };\n }\n\n // Non-streaming mode\n const details = createLlmCallDetails(\n modelName,\n params,\n systemPrompt,\n \"ai.generateText\",\n modelType,\n providerOptions,\n generateParams\n );\n const result = await recordLlmCall(runtime, details, async () => {\n const result = await generateText(generateParams);\n details.response = result.text;\n details.toolCalls = result.toolCalls;\n details.finishReason = result.finishReason as string | undefined;\n details.providerMetadata = result.providerMetadata;\n applyUsageToDetails(details, result.usage);\n return result;\n });\n\n if (result.usage) {\n emitModelUsageEvent(runtime, modelType, params.prompt ?? \"\", result.usage);\n }\n\n if (shouldReturnNativeResult) {\n return buildNativeTextResult(result, modelName) as NativeTextModelResult;\n }\n\n return result.text;\n}\n\n// ============================================================================\n// Public Handlers\n// ============================================================================\n\n/**\n * Handles TEXT_SMALL model requests.\n *\n * Uses the configured small model (default: gpt-5-mini).\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @returns Generated text or stream result\n */\nexport async function handleTextSmall(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ModelType.TEXT_SMALL, getSmallModel);\n}\n\nexport async function handleTextNano(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_NANO_MODEL_TYPE, getNanoModel);\n}\n\nexport async function handleTextMedium(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_MEDIUM_MODEL_TYPE, getMediumModel);\n}\n\n/**\n * Handles TEXT_LARGE model requests.\n *\n * Uses the configured large model (default: gpt-5).\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @returns Generated text or stream result\n */\nexport async function handleTextLarge(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ModelType.TEXT_LARGE, getLargeModel);\n}\n\nexport async function handleTextMega(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_MEGA_MODEL_TYPE, getMegaModel);\n}\n\nexport async function handleResponseHandler(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(\n runtime,\n params,\n RESPONSE_HANDLER_MODEL_TYPE,\n getResponseHandlerModel\n );\n}\n\nexport async function handleActionPlanner(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ACTION_PLANNER_MODEL_TYPE, getActionPlannerModel);\n}\n\n// ─── Test-only exports ──────────────────────────────────────────────────────\n// These are exported for the shape tests in `__tests__/reasoning-effort.shape.test.ts`.\n// Not part of the public API; do not import outside tests.\n\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_resolveProviderOptions = resolveProviderOptions;\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_normalizeNativeMessages = normalizeNativeMessages;\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_stripReasoningParts = stripReasoningParts;\n",
|
|
15
15
|
"import { createOpenAI, type OpenAIProvider } from \"@ai-sdk/openai\";\nimport type { IAgentRuntime } from \"@elizaos/core\";\nimport { getApiKey, getBaseURL, isProxyMode } from \"../utils/config\";\n\nconst PROXY_API_KEY = \"sk-proxy\";\n\nexport function createOpenAIClient(runtime: IAgentRuntime): OpenAIProvider {\n const baseURL = getBaseURL(runtime);\n const apiKey = getApiKey(runtime);\n\n if (!apiKey && isProxyMode(runtime)) {\n return createOpenAI({\n apiKey: PROXY_API_KEY,\n baseURL,\n });\n }\n\n if (!apiKey) {\n throw new Error(\n \"OPENAI_API_KEY is required. Set it in your environment variables or runtime settings.\"\n );\n }\n\n return createOpenAI({\n apiKey,\n baseURL,\n });\n}\n",
|
|
16
16
|
"import type { IAgentRuntime, ModelTypeName } from \"@elizaos/core\";\nimport { ModelType } from \"@elizaos/core\";\nimport {\n encodingForModel,\n getEncoding,\n type Tiktoken,\n type TiktokenEncoding,\n type TiktokenModel,\n} from \"js-tiktoken\";\nimport { getLargeModel, getSmallModel } from \"./config\";\n\ntype SupportedEncoding = \"cl100k_base\" | \"o200k_base\";\n\nfunction resolveTokenizerEncoding(modelName: string): Tiktoken {\n const normalized = modelName.toLowerCase();\n const fallbackEncoding: SupportedEncoding = normalized.includes(\"4o\")\n ? \"o200k_base\"\n : \"cl100k_base\";\n try {\n return encodingForModel(modelName as TiktokenModel);\n } catch {\n return getEncoding(fallbackEncoding as TiktokenEncoding);\n }\n}\n\nfunction getModelName(runtime: IAgentRuntime, modelType: ModelTypeName): string {\n if (modelType === ModelType.TEXT_SMALL) {\n return getSmallModel(runtime);\n }\n return getLargeModel(runtime);\n}\n\nexport function tokenizeText(\n runtime: IAgentRuntime,\n modelType: ModelTypeName,\n text: string\n): number[] {\n const modelName = getModelName(runtime, modelType);\n const encoder = resolveTokenizerEncoding(modelName);\n return encoder.encode(text);\n}\n\nexport function detokenizeText(\n runtime: IAgentRuntime,\n modelType: ModelTypeName,\n tokens: number[]\n): string {\n const modelName = getModelName(runtime, modelType);\n const encoder = resolveTokenizerEncoding(modelName);\n return encoder.decode(tokens);\n}\n\nexport function countTokens(\n runtime: IAgentRuntime,\n modelType: ModelTypeName,\n text: string\n): number {\n const tokens = tokenizeText(runtime, modelType, text);\n return tokens.length;\n}\n\nexport function truncateToTokenLimit(\n runtime: IAgentRuntime,\n modelType: ModelTypeName,\n text: string,\n maxTokens: number\n): string {\n const tokens = tokenizeText(runtime, modelType, text);\n if (tokens.length <= maxTokens) {\n return text;\n }\n const truncatedTokens = tokens.slice(0, maxTokens);\n return detokenizeText(runtime, modelType, truncatedTokens);\n}\n",
|
|
17
17
|
"import type { DetokenizeTextParams, IAgentRuntime, TokenizeTextParams } from \"@elizaos/core\";\nimport { detokenizeText, tokenizeText } from \"../utils/tokenization\";\n\nexport async function handleTokenizerEncode(\n runtime: IAgentRuntime,\n params: TokenizeTextParams\n): Promise<number[]> {\n if (!params.prompt) {\n throw new Error(\"Tokenization requires a non-empty prompt\");\n }\n const modelType = params.modelType;\n return tokenizeText(runtime, modelType, params.prompt);\n}\n\nexport async function handleTokenizerDecode(\n runtime: IAgentRuntime,\n params: DetokenizeTextParams\n): Promise<string> {\n if (!params.tokens || !Array.isArray(params.tokens)) {\n throw new Error(\"Detokenization requires a valid tokens array\");\n }\n if (params.tokens.length === 0) {\n return \"\";\n }\n for (let i = 0; i < params.tokens.length; i++) {\n const token = params.tokens[i];\n if (typeof token !== \"number\" || !Number.isFinite(token)) {\n throw new Error(`Invalid token at index ${i}: expected number`);\n }\n }\n const modelType = params.modelType;\n return detokenizeText(runtime, modelType, params.tokens);\n}\n",
|
package/dist/models/text.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare function normalizeNativeMessages(messages: unknown): ModelMessage[] | un
|
|
|
12
12
|
* Strip reasoning-only parts from outbound assistant content.
|
|
13
13
|
*
|
|
14
14
|
* OpenAI-spec reasoning models (Cerebras gpt-oss-120b, OpenAI o1/o3,
|
|
15
|
-
* DeepSeek R1,
|
|
15
|
+
* DeepSeek R1, and similar families) return reasoning in the assistant
|
|
16
16
|
* response — either as a separate `reasoning` / `reasoning_content`
|
|
17
17
|
* field, or as content parts with `type: "reasoning"`. Echoing those
|
|
18
18
|
* back to the next turn is wrong on both ends:
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"import type { IAgentRuntime, ModelTypeName } from \"@elizaos/core\";\nimport { EventType } from \"@elizaos/core\";\nimport type { TokenUsage } from \"../types\";\n\nconst MAX_PROMPT_LENGTH = 200;\n\ninterface ModelUsageEventPayload {\n runtime: IAgentRuntime;\n source: \"openai\";\n provider: \"openai\";\n type: ModelTypeName;\n prompt: string;\n tokens: {\n prompt: number;\n completion: number;\n total: number;\n cached?: number;\n };\n}\n\ninterface AISDKUsage {\n inputTokens?: number;\n outputTokens?: number;\n totalTokens?: number;\n cachedInputTokens?: number;\n}\n\ninterface OpenAIAPIUsage {\n promptTokens?: number;\n completionTokens?: number;\n totalTokens?: number;\n cachedPromptTokens?: number;\n promptTokensDetails?: {\n cachedTokens?: number;\n };\n}\n\ntype ModelUsage = TokenUsage | AISDKUsage | OpenAIAPIUsage;\n\nfunction truncatePrompt(prompt: string): string {\n if (prompt.length <= MAX_PROMPT_LENGTH) {\n return prompt;\n }\n return `${prompt.slice(0, MAX_PROMPT_LENGTH)}…`;\n}\n\nfunction normalizeUsage(usage: ModelUsage): TokenUsage {\n if (\"promptTokens\" in usage) {\n const promptTokensDetails =\n \"promptTokensDetails\" in usage ? usage.promptTokensDetails : undefined;\n const cachedPromptTokens = usage.cachedPromptTokens ?? promptTokensDetails?.cachedTokens;\n return {\n promptTokens: usage.promptTokens ?? 0,\n completionTokens: usage.completionTokens ?? 0,\n totalTokens: usage.totalTokens ?? (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0),\n cachedPromptTokens,\n };\n }\n if (\"inputTokens\" in usage || \"outputTokens\" in usage) {\n const input = (usage as AISDKUsage).inputTokens ?? 0;\n const output = (usage as AISDKUsage).outputTokens ?? 0;\n const total = (usage as AISDKUsage).totalTokens ?? input + output;\n return {\n promptTokens: input,\n completionTokens: output,\n totalTokens: total,\n cachedPromptTokens: (usage as AISDKUsage).cachedInputTokens,\n };\n }\n return {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n}\n\nexport function emitModelUsageEvent(\n runtime: IAgentRuntime,\n type: ModelTypeName,\n prompt: string,\n usage: ModelUsage\n): void {\n const normalized = normalizeUsage(usage);\n\n const payload: ModelUsageEventPayload = {\n runtime,\n source: \"openai\",\n provider: \"openai\",\n type,\n prompt: truncatePrompt(prompt),\n tokens: {\n prompt: normalized.promptTokens,\n completion: normalized.completionTokens,\n total: normalized.totalTokens,\n ...(normalized.cachedPromptTokens !== undefined\n ? { cached: normalized.cachedPromptTokens }\n : {}),\n },\n };\n\n runtime.emitEvent(EventType.MODEL_USED, payload);\n}\n",
|
|
12
12
|
"import type {\n IAgentRuntime,\n ImageDescriptionParams,\n ImageGenerationParams,\n RecordLlmCallDetails,\n} from \"@elizaos/core\";\nimport { logger, ModelType, recordLlmCall } from \"@elizaos/core\";\nimport type {\n ImageDescriptionResult,\n ImageGenerationResult,\n ImageQuality,\n ImageSize,\n ImageStyle,\n OpenAIChatCompletionResponse,\n OpenAIImageGenerationResponse,\n} from \"../types\";\nimport {\n getAuthHeader,\n getBaseURL,\n getImageDescriptionAuthHeader,\n getImageDescriptionBaseURL,\n getImageDescriptionMaxTokens,\n getImageDescriptionModel,\n getImageModel,\n} from \"../utils/config\";\nimport { emitModelUsageEvent } from \"../utils/events\";\n\ninterface ExtendedImageGenerationParams extends ImageGenerationParams {\n quality?: ImageQuality;\n style?: ImageStyle;\n}\n\nconst DEFAULT_IMAGE_DESCRIPTION_PROMPT =\n \"Please analyze this image and provide a title and detailed description.\";\n\nexport async function handleImageGeneration(\n runtime: IAgentRuntime,\n params: ImageGenerationParams\n): Promise<ImageGenerationResult[]> {\n const modelName = getImageModel(runtime);\n const count = params.count ?? 1;\n const size: ImageSize = (params.size as ImageSize) ?? \"1024x1024\";\n const extendedParams = params as ExtendedImageGenerationParams;\n\n logger.debug(`[OpenAI] Using IMAGE model: ${modelName}`);\n\n if (typeof params.prompt !== \"string\" || params.prompt.trim().length === 0) {\n throw new Error(\"IMAGE generation requires a non-empty prompt\");\n }\n\n if (count < 1 || count > 10) {\n throw new Error(\"IMAGE count must be between 1 and 10\");\n }\n\n const baseURL = getBaseURL(runtime);\n\n const requestBody: Record<string, string | number> = {\n model: modelName,\n prompt: params.prompt,\n n: count,\n size,\n };\n\n if (extendedParams.quality) {\n requestBody.quality = extendedParams.quality;\n }\n if (extendedParams.style) {\n requestBody.style = extendedParams.style;\n }\n\n const details: RecordLlmCallDetails = {\n model: modelName,\n systemPrompt: \"\",\n userPrompt: params.prompt,\n temperature: 0,\n maxTokens: 0,\n purpose: \"external_llm\",\n actionType: \"openai.images.generate\",\n };\n const data = await recordLlmCall(runtime, details, async () => {\n const response = await fetch(`${baseURL}/images/generations`, {\n method: \"POST\",\n headers: {\n ...getAuthHeader(runtime),\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestBody),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n throw new Error(\n `OpenAI image generation failed: ${response.status} ${response.statusText} - ${errorText}`\n );\n }\n\n const responseData = (await response.json()) as OpenAIImageGenerationResponse;\n details.response = JSON.stringify(responseData.data);\n return responseData;\n });\n\n if (data.data.length === 0) {\n throw new Error(\"OpenAI API returned no images\");\n }\n\n return data.data.map((item) => ({\n url: item.url,\n revisedPrompt: item.revised_prompt,\n }));\n}\n\nfunction parseTitleFromResponse(content: string): string {\n const titleMatch = content.match(/title[:\\s]+(.+?)(?:\\n|$)/i);\n return titleMatch?.[1]?.trim() ?? \"Image Analysis\";\n}\n\nfunction parseDescriptionFromResponse(content: string): string {\n return content.replace(/title[:\\s]+(.+?)(?:\\n|$)/i, \"\").trim();\n}\n\nexport async function handleImageDescription(\n runtime: IAgentRuntime,\n params: ImageDescriptionParams | string\n): Promise<ImageDescriptionResult> {\n const modelName = getImageDescriptionModel(runtime);\n const paramsWithMaxTokens = params as ImageDescriptionParams & { maxTokens?: number };\n const maxTokens =\n typeof params === \"object\" && typeof paramsWithMaxTokens.maxTokens === \"number\"\n ? paramsWithMaxTokens.maxTokens\n : getImageDescriptionMaxTokens(runtime);\n\n logger.debug(`[OpenAI] Using IMAGE_DESCRIPTION model: ${modelName}`);\n\n let imageUrl: string;\n let promptText: string;\n\n if (typeof params === \"string\") {\n imageUrl = params;\n promptText = DEFAULT_IMAGE_DESCRIPTION_PROMPT;\n } else {\n imageUrl = params.imageUrl;\n promptText = params.prompt ?? DEFAULT_IMAGE_DESCRIPTION_PROMPT;\n }\n\n if (!imageUrl || imageUrl.trim().length === 0) {\n throw new Error(\"IMAGE_DESCRIPTION requires a valid image URL\");\n }\n\n const baseURL = getImageDescriptionBaseURL(runtime);\n\n const requestBody = {\n model: modelName,\n messages: [\n {\n role: \"user\",\n content: [\n { type: \"text\", text: promptText },\n { type: \"image_url\", image_url: { url: imageUrl } },\n ],\n },\n ],\n max_tokens: maxTokens,\n };\n\n const details: RecordLlmCallDetails = {\n model: modelName,\n systemPrompt: \"\",\n userPrompt: promptText,\n temperature: 0,\n maxTokens,\n purpose: \"external_llm\",\n actionType: \"openai.chat.completions.create\",\n };\n const data = await recordLlmCall(runtime, details, async () => {\n const response = await fetch(`${baseURL}/chat/completions`, {\n method: \"POST\",\n headers: {\n ...getImageDescriptionAuthHeader(runtime),\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestBody),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n throw new Error(\n `OpenAI image description failed: ${response.status} ${response.statusText} - ${errorText}`\n );\n }\n\n const responseData = (await response.json()) as OpenAIChatCompletionResponse;\n const responseContent = responseData.choices[0]?.message.content;\n if (!responseContent) {\n throw new Error(\"OpenAI API returned empty image description\");\n }\n details.response = responseContent;\n if (responseData.usage) {\n details.promptTokens = responseData.usage.prompt_tokens;\n details.completionTokens = responseData.usage.completion_tokens;\n }\n return responseData;\n });\n\n if (data.usage) {\n emitModelUsageEvent(\n runtime,\n ModelType.IMAGE_DESCRIPTION,\n typeof params === \"string\" ? params : (params.prompt ?? \"\"),\n {\n promptTokens: data.usage.prompt_tokens,\n completionTokens: data.usage.completion_tokens,\n totalTokens: data.usage.total_tokens,\n }\n );\n }\n\n const firstChoice = data.choices[0];\n const content = firstChoice?.message.content;\n\n if (!content) {\n throw new Error(\"OpenAI API returned empty image description\");\n }\n\n return {\n title: parseTitleFromResponse(content),\n description: parseDescriptionFromResponse(content),\n };\n}\n",
|
|
13
13
|
"/**\n * Deep Research model handler\n *\n * Provides deep research capabilities using OpenAI's o3-deep-research and o4-mini-deep-research models.\n * These models can find, analyze, and synthesize hundreds of sources to create comprehensive reports.\n *\n * @see https://platform.openai.com/docs/guides/deep-research\n */\n\nimport type {\n IAgentRuntime,\n JsonValue,\n RecordLlmCallDetails,\n ResearchAnnotation,\n ResearchCodeInterpreterCall,\n ResearchFileSearchCall,\n ResearchMcpToolCall,\n ResearchMessageOutput,\n ResearchOutputItem,\n ResearchParams,\n ResearchResult,\n ResearchTool,\n ResearchWebSearchCall,\n} from \"@elizaos/core\";\nimport { logger, recordLlmCall } from \"@elizaos/core\";\nimport { getApiKey, getBaseURL, getResearchModel, getResearchTimeout } from \"../utils/config\";\n\n// ============================================================================\n// Types for OpenAI Responses API\n// ============================================================================\n\n/**\n * Tool configuration for the Responses API\n */\ninterface ResponsesApiTool {\n type: \"web_search_preview\" | \"file_search\" | \"code_interpreter\" | \"mcp\";\n vector_store_ids?: string[];\n container?: { type: \"auto\" };\n server_label?: string;\n server_url?: string;\n require_approval?: \"never\";\n}\n\n/**\n * Raw response from the OpenAI Responses API\n */\ninterface ResponsesApiResponse {\n id: string;\n object: string;\n status?: \"queued\" | \"in_progress\" | \"completed\" | \"failed\";\n output?: ResponsesApiOutputItem[];\n output_text?: string;\n error?: {\n message: string;\n code: string;\n };\n}\n\n/**\n * Raw output item from the Responses API\n */\ninterface ResponsesApiOutputItem {\n id?: string;\n type: string;\n status?: string;\n action?: {\n type: string;\n query?: string;\n url?: string;\n };\n query?: string;\n results?: Array<{\n file_id: string;\n file_name: string;\n score: number;\n }>;\n code?: string;\n output?: string;\n server_label?: string;\n tool_name?: string;\n arguments?: Record<string, unknown>;\n result?: unknown;\n content?: Array<{\n type: string;\n text: string;\n annotations?: Array<{\n url: string;\n title: string;\n start_index: number;\n end_index: number;\n }>;\n }>;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Converts ResearchTool params to Responses API tool format\n */\nfunction convertToolToApi(tool: ResearchTool): ResponsesApiTool {\n switch (tool.type) {\n case \"web_search_preview\":\n return { type: \"web_search_preview\" };\n case \"file_search\":\n return {\n type: \"file_search\",\n vector_store_ids: tool.vectorStoreIds,\n };\n case \"code_interpreter\":\n return {\n type: \"code_interpreter\",\n container: tool.container ?? { type: \"auto\" },\n };\n case \"mcp\":\n return {\n type: \"mcp\",\n server_label: tool.serverLabel,\n server_url: tool.serverUrl,\n require_approval: tool.requireApproval ?? \"never\",\n };\n default:\n throw new Error(`Unknown research tool type: ${(tool as ResearchTool).type}`);\n }\n}\n\n/**\n * Converts raw API output items to typed ResearchOutputItem\n */\nfunction convertOutputItem(item: ResponsesApiOutputItem): ResearchOutputItem | null {\n switch (item.type) {\n case \"web_search_call\":\n return {\n id: item.id ?? \"\",\n type: \"web_search_call\",\n status: (item.status as \"completed\" | \"failed\") ?? \"completed\",\n action: {\n type: (item.action?.type as \"search\" | \"open_page\" | \"find_in_page\") ?? \"search\",\n query: item.action?.query,\n url: item.action?.url,\n },\n } satisfies ResearchWebSearchCall;\n\n case \"file_search_call\":\n return {\n id: item.id ?? \"\",\n type: \"file_search_call\",\n status: (item.status as \"completed\" | \"failed\") ?? \"completed\",\n query: item.query ?? \"\",\n results: item.results?.map((r) => ({\n fileId: r.file_id,\n fileName: r.file_name,\n score: r.score,\n })),\n } satisfies ResearchFileSearchCall;\n\n case \"code_interpreter_call\":\n return {\n id: item.id ?? \"\",\n type: \"code_interpreter_call\",\n status: (item.status as \"completed\" | \"failed\") ?? \"completed\",\n code: item.code ?? \"\",\n output: item.output,\n } satisfies ResearchCodeInterpreterCall;\n\n case \"mcp_tool_call\":\n return {\n id: item.id ?? \"\",\n type: \"mcp_tool_call\",\n status: (item.status as \"completed\" | \"failed\") ?? \"completed\",\n serverLabel: item.server_label ?? \"\",\n toolName: item.tool_name ?? \"\",\n arguments: (item.arguments ?? {}) as Record<string, JsonValue>,\n result: item.result as JsonValue,\n } satisfies ResearchMcpToolCall;\n\n case \"message\":\n return {\n type: \"message\",\n content:\n item.content?.map((c) => ({\n type: \"output_text\" as const,\n text: c.text,\n annotations:\n c.annotations?.map((a) => ({\n url: a.url,\n title: a.title,\n startIndex: a.start_index,\n endIndex: a.end_index,\n })) ?? [],\n })) ?? [],\n } satisfies ResearchMessageOutput;\n\n default:\n // Unknown output type, skip\n return null;\n }\n}\n\n/**\n * Extracts text and annotations from the response\n */\nfunction extractTextAndAnnotations(response: ResponsesApiResponse): {\n text: string;\n annotations: ResearchAnnotation[];\n} {\n // Try output_text first (convenience field)\n if (response.output_text) {\n // Find annotations from message output items\n const annotations: ResearchAnnotation[] = [];\n if (response.output) {\n for (const item of response.output) {\n if (item.type === \"message\" && item.content) {\n for (const content of item.content) {\n if (content.annotations) {\n for (const ann of content.annotations) {\n annotations.push({\n url: ann.url,\n title: ann.title,\n startIndex: ann.start_index,\n endIndex: ann.end_index,\n });\n }\n }\n }\n }\n }\n }\n return { text: response.output_text, annotations };\n }\n\n // Fall back to extracting from message output items\n let text = \"\";\n const annotations: ResearchAnnotation[] = [];\n\n if (response.output) {\n for (const item of response.output) {\n if (item.type === \"message\" && item.content) {\n for (const content of item.content) {\n text += content.text;\n if (content.annotations) {\n for (const ann of content.annotations) {\n annotations.push({\n url: ann.url,\n title: ann.title,\n startIndex: ann.start_index,\n endIndex: ann.end_index,\n });\n }\n }\n }\n }\n }\n }\n\n return { text, annotations };\n}\n\n// ============================================================================\n// Main Handler\n// ============================================================================\n\n/**\n * Handles RESEARCH model requests using OpenAI's deep research models.\n *\n * Deep research models can take tens of minutes to complete tasks.\n * Use background mode for long-running tasks.\n *\n * @param runtime - The agent runtime\n * @param params - Research parameters\n * @returns Research result with text, annotations, and output items\n *\n * @example\n * ```typescript\n * const result = await handleResearch(runtime, {\n * input: \"Research the economic impact of AI on global labor markets\",\n * tools: [\n * { type: \"web_search_preview\" },\n * { type: \"code_interpreter\", container: { type: \"auto\" } }\n * ],\n * background: true,\n * });\n * console.log(result.text);\n * ```\n */\nexport async function handleResearch(\n runtime: IAgentRuntime,\n params: ResearchParams\n): Promise<ResearchResult> {\n const apiKey = getApiKey(runtime);\n if (!apiKey) {\n throw new Error(\n \"OPENAI_API_KEY is required for deep research. Set it in your environment variables or runtime settings.\"\n );\n }\n\n const baseURL = getBaseURL(runtime);\n const modelName = params.model ?? getResearchModel(runtime);\n const timeout = getResearchTimeout(runtime);\n\n logger.debug(`[OpenAI] Starting deep research with model: ${modelName}`);\n logger.debug(`[OpenAI] Research input: ${params.input.substring(0, 100)}...`);\n\n // Validate that at least one data source tool is provided\n const dataSourceTools = params.tools?.filter(\n (t) => t.type === \"web_search_preview\" || t.type === \"file_search\" || t.type === \"mcp\"\n );\n\n if (!dataSourceTools || dataSourceTools.length === 0) {\n // Default to web search if no tools specified\n logger.debug(\"[OpenAI] No data source tools specified, defaulting to web_search_preview\");\n params.tools = [{ type: \"web_search_preview\" }, ...(params.tools ?? [])];\n }\n\n // Build the request body for the Responses API\n const requestBody: Record<string, unknown> = {\n model: modelName,\n input: params.input,\n };\n\n if (params.instructions) {\n requestBody.instructions = params.instructions;\n }\n\n if (params.background !== undefined) {\n requestBody.background = params.background;\n }\n\n if (params.tools && params.tools.length > 0) {\n requestBody.tools = params.tools.map(convertToolToApi);\n }\n\n if (params.maxToolCalls !== undefined) {\n requestBody.max_tool_calls = params.maxToolCalls;\n }\n\n if (params.reasoningSummary) {\n requestBody.reasoning = { summary: params.reasoningSummary };\n }\n\n logger.debug(`[OpenAI] Research request body: ${JSON.stringify(requestBody, null, 2)}`);\n\n const details: RecordLlmCallDetails = {\n model: modelName,\n systemPrompt: params.instructions ?? \"\",\n userPrompt: params.input,\n temperature: 0,\n maxTokens: 0,\n purpose: \"external_llm\",\n actionType: \"openai.responses.create\",\n };\n const data = await recordLlmCall(runtime, details, async () => {\n // Make the API request\n const response = await fetch(`${baseURL}/responses`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestBody),\n signal: AbortSignal.timeout(timeout),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(`[OpenAI] Research request failed: ${response.status} ${errorText}`);\n throw new Error(`Deep research request failed: ${response.status} ${response.statusText}`);\n }\n\n const responseData = (await response.json()) as ResponsesApiResponse;\n details.response = responseData.output_text ?? \"\";\n return responseData;\n });\n\n if (data.error) {\n logger.error(`[OpenAI] Research API error: ${data.error.message}`);\n throw new Error(`Deep research error: ${data.error.message}`);\n }\n\n logger.debug(`[OpenAI] Research response received. Status: ${data.status ?? \"completed\"}`);\n\n // Extract text and annotations\n const { text, annotations } = extractTextAndAnnotations(data);\n\n // Convert output items\n const outputItems: ResearchOutputItem[] = [];\n if (data.output) {\n for (const item of data.output) {\n const converted = convertOutputItem(item);\n if (converted) {\n outputItems.push(converted);\n }\n }\n }\n\n const result: ResearchResult = {\n id: data.id,\n text,\n annotations,\n outputItems,\n status: data.status,\n };\n\n logger.info(\n `[OpenAI] Research completed. Text length: ${text.length}, Annotations: ${annotations.length}, Output items: ${outputItems.length}`\n );\n\n return result;\n}\n",
|
|
14
|
-
"/**\n * Text generation model handlers\n *\n * Provides text generation using OpenAI's language models.\n */\n\nimport type {\n GenerateTextParams,\n IAgentRuntime,\n JsonValue,\n ModelTypeName,\n RecordLlmCallDetails,\n} from \"@elizaos/core\";\nimport {\n buildCanonicalSystemPrompt,\n dropDuplicateLeadingSystemMessage,\n logger,\n ModelType,\n normalizeSchemaForCerebras,\n recordLlmCall,\n resolveEffectiveSystemPrompt,\n sanitizeFunctionNameForCerebras,\n} from \"@elizaos/core\";\nimport {\n generateText,\n type JSONSchema7,\n jsonSchema,\n type LanguageModelUsage,\n type ModelMessage,\n Output,\n streamText,\n type ToolChoice,\n type ToolSet,\n type UserContent,\n} from \"ai\";\nimport { createOpenAIClient } from \"../providers\";\nimport type { TextStreamResult, TokenUsage } from \"../types\";\nimport {\n getActionPlannerModel,\n getExperimentalTelemetry,\n getLargeModel,\n getMediumModel,\n getMegaModel,\n getNanoModel,\n getResponseHandlerModel,\n getSmallModel,\n isCerebrasMode,\n} from \"../utils/config\";\nimport { emitModelUsageEvent } from \"../utils/events\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Function to get model name from runtime\n */\ntype ModelNameGetter = (runtime: IAgentRuntime) => string;\n\ntype PromptCacheRetention = \"in_memory\" | \"24h\";\ntype ChatAttachment = {\n data: string | Uint8Array | URL;\n mediaType: string;\n filename?: string;\n};\n\ninterface OpenAIPromptCacheOptions {\n promptCacheKey?: string;\n promptCacheRetention?: PromptCacheRetention;\n}\n\ninterface GenerateTextParamsWithOpenAIOptions\n extends Omit<\n GenerateTextParams,\n \"messages\" | \"tools\" | \"toolChoice\" | \"responseSchema\" | \"providerOptions\"\n > {\n model?: string;\n attachments?: ChatAttachment[];\n messages?: unknown[];\n tools?: unknown;\n toolChoice?: unknown;\n responseSchema?: unknown;\n providerOptions?: Record<string, object | JsonValue> & {\n agentName?: string;\n openai?: OpenAIPromptCacheOptions;\n };\n}\n\ntype NativeOutput = NonNullable<Parameters<typeof generateText<ToolSet>>[0][\"output\"]>;\ntype NativeGenerateTextParams = Parameters<typeof generateText<ToolSet, NativeOutput>>[0];\ntype NativeStreamTextParams = Parameters<typeof streamText<ToolSet, NativeOutput>>[0];\ntype NativePrompt =\n | { prompt: string; messages?: never }\n | { messages: ModelMessage[]; prompt?: never };\ntype NativeTextParams = Omit<NativeGenerateTextParams, \"messages\" | \"prompt\"> &\n Omit<NativeStreamTextParams, \"messages\" | \"prompt\"> &\n NativePrompt & {\n // Re-declared explicitly: TypeScript's `Parameters<typeof generateText>`\n // inference produces an overload-union that drops this field, but the\n // ai SDK's runtime signature accepts it (see ai@6 `CallSettings & Prompt`).\n allowSystemInMessages?: boolean;\n };\ntype NativeProviderOptions = NativeTextParams[\"providerOptions\"];\ntype NativeTelemetrySettings = NativeTextParams[\"experimental_telemetry\"];\n\ntype LanguageModelUsageWithCache = Omit<LanguageModelUsage, \"inputTokenDetails\"> & {\n inputTokenDetails?: LanguageModelUsage[\"inputTokenDetails\"] & {\n cachedInputTokens?: number;\n cacheCreationInputTokens?: number;\n cacheCreationTokens?: number;\n };\n cachedInputTokens?: number;\n cacheReadInputTokens?: number;\n cacheCreationInputTokens?: number;\n cacheWriteInputTokens?: number;\n input_tokens_details?: {\n cached_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n prompt_tokens_details?: {\n cached_tokens?: number;\n };\n};\n\ninterface NativeGenerateTextResult {\n text: string;\n toolCalls?: unknown[];\n finishReason?: string;\n usage?: TokenUsage;\n providerMetadata?: unknown;\n}\n\ntype NativeTextModelResult = string & NativeGenerateTextResult;\n\nconst TEXT_NANO_MODEL_TYPE = ModelType.TEXT_NANO as ModelTypeName;\nconst TEXT_MEDIUM_MODEL_TYPE = ModelType.TEXT_MEDIUM as ModelTypeName;\nconst TEXT_MEGA_MODEL_TYPE = ModelType.TEXT_MEGA as ModelTypeName;\nconst RESPONSE_HANDLER_MODEL_TYPE = ModelType.RESPONSE_HANDLER as ModelTypeName;\nconst ACTION_PLANNER_MODEL_TYPE = ModelType.ACTION_PLANNER as ModelTypeName;\n\nfunction resolveRequestedModelName(\n params: GenerateTextParamsWithOpenAIOptions,\n runtime: IAgentRuntime,\n getModelFn: ModelNameGetter\n): string {\n return typeof params.model === \"string\" && params.model.trim().length > 0\n ? params.model.trim()\n : getModelFn(runtime);\n}\n\nfunction buildUserContent(params: GenerateTextParamsWithOpenAIOptions): UserContent {\n const content: Array<\n | { type: \"text\"; text: string }\n | {\n type: \"file\";\n data: string | Uint8Array | URL;\n mediaType: string;\n filename?: string;\n }\n > = [{ type: \"text\", text: params.prompt ?? \"\" }];\n\n for (const attachment of params.attachments ?? []) {\n content.push({\n type: \"file\",\n data: attachment.data,\n mediaType: attachment.mediaType,\n ...(attachment.filename ? { filename: attachment.filename } : {}),\n });\n }\n\n return content;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Converts AI SDK usage to our token usage format.\n *\n * Emits both the legacy `cachedPromptTokens` (kept for back-compat with\n * existing OpenAI consumers) and the canonical v5 `cacheReadInputTokens`\n * (consumed by the trajectory recorder + cost table). They always carry the\n * same value when the AI SDK reports cached input.\n */\nfunction convertUsage(usage: LanguageModelUsage | undefined): TokenUsage | undefined {\n if (!usage) {\n return undefined;\n }\n\n // The AI SDK uses inputTokens/outputTokens\n const promptTokens = usage.inputTokens ?? 0;\n const completionTokens = usage.outputTokens ?? 0;\n const usageWithCache: LanguageModelUsageWithCache = usage;\n const cachedInput =\n firstNumber(\n usageWithCache.cacheReadInputTokens,\n usageWithCache.cachedInputTokens,\n usageWithCache.inputTokenDetails?.cacheReadTokens,\n usageWithCache.inputTokenDetails?.cachedInputTokens,\n usageWithCache.input_tokens_details?.cache_read_input_tokens,\n usageWithCache.input_tokens_details?.cached_tokens,\n usageWithCache.prompt_tokens_details?.cached_tokens\n ) ?? undefined;\n const cacheCreationInput = firstNumber(\n usageWithCache.cacheCreationInputTokens,\n usageWithCache.cacheWriteInputTokens,\n usageWithCache.inputTokenDetails?.cacheCreationInputTokens,\n usageWithCache.inputTokenDetails?.cacheCreationTokens,\n usageWithCache.inputTokenDetails?.cacheWriteTokens,\n usageWithCache.input_tokens_details?.cache_creation_input_tokens\n );\n\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n cachedPromptTokens: cachedInput,\n cacheReadInputTokens: cachedInput,\n cacheCreationInputTokens: cacheCreationInput,\n };\n}\n\nfunction firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"string\" && value.trim().length > 0) {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) {\n return parsed;\n }\n }\n }\n return undefined;\n}\n\nfunction resolvePromptCacheOptions(params: GenerateTextParams): OpenAIPromptCacheOptions {\n const withOpenAIOptions = params as GenerateTextParamsWithOpenAIOptions;\n return {\n promptCacheKey: withOpenAIOptions.providerOptions?.openai?.promptCacheKey,\n promptCacheRetention: withOpenAIOptions.providerOptions?.openai?.promptCacheRetention,\n };\n}\n\n/**\n * Forward `OPENAI_REASONING_EFFORT` (runtime setting / process.env) as\n * `reasoning_effort` on the outbound chat completions request. This is\n * the OpenAI-spec knob for reasoning-capable models (`o1-*`, `o3-*`,\n * `gpt-oss-*`, `deepseek-r1`, `qwen-3-thinking`, etc.) — including\n * Cerebras and OpenRouter, which honor the same field. `\"low\"` keeps\n * reasoning short enough that visible content always fits inside\n * `max_tokens`, which is the failure mode on Cerebras gpt-oss-120b when\n * left unset.\n *\n * In Cerebras mode the field defaults to `\"low\"` when unset, but ONLY for\n * reasoning-capable models (e.g. gpt-oss-*, deepseek-r1, qwen-3-thinking):\n * gpt-oss-120b emits a separate reasoning channel and, left unbounded, spends\n * the whole token budget reasoning — returning empty visible content, which\n * makes the agent fall back to \"I don't have a reply for that\". `\"low\"` keeps\n * reasoning short so a reply always materializes. Non-reasoning Cerebras models\n * (Llama, etc.) reject `reasoning_effort`, so they must never receive the\n * default. For all other models an unset/invalid value yields `undefined`, so\n * they pay no overhead and the wire stays clean. An explicit valid\n * `OPENAI_REASONING_EFFORT` always wins.\n *\n * Valid values follow the OpenAI spec exactly: `minimal`, `low`,\n * `medium`, `high`. Anything else is logged and ignored.\n */\ntype ReasoningEffort = \"minimal\" | \"low\" | \"medium\" | \"high\";\n\nconst VALID_REASONING_EFFORTS: readonly ReasoningEffort[] = [\"minimal\", \"low\", \"medium\", \"high\"];\n\n/**\n * Reasoning-capable model families that emit a separate reasoning channel and\n * honor `reasoning_effort`. Used to gate the Cerebras `\"low\"` default so\n * non-reasoning models (Llama, etc.) are never sent the field.\n */\nfunction isReasoningModel(modelName: string | undefined): boolean {\n if (!modelName) return false;\n const m = modelName.toLowerCase();\n return (\n m.includes(\"gpt-oss\") ||\n m.includes(\"o1\") ||\n m.includes(\"o3\") ||\n m.includes(\"o4\") ||\n m.includes(\"deepseek-r1\") ||\n m.includes(\"thinking\") ||\n m.includes(\"reasoning\") ||\n m.includes(\"qwq\")\n );\n}\n\nfunction resolveReasoningEffort(\n runtime: IAgentRuntime,\n modelName?: string\n): ReasoningEffort | undefined {\n const raw = runtime.getSetting(\"OPENAI_REASONING_EFFORT\");\n const normalized = typeof raw === \"string\" ? raw.trim().toLowerCase() : \"\";\n if (normalized) {\n if ((VALID_REASONING_EFFORTS as readonly string[]).includes(normalized)) {\n return normalized as ReasoningEffort;\n }\n logger.warn(\n `[OpenAI] OPENAI_REASONING_EFFORT=${raw} is not a valid reasoning effort; ignoring. Expected one of: ${VALID_REASONING_EFFORTS.join(\", \")}.`\n );\n }\n // gpt-oss-120b on Cerebras returns empty content when reasoning runs\n // unbounded; default to \"low\" so a visible reply always fits — but only for\n // reasoning-capable models. Non-reasoning Cerebras models (Llama, etc.)\n // reject `reasoning_effort` and would break. An explicit valid value above\n // wins over this default.\n if (isCerebrasMode(runtime) && isReasoningModel(modelName)) {\n return \"low\";\n }\n return undefined;\n}\n\nfunction resolveProviderOptions(\n params: GenerateTextParams,\n runtime: IAgentRuntime,\n modelName?: string\n): Record<string, unknown> | undefined {\n const withOpenAIOptions = params as GenerateTextParamsWithOpenAIOptions;\n const rawProviderOptions = withOpenAIOptions.providerOptions;\n const promptCacheOptions = resolvePromptCacheOptions(params);\n const reasoningEffort = resolveReasoningEffort(runtime, modelName);\n\n if (\n !rawProviderOptions &&\n !promptCacheOptions.promptCacheKey &&\n !promptCacheOptions.promptCacheRetention &&\n !reasoningEffort\n ) {\n return undefined;\n }\n\n // Cerebras supports prompt caching on gpt-oss-120b — 128-token blocks,\n // default-on. The `prompt_cache_key` field IS accepted by Cerebras's\n // OpenAI-compatible endpoint and surfaces hit counts via\n // `usage.prompt_tokens_details.cached_tokens` (same shape as OpenAI), so\n // we keep it in the request body. Only `prompt_cache_retention` is an\n // OpenAI-direct-only field that Cerebras rejects with HTTP 400\n // (`wrong_api_format`), so we strip just that one when in Cerebras mode.\n const skipCacheRetention = isCerebrasMode(runtime);\n\n const { agentName: _agentName, openai: rawOpenAIOptions, ...rest } = rawProviderOptions ?? {};\n // When on Cerebras, scrub OpenAI-direct-only fields (e.g. `promptCacheRetention`)\n // from `rawOpenAIOptions` before they're spread; otherwise they reach the wire\n // and the Cerebras endpoint rejects with HTTP 400 `wrong_api_format`.\n const sanitizedRawOpenAIOptions = (() => {\n if (!rawOpenAIOptions || typeof rawOpenAIOptions !== \"object\") return rawOpenAIOptions;\n if (!skipCacheRetention) return rawOpenAIOptions;\n const { promptCacheRetention: _drop, ...rest2 } = rawOpenAIOptions as Record<string, unknown>;\n return rest2;\n })();\n const openaiOptions = {\n ...(sanitizedRawOpenAIOptions ?? {}),\n ...(promptCacheOptions.promptCacheKey\n ? { promptCacheKey: promptCacheOptions.promptCacheKey }\n : {}),\n ...(!skipCacheRetention && promptCacheOptions.promptCacheRetention\n ? { promptCacheRetention: promptCacheOptions.promptCacheRetention }\n : {}),\n // The caller's explicit `reasoningEffort` wins over the resolved default\n // (env var, or Cerebras \"low\") — same precedence pattern as promptCacheKey.\n ...((sanitizedRawOpenAIOptions as { reasoningEffort?: unknown } | undefined)\n ?.reasoningEffort === undefined && reasoningEffort\n ? { reasoningEffort }\n : {}),\n };\n\n const providerOptions = {\n ...rest,\n ...(Object.keys(openaiOptions).length > 0 ? { openai: openaiOptions } : {}),\n };\n\n return Object.keys(providerOptions).length > 0 ? providerOptions : undefined;\n}\n\nfunction buildStructuredOutput(responseSchema: unknown): NativeOutput {\n if (\n responseSchema &&\n typeof responseSchema === \"object\" &&\n \"responseFormat\" in responseSchema &&\n \"parseCompleteOutput\" in responseSchema\n ) {\n return responseSchema as NativeOutput;\n }\n\n const schemaOptions =\n responseSchema && typeof responseSchema === \"object\" && \"schema\" in responseSchema\n ? (responseSchema as { schema: unknown; name?: string; description?: string })\n : { schema: responseSchema };\n\n return Output.object({\n schema: jsonSchema(sanitizeJsonSchema(schemaOptions.schema, true)),\n ...(schemaOptions.name ? { name: schemaOptions.name } : {}),\n ...(schemaOptions.description ? { description: schemaOptions.description } : {}),\n }) as NativeOutput;\n}\n\nfunction normalizeNativeTools(\n tools: unknown,\n options: { cerebrasMode?: boolean } = {}\n): ToolSet | undefined {\n if (!tools) {\n return undefined;\n }\n\n // Existing AI SDK callers already pass a ToolSet keyed by tool name. Keep it\n // intact so custom tool instances, execute hooks, and dynamic tool metadata\n // are preserved.\n if (!Array.isArray(tools)) {\n return tools as ToolSet;\n }\n\n const toolSet: Record<string, unknown> = {};\n\n for (const rawTool of tools) {\n const tool = asRecord(rawTool);\n const functionTool = asRecord(tool.function);\n const name = firstString(tool.name, functionTool.name);\n\n if (!name) {\n throw new Error(\"[OpenAI] Native tool definition is missing a name.\");\n }\n\n const description = firstString(tool.description, functionTool.description);\n // Default to a permissive object schema. The empty-properties shape\n // (`{ type: \"object\", properties: {}, additionalProperties: false }`) is\n // accepted by OpenAI but rejected by strict-grammar providers like\n // Cerebras with `Object fields require at least one of: 'properties' or\n // 'anyOf' with a list of possible properties`.\n const rawSchema =\n tool.parameters ?? functionTool.parameters ?? ({ type: \"object\" } satisfies JSONSchema7);\n let inputSchema = sanitizeJsonSchema(rawSchema, true);\n if (options.cerebrasMode) {\n // User-supplied schemas may still contain empty-properties subobjects\n // even after sanitizeJsonSchema. Apply Cerebras-specific normalization\n // recursively so deep schemas are accepted by the grammar compiler.\n // Pass isRoot: true so the top-level invariant is enforced (must be\n // type:\"object\" with no root oneOf/anyOf/enum/not).\n inputSchema = normalizeSchemaForCerebras(inputSchema, true) as JSONSchema7;\n }\n\n // Cerebras's grammar compiler rejects function names containing characters\n // outside `[a-zA-Z0-9_-]` (e.g. `math.factorial`). The AI SDK looks up\n // tools by the registered key, so we register under the sanitized name AND\n // surface it to the model under that name. Tool calls come back with the\n // sanitized name, which the runtime resolves through its action registry —\n // any caller relying on dotted action names should pre-sanitize.\n const registeredName = options.cerebrasMode ? sanitizeFunctionNameForCerebras(name) : name;\n\n toolSet[registeredName] = {\n ...(description ? { description } : {}),\n inputSchema: jsonSchema(inputSchema as JSONSchema7),\n };\n }\n\n return Object.keys(toolSet).length > 0 ? (toolSet as ToolSet) : undefined;\n}\n\nfunction normalizeNativeMessages(messages: unknown): ModelMessage[] | undefined {\n if (!Array.isArray(messages)) {\n return undefined;\n }\n\n return messages.map((message) => normalizeNativeMessage(message));\n}\n\nfunction normalizeNativeMessage(message: unknown): ModelMessage {\n const raw = asRecord(message);\n const providerOptions = asOptionalRecord(raw.providerOptions);\n\n if (raw.role === \"system\") {\n return {\n role: \"system\",\n content: stringifyMessageContent(raw.content),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n if (raw.role === \"assistant\") {\n return {\n role: \"assistant\",\n content: normalizeAssistantContent(raw),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n if (raw.role === \"tool\") {\n return {\n role: \"tool\",\n content: normalizeToolContent(raw),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n return {\n role: \"user\",\n content: normalizeUserContent(raw.content),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n}\n\n/**\n * Strip reasoning-only parts from outbound assistant content.\n *\n * OpenAI-spec reasoning models (Cerebras gpt-oss-120b, OpenAI o1/o3,\n * DeepSeek R1, Qwen-3-thinking, etc.) return reasoning in the assistant\n * response — either as a separate `reasoning` / `reasoning_content`\n * field, or as content parts with `type: \"reasoning\"`. Echoing those\n * back to the next turn is wrong on both ends:\n * - Cerebras returns HTTP 400 (`messages.X.assistant.reasoning_content:\n * property is unsupported`).\n * - OpenAI silently drops them, which wastes prompt tokens.\n *\n * The AI SDK upstream of this normalizer surfaces those reasoning blocks\n * as `{ type: \"reasoning\", ... }` content parts. We drop them here so\n * the wire stays spec-clean for the next turn. The reasoning itself\n * remains usable as a single-turn signal (still on the response object);\n * we only refuse to round-trip it.\n */\nfunction stripReasoningParts(content: unknown[]): unknown[] {\n return content.filter((part) => {\n if (!part || typeof part !== \"object\") return true;\n const type = (part as { type?: unknown }).type;\n return type !== \"reasoning\" && type !== \"thinking\";\n });\n}\n\nfunction normalizeAssistantContent(message: Record<string, unknown>): unknown {\n const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];\n\n if (toolCalls.length === 0) {\n if (Array.isArray(message.content)) {\n return stripReasoningParts(message.content);\n }\n if (typeof message.content === \"string\") {\n return message.content;\n }\n return \"\";\n }\n\n const parts: unknown[] = [];\n if (typeof message.content === \"string\" && message.content.length > 0) {\n parts.push({ type: \"text\", text: message.content });\n } else if (Array.isArray(message.content)) {\n parts.push(...stripReasoningParts(message.content));\n }\n\n for (const toolCall of toolCalls) {\n const rawCall = asRecord(toolCall);\n const rawFunction = asRecord(rawCall.function);\n const toolCallId = firstString(rawCall.toolCallId, rawCall.id);\n const toolName = firstString(rawCall.toolName, rawCall.name, rawFunction.name);\n\n if (!toolCallId || !toolName) {\n continue;\n }\n\n parts.push({\n type: \"tool-call\",\n toolCallId,\n toolName,\n input: parseToolCallInput(rawCall, rawFunction),\n });\n }\n\n return parts;\n}\n\nfunction normalizeToolContent(message: Record<string, unknown>): unknown[] {\n if (Array.isArray(message.content)) {\n return message.content;\n }\n\n const toolCallId = firstString(message.toolCallId, message.id) ?? \"tool-call\";\n const toolName = firstString(message.toolName, message.name) ?? \"tool\";\n const parsed = parseJsonIfPossible(message.content);\n\n return [\n {\n type: \"tool-result\",\n toolCallId,\n toolName,\n output:\n typeof parsed === \"string\"\n ? { type: \"text\", value: parsed }\n : { type: \"json\", value: parsed },\n },\n ];\n}\n\nfunction normalizeUserContent(content: unknown): UserContent {\n if (Array.isArray(content)) {\n return content as UserContent;\n }\n return stringifyMessageContent(content);\n}\n\nfunction stringifyMessageContent(content: unknown): string {\n if (typeof content === \"string\") {\n return content;\n }\n if (content == null) {\n return \"\";\n }\n return typeof content === \"object\" ? JSON.stringify(content) : String(content);\n}\n\nfunction parseToolCallInput(\n rawCall: Record<string, unknown>,\n rawFunction: Record<string, unknown>\n): unknown {\n if (\"input\" in rawCall) {\n return rawCall.input;\n }\n return parseJsonIfPossible(rawCall.arguments ?? rawFunction.arguments ?? {});\n}\n\nfunction parseJsonIfPossible(value: unknown): unknown {\n if (typeof value !== \"string\") {\n return value ?? \"\";\n }\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeToolChoice(toolChoice: unknown): ToolChoice<ToolSet> | undefined {\n if (!toolChoice) {\n return undefined;\n }\n\n if (\n typeof toolChoice === \"string\" &&\n (toolChoice === \"auto\" || toolChoice === \"none\" || toolChoice === \"required\")\n ) {\n return toolChoice;\n }\n\n const choice = asRecord(toolChoice);\n if (choice.type === \"tool\") {\n if (typeof choice.toolName === \"string\" && choice.toolName.length > 0) {\n return toolChoice as ToolChoice<ToolSet>;\n }\n const toolName = firstString(choice.toolName, choice.name);\n if (toolName) {\n return { type: \"tool\", toolName };\n }\n }\n\n if (choice.type === \"function\") {\n const fn = asRecord(choice.function);\n const toolName = firstString(fn.name);\n if (toolName) {\n return { type: \"tool\", toolName };\n }\n }\n\n const namedTool = firstString(choice.name);\n if (namedTool) {\n return { type: \"tool\", toolName: namedTool };\n }\n\n return toolChoice as ToolChoice<ToolSet>;\n}\n\nfunction hasIllegalStrictRoot(node: Record<string, unknown>): boolean {\n // Strict-mode JSON schema validators on OpenAI-compatible providers (Groq,\n // Cerebras, OpenAI strict tools) reject tool-parameters whose top level is\n // not `type: \"object\"` or carries `oneOf`/`anyOf`/`enum`/`not` at the root.\n // The error wording varies by provider but the constraint is uniform.\n if (node.type !== \"object\") return true;\n if (Array.isArray(node.oneOf) && node.oneOf.length > 0) return true;\n if (Array.isArray(node.anyOf) && node.anyOf.length > 0) return true;\n if (Array.isArray(node.enum)) return true;\n if (node.not !== undefined) return true;\n return false;\n}\n\nfunction sanitizeJsonSchema(schema: unknown, isRoot = false): JSONSchema7 {\n if (!schema || typeof schema !== \"object\" || Array.isArray(schema)) {\n // Permissive fallback: no `properties: {}`/`additionalProperties: false`\n // pair, which strict-grammar providers reject. See `normalizeSchemaForCerebras`\n // in @elizaos/core for the rationale.\n return { type: \"object\" };\n }\n\n const record = schema as Record<string, unknown>;\n let sanitized: Record<string, unknown> = { ...record };\n\n if (typeof sanitized.type !== \"string\") {\n const inferredType = inferJsonSchemaType(sanitized, isRoot);\n if (inferredType) {\n sanitized.type = inferredType;\n }\n }\n\n if (isRoot && hasIllegalStrictRoot(sanitized)) {\n // Wrap the original schema under properties.value. Strict-tool callers\n // that unwrap arguments will see `{ value: <original> }`. The recursion\n // below normalises the wrapped child like any other property.\n sanitized = {\n type: \"object\",\n properties: { value: { ...record } },\n required: [\"value\"],\n additionalProperties: false,\n };\n }\n\n if (\n sanitized.properties &&\n typeof sanitized.properties === \"object\" &&\n !Array.isArray(sanitized.properties)\n ) {\n const properties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitized.properties as Record<string, unknown>)) {\n properties[key] = sanitizeJsonSchema(value);\n }\n sanitized.properties = properties;\n\n const propertyKeys = Object.keys(properties);\n const existingRequired = Array.isArray(sanitized.required)\n ? sanitized.required.filter((key): key is string => typeof key === \"string\")\n : [];\n sanitized.required = [...new Set([...existingRequired, ...propertyKeys])];\n }\n\n if (sanitized.type === \"object\" && sanitized.additionalProperties !== false) {\n sanitized.additionalProperties = false;\n }\n\n if (sanitized.items) {\n sanitized.items = Array.isArray(sanitized.items)\n ? sanitized.items.map((item) => sanitizeJsonSchema(item))\n : sanitizeJsonSchema(sanitized.items);\n }\n\n for (const unionKey of [\"anyOf\", \"oneOf\", \"allOf\"] as const) {\n const value = sanitized[unionKey];\n if (Array.isArray(value)) {\n sanitized[unionKey] = value.map((item) => sanitizeJsonSchema(item));\n }\n }\n\n return sanitized as JSONSchema7;\n}\n\nfunction inferJsonSchemaType(schema: Record<string, unknown>, isRoot: boolean): string | undefined {\n if (\n \"properties\" in schema ||\n \"required\" in schema ||\n \"additionalProperties\" in schema ||\n isRoot\n ) {\n return \"object\";\n }\n if (\"items\" in schema) {\n return \"array\";\n }\n if (Array.isArray(schema.enum) && schema.enum.length > 0) {\n const types = new Set(schema.enum.map((value) => typeof value));\n if (types.size === 1) {\n const [type] = [...types];\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n return type;\n }\n }\n }\n return undefined;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction asOptionalRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n }\n return undefined;\n}\n\nfunction usesNativeTextResult(params: GenerateTextParamsWithOpenAIOptions): boolean {\n return Boolean(params.messages || params.tools || params.toolChoice || params.responseSchema);\n}\n\nfunction buildNativeTextResult(\n result: {\n text: string;\n toolCalls?: unknown[];\n finishReason?: string;\n usage?: LanguageModelUsage;\n providerMetadata?: unknown;\n },\n modelName?: string\n): NativeGenerateTextResult {\n return {\n text: result.text,\n toolCalls: result.toolCalls ?? [],\n finishReason: result.finishReason,\n usage: convertUsage(result.usage),\n providerMetadata: mergeProviderModelName(result.providerMetadata, modelName),\n };\n}\n\nfunction handledPromise<T>(value: T | PromiseLike<T>): Promise<T> {\n const promise = Promise.resolve(value);\n promise.catch(() => {\n // The streaming path primarily consumes `textStream`. AI SDK companion\n // promises such as `text` can reject later on empty streams even when no\n // caller requested them, which otherwise surfaces as an unhandled rejection.\n });\n return promise;\n}\n\nfunction handledMappedPromise<T, U>(\n value: T | PromiseLike<T>,\n mapper: (resolved: T) => U | PromiseLike<U>\n): Promise<U> {\n return handledPromise(handledPromise(value).then(mapper));\n}\n\nfunction mergeProviderModelName(providerMetadata: unknown, modelName?: string): unknown {\n if (!modelName) {\n return providerMetadata;\n }\n if (\n providerMetadata &&\n typeof providerMetadata === \"object\" &&\n !Array.isArray(providerMetadata)\n ) {\n return {\n ...(providerMetadata as Record<string, unknown>),\n modelName,\n };\n }\n return { modelName };\n}\n\nfunction createLlmCallDetails(\n modelName: string,\n params: GenerateTextParams,\n systemPrompt: string | undefined,\n actionType: string,\n modelType?: ModelTypeName,\n providerOptions?: Record<string, unknown>,\n generateParams?: NativeTextParams\n): RecordLlmCallDetails {\n const originalParams = params as GenerateTextParamsWithOpenAIOptions;\n const nativeParams = generateParams as\n | (NativeTextParams & {\n output?: unknown;\n maxOutputTokens?: unknown;\n })\n | undefined;\n const nativePrompt = nativeParams && \"prompt\" in nativeParams ? nativeParams.prompt : undefined;\n const nativeMessages =\n nativeParams && \"messages\" in nativeParams && Array.isArray(nativeParams.messages)\n ? nativeParams.messages\n : undefined;\n const nativeSystem =\n typeof nativeParams?.system === \"string\" ? nativeParams.system : systemPrompt;\n return {\n model: modelName,\n modelType,\n provider: \"vercel-ai-sdk\",\n systemPrompt: nativeSystem ?? \"\",\n userPrompt:\n typeof nativePrompt === \"string\"\n ? nativePrompt\n : typeof params.prompt === \"string\"\n ? params.prompt\n : \"\",\n prompt: typeof nativePrompt === \"string\" ? nativePrompt : undefined,\n messages: nativeMessages,\n tools: nativeParams?.tools ?? originalParams.tools,\n toolChoice: nativeParams?.toolChoice ?? originalParams.toolChoice,\n output:\n nativeParams?.output !== undefined\n ? buildTrajectoryOutputDescriptor(originalParams.responseSchema, nativeParams.output)\n : undefined,\n responseSchema: originalParams.responseSchema,\n providerOptions:\n providerOptions ?? nativeParams?.providerOptions ?? originalParams.providerOptions,\n temperature: params.temperature ?? 0,\n maxTokens:\n typeof nativeParams?.maxOutputTokens === \"number\"\n ? nativeParams.maxOutputTokens\n : params.omitMaxTokens\n ? 0\n : (params.maxTokens ?? 8192),\n maxTokensOmitted:\n params.omitMaxTokens && typeof nativeParams?.maxOutputTokens !== \"number\" ? true : undefined,\n purpose: \"external_llm\",\n actionType,\n };\n}\n\nfunction buildTrajectoryOutputDescriptor(responseSchema: unknown, output: unknown): unknown {\n if (responseSchema !== undefined) {\n return {\n type: \"object\",\n schema: responseSchema,\n };\n }\n return toTrajectoryJsonSafe(output);\n}\n\nfunction toTrajectoryJsonSafe(value: unknown): unknown {\n try {\n return JSON.parse(\n JSON.stringify(value, (_key, nested) => {\n if (typeof nested === \"function\") return undefined;\n if (typeof nested === \"bigint\") return nested.toString();\n return nested;\n })\n ) as unknown;\n } catch {\n return String(value);\n }\n}\n\nfunction applyUsageToDetails(\n details: RecordLlmCallDetails,\n usage: LanguageModelUsage | undefined\n): void {\n if (!usage) {\n return;\n }\n details.promptTokens = usage.inputTokens ?? 0;\n details.completionTokens = usage.outputTokens ?? 0;\n}\n\n// ============================================================================\n// Core Generation Function\n// ============================================================================\n\n/**\n * Generates text using the specified model type.\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @param modelType - The type of model (TEXT_SMALL or TEXT_LARGE)\n * @param getModelFn - Function to get the model name\n * @returns Generated text or stream result\n */\nasync function generateTextByModelType(\n runtime: IAgentRuntime,\n params: GenerateTextParams,\n modelType: ModelTypeName,\n getModelFn: ModelNameGetter\n): Promise<string | TextStreamResult> {\n const paramsWithAttachments = params as GenerateTextParamsWithOpenAIOptions;\n const openai = createOpenAIClient(runtime);\n const modelName = resolveRequestedModelName(paramsWithAttachments, runtime, getModelFn);\n\n logger.debug(`[OpenAI] Using ${modelType} model: ${modelName}`);\n const providerOptions = resolveProviderOptions(params, runtime, modelName);\n const hasAttachments = (paramsWithAttachments.attachments?.length ?? 0) > 0;\n const userContent = hasAttachments ? buildUserContent(paramsWithAttachments) : undefined;\n const shouldReturnNativeResult = usesNativeTextResult(paramsWithAttachments);\n\n const systemPrompt = resolveEffectiveSystemPrompt({\n params: paramsWithAttachments,\n fallback: buildCanonicalSystemPrompt({ character: runtime.character }),\n });\n const agentName = paramsWithAttachments.providerOptions?.agentName;\n const telemetryConfig: NativeTelemetrySettings = {\n isEnabled: getExperimentalTelemetry(runtime),\n functionId: agentName ? `agent:${agentName}` : undefined,\n metadata: agentName ? { agentName } : undefined,\n };\n\n // Chat Completions is the default: broadest compatibility, and it works\n // against every OpenAI-compatible endpoint (Cerebras, local servers, proxies).\n // gpt-5 / gpt-5-mini reasoning models ignore temperature/penalty/stop params.\n //\n const model = openai.chat(modelName);\n const cerebrasMode = isCerebrasMode(runtime);\n const normalizedTools = normalizeNativeTools(paramsWithAttachments.tools, {\n cerebrasMode,\n });\n const normalizedToolChoice = normalizeToolChoice(paramsWithAttachments.toolChoice);\n const normalizedMessages = normalizeNativeMessages(paramsWithAttachments.messages);\n const wireMessages = dropDuplicateLeadingSystemMessage(normalizedMessages, systemPrompt);\n const effectiveMessages =\n wireMessages && wireMessages.length > 0 ? wireMessages : normalizedMessages;\n const promptText =\n typeof params.prompt === \"string\" && params.prompt.length > 0 ? params.prompt : \"\";\n const promptOrMessages: NativePrompt =\n effectiveMessages && effectiveMessages.length > 0\n ? { messages: effectiveMessages }\n : userContent\n ? { messages: [{ role: \"user\" as const, content: userContent }] }\n : { prompt: promptText };\n // elizaOS callers pass `responseFormat: { type: \"json_object\" | \"text\" }`\n // (see `GenerateTextParams` in @elizaos/core). The AI SDK's equivalent\n // is `responseFormat: { type: \"json\" }` (which translates to\n // `response_format: { type: \"json_object\" }` at the OpenAI wire layer).\n // Translate the shape so the param actually reaches the API call —\n // before this, callers asking for json_object were silently ignored\n // and Cerebras returned plain text, dropping us into the simple-reply\n // fallback every turn.\n const callerResponseFormat = (paramsWithAttachments as { responseFormat?: unknown })\n .responseFormat;\n const responseFormatType =\n typeof callerResponseFormat === \"string\"\n ? callerResponseFormat\n : callerResponseFormat &&\n typeof callerResponseFormat === \"object\" &&\n \"type\" in callerResponseFormat\n ? (callerResponseFormat as { type: string }).type\n : undefined;\n const wireResponseFormat: { type: \"json\" } | { type: \"text\" } | undefined =\n responseFormatType === \"json_object\"\n ? { type: \"json\" }\n : responseFormatType === \"text\"\n ? { type: \"text\" }\n : undefined;\n\n const generateParams: NativeTextParams = {\n model,\n ...promptOrMessages,\n system: systemPrompt,\n allowSystemInMessages: true,\n // Omit the cap when the caller opted out (direct-channel Stage-1) so the\n // model's own max applies — a hardcoded value 400s when it exceeds the\n // model's limit. Other callers keep the 8192 default.\n ...(params.omitMaxTokens ? {} : { maxOutputTokens: params.maxTokens ?? 8192 }),\n experimental_telemetry: telemetryConfig,\n ...(normalizedTools ? { tools: normalizedTools } : {}),\n ...(normalizedToolChoice ? { toolChoice: normalizedToolChoice } : {}),\n // Cerebras's OpenAI-compatible endpoint does not accept the\n // `response_format: { type: \"json_schema\", ... }` payload that the AI SDK\n // emits when `output: Output.object(...)` is set. Fall back to relying on\n // `responseFormat: { type: \"json_object\" }` (already passed by callers)\n // plus the schema embedded in the prompt body.\n ...(paramsWithAttachments.responseSchema && !isCerebrasMode(runtime)\n ? { output: buildStructuredOutput(paramsWithAttachments.responseSchema) }\n : {}),\n ...(wireResponseFormat ? { responseFormat: wireResponseFormat } : {}),\n ...(providerOptions ? { providerOptions: providerOptions as NativeProviderOptions } : {}),\n };\n\n // Handle streaming mode\n if (params.stream) {\n const details = createLlmCallDetails(\n modelName,\n params,\n systemPrompt,\n \"ai.streamText\",\n modelType,\n providerOptions,\n generateParams\n );\n details.response = \"\";\n const result = await recordLlmCall(runtime, details, () => streamText(generateParams));\n\n return {\n textStream: (async function* textStreamWithCallback() {\n for await (const chunk of result.textStream) {\n params.onStreamChunk?.(chunk);\n yield chunk;\n }\n })(),\n text: handledPromise(result.text),\n ...(shouldReturnNativeResult ? { toolCalls: handledPromise(result.toolCalls) } : {}),\n usage: handledMappedPromise(result.usage, convertUsage),\n finishReason: handledMappedPromise(result.finishReason, (r) => r as string | undefined),\n };\n }\n\n // Non-streaming mode\n const details = createLlmCallDetails(\n modelName,\n params,\n systemPrompt,\n \"ai.generateText\",\n modelType,\n providerOptions,\n generateParams\n );\n const result = await recordLlmCall(runtime, details, async () => {\n const result = await generateText(generateParams);\n details.response = result.text;\n details.toolCalls = result.toolCalls;\n details.finishReason = result.finishReason as string | undefined;\n details.providerMetadata = result.providerMetadata;\n applyUsageToDetails(details, result.usage);\n return result;\n });\n\n if (result.usage) {\n emitModelUsageEvent(runtime, modelType, params.prompt ?? \"\", result.usage);\n }\n\n if (shouldReturnNativeResult) {\n return buildNativeTextResult(result, modelName) as NativeTextModelResult;\n }\n\n return result.text;\n}\n\n// ============================================================================\n// Public Handlers\n// ============================================================================\n\n/**\n * Handles TEXT_SMALL model requests.\n *\n * Uses the configured small model (default: gpt-5-mini).\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @returns Generated text or stream result\n */\nexport async function handleTextSmall(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ModelType.TEXT_SMALL, getSmallModel);\n}\n\nexport async function handleTextNano(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_NANO_MODEL_TYPE, getNanoModel);\n}\n\nexport async function handleTextMedium(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_MEDIUM_MODEL_TYPE, getMediumModel);\n}\n\n/**\n * Handles TEXT_LARGE model requests.\n *\n * Uses the configured large model (default: gpt-5).\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @returns Generated text or stream result\n */\nexport async function handleTextLarge(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ModelType.TEXT_LARGE, getLargeModel);\n}\n\nexport async function handleTextMega(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_MEGA_MODEL_TYPE, getMegaModel);\n}\n\nexport async function handleResponseHandler(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(\n runtime,\n params,\n RESPONSE_HANDLER_MODEL_TYPE,\n getResponseHandlerModel\n );\n}\n\nexport async function handleActionPlanner(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ACTION_PLANNER_MODEL_TYPE, getActionPlannerModel);\n}\n\n// ─── Test-only exports ──────────────────────────────────────────────────────\n// These are exported for the shape tests in `__tests__/reasoning-effort.shape.test.ts`.\n// Not part of the public API; do not import outside tests.\n\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_resolveProviderOptions = resolveProviderOptions;\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_normalizeNativeMessages = normalizeNativeMessages;\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_stripReasoningParts = stripReasoningParts;\n",
|
|
14
|
+
"/**\n * Text generation model handlers\n *\n * Provides text generation using OpenAI's language models.\n */\n\nimport type {\n GenerateTextParams,\n IAgentRuntime,\n JsonValue,\n ModelTypeName,\n RecordLlmCallDetails,\n} from \"@elizaos/core\";\nimport {\n buildCanonicalSystemPrompt,\n dropDuplicateLeadingSystemMessage,\n logger,\n ModelType,\n normalizeSchemaForCerebras,\n recordLlmCall,\n resolveEffectiveSystemPrompt,\n sanitizeFunctionNameForCerebras,\n} from \"@elizaos/core\";\nimport {\n generateText,\n type JSONSchema7,\n jsonSchema,\n type LanguageModelUsage,\n type ModelMessage,\n Output,\n streamText,\n type ToolChoice,\n type ToolSet,\n type UserContent,\n} from \"ai\";\nimport { createOpenAIClient } from \"../providers\";\nimport type { TextStreamResult, TokenUsage } from \"../types\";\nimport {\n getActionPlannerModel,\n getExperimentalTelemetry,\n getLargeModel,\n getMediumModel,\n getMegaModel,\n getNanoModel,\n getResponseHandlerModel,\n getSmallModel,\n isCerebrasMode,\n} from \"../utils/config\";\nimport { emitModelUsageEvent } from \"../utils/events\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Function to get model name from runtime\n */\ntype ModelNameGetter = (runtime: IAgentRuntime) => string;\n\ntype PromptCacheRetention = \"in_memory\" | \"24h\";\ntype ChatAttachment = {\n data: string | Uint8Array | URL;\n mediaType: string;\n filename?: string;\n};\n\ninterface OpenAIPromptCacheOptions {\n promptCacheKey?: string;\n promptCacheRetention?: PromptCacheRetention;\n}\n\ninterface GenerateTextParamsWithOpenAIOptions\n extends Omit<\n GenerateTextParams,\n \"messages\" | \"tools\" | \"toolChoice\" | \"responseSchema\" | \"providerOptions\"\n > {\n model?: string;\n attachments?: ChatAttachment[];\n messages?: unknown[];\n tools?: unknown;\n toolChoice?: unknown;\n responseSchema?: unknown;\n providerOptions?: Record<string, object | JsonValue> & {\n agentName?: string;\n openai?: OpenAIPromptCacheOptions;\n };\n}\n\ntype NativeOutput = NonNullable<Parameters<typeof generateText<ToolSet>>[0][\"output\"]>;\ntype NativeGenerateTextParams = Parameters<typeof generateText<ToolSet, NativeOutput>>[0];\ntype NativeStreamTextParams = Parameters<typeof streamText<ToolSet, NativeOutput>>[0];\ntype NativePrompt =\n | { prompt: string; messages?: never }\n | { messages: ModelMessage[]; prompt?: never };\ntype NativeTextParams = Omit<NativeGenerateTextParams, \"messages\" | \"prompt\"> &\n Omit<NativeStreamTextParams, \"messages\" | \"prompt\"> &\n NativePrompt & {\n // Re-declared explicitly: TypeScript's `Parameters<typeof generateText>`\n // inference produces an overload-union that drops this field, but the\n // ai SDK's runtime signature accepts it (see ai@6 `CallSettings & Prompt`).\n allowSystemInMessages?: boolean;\n };\ntype NativeProviderOptions = NativeTextParams[\"providerOptions\"];\ntype NativeTelemetrySettings = NativeTextParams[\"experimental_telemetry\"];\n\ntype LanguageModelUsageWithCache = Omit<LanguageModelUsage, \"inputTokenDetails\"> & {\n inputTokenDetails?: LanguageModelUsage[\"inputTokenDetails\"] & {\n cachedInputTokens?: number;\n cacheCreationInputTokens?: number;\n cacheCreationTokens?: number;\n };\n cachedInputTokens?: number;\n cacheReadInputTokens?: number;\n cacheCreationInputTokens?: number;\n cacheWriteInputTokens?: number;\n input_tokens_details?: {\n cached_tokens?: number;\n cache_read_input_tokens?: number;\n cache_creation_input_tokens?: number;\n };\n prompt_tokens_details?: {\n cached_tokens?: number;\n };\n};\n\ninterface NativeGenerateTextResult {\n text: string;\n toolCalls?: unknown[];\n finishReason?: string;\n usage?: TokenUsage;\n providerMetadata?: unknown;\n}\n\ntype NativeTextModelResult = string & NativeGenerateTextResult;\n\nconst TEXT_NANO_MODEL_TYPE = ModelType.TEXT_NANO as ModelTypeName;\nconst TEXT_MEDIUM_MODEL_TYPE = ModelType.TEXT_MEDIUM as ModelTypeName;\nconst TEXT_MEGA_MODEL_TYPE = ModelType.TEXT_MEGA as ModelTypeName;\nconst RESPONSE_HANDLER_MODEL_TYPE = ModelType.RESPONSE_HANDLER as ModelTypeName;\nconst ACTION_PLANNER_MODEL_TYPE = ModelType.ACTION_PLANNER as ModelTypeName;\n\nfunction resolveRequestedModelName(\n params: GenerateTextParamsWithOpenAIOptions,\n runtime: IAgentRuntime,\n getModelFn: ModelNameGetter\n): string {\n return typeof params.model === \"string\" && params.model.trim().length > 0\n ? params.model.trim()\n : getModelFn(runtime);\n}\n\nfunction buildUserContent(params: GenerateTextParamsWithOpenAIOptions): UserContent {\n const content: Array<\n | { type: \"text\"; text: string }\n | {\n type: \"file\";\n data: string | Uint8Array | URL;\n mediaType: string;\n filename?: string;\n }\n > = [{ type: \"text\", text: params.prompt ?? \"\" }];\n\n for (const attachment of params.attachments ?? []) {\n content.push({\n type: \"file\",\n data: attachment.data,\n mediaType: attachment.mediaType,\n ...(attachment.filename ? { filename: attachment.filename } : {}),\n });\n }\n\n return content;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Converts AI SDK usage to our token usage format.\n *\n * Emits both the legacy `cachedPromptTokens` (kept for back-compat with\n * existing OpenAI consumers) and the canonical v5 `cacheReadInputTokens`\n * (consumed by the trajectory recorder + cost table). They always carry the\n * same value when the AI SDK reports cached input.\n */\nfunction convertUsage(usage: LanguageModelUsage | undefined): TokenUsage | undefined {\n if (!usage) {\n return undefined;\n }\n\n // The AI SDK uses inputTokens/outputTokens\n const promptTokens = usage.inputTokens ?? 0;\n const completionTokens = usage.outputTokens ?? 0;\n const usageWithCache: LanguageModelUsageWithCache = usage;\n const cachedInput =\n firstNumber(\n usageWithCache.cacheReadInputTokens,\n usageWithCache.cachedInputTokens,\n usageWithCache.inputTokenDetails?.cacheReadTokens,\n usageWithCache.inputTokenDetails?.cachedInputTokens,\n usageWithCache.input_tokens_details?.cache_read_input_tokens,\n usageWithCache.input_tokens_details?.cached_tokens,\n usageWithCache.prompt_tokens_details?.cached_tokens\n ) ?? undefined;\n const cacheCreationInput = firstNumber(\n usageWithCache.cacheCreationInputTokens,\n usageWithCache.cacheWriteInputTokens,\n usageWithCache.inputTokenDetails?.cacheCreationInputTokens,\n usageWithCache.inputTokenDetails?.cacheCreationTokens,\n usageWithCache.inputTokenDetails?.cacheWriteTokens,\n usageWithCache.input_tokens_details?.cache_creation_input_tokens\n );\n\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n cachedPromptTokens: cachedInput,\n cacheReadInputTokens: cachedInput,\n cacheCreationInputTokens: cacheCreationInput,\n };\n}\n\nfunction firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"string\" && value.trim().length > 0) {\n const parsed = Number(value);\n if (Number.isFinite(parsed)) {\n return parsed;\n }\n }\n }\n return undefined;\n}\n\nfunction resolvePromptCacheOptions(params: GenerateTextParams): OpenAIPromptCacheOptions {\n const withOpenAIOptions = params as GenerateTextParamsWithOpenAIOptions;\n return {\n promptCacheKey: withOpenAIOptions.providerOptions?.openai?.promptCacheKey,\n promptCacheRetention: withOpenAIOptions.providerOptions?.openai?.promptCacheRetention,\n };\n}\n\n/**\n * Forward `OPENAI_REASONING_EFFORT` (runtime setting / process.env) as\n * `reasoning_effort` on the outbound chat completions request. This is\n * the OpenAI-spec knob for reasoning-capable models (`o1-*`, `o3-*`,\n * `gpt-oss-*`, `deepseek-r1`, and similar families) — including\n * Cerebras and OpenRouter, which honor the same field. `\"low\"` keeps\n * reasoning short enough that visible content always fits inside\n * `max_tokens`, which is the failure mode on Cerebras gpt-oss-120b when\n * left unset.\n *\n * In Cerebras mode the field defaults to `\"low\"` when unset, but ONLY for\n * reasoning-capable models (e.g. gpt-oss-* and deepseek-r1):\n * gpt-oss-120b emits a separate reasoning channel and, left unbounded, spends\n * the whole token budget reasoning — returning empty visible content, which\n * makes the agent fall back to \"I don't have a reply for that\". `\"low\"` keeps\n * reasoning short so a reply always materializes. Non-reasoning Cerebras models\n * (Llama, etc.) reject `reasoning_effort`, so they must never receive the\n * default. For all other models an unset/invalid value yields `undefined`, so\n * they pay no overhead and the wire stays clean. An explicit valid\n * `OPENAI_REASONING_EFFORT` always wins.\n *\n * Valid values follow the OpenAI spec exactly: `minimal`, `low`,\n * `medium`, `high`. Anything else is logged and ignored.\n */\ntype ReasoningEffort = \"minimal\" | \"low\" | \"medium\" | \"high\";\n\nconst VALID_REASONING_EFFORTS: readonly ReasoningEffort[] = [\"minimal\", \"low\", \"medium\", \"high\"];\n\n/**\n * Reasoning-capable model families that emit a separate reasoning channel and\n * honor `reasoning_effort`. Used to gate the Cerebras `\"low\"` default so\n * non-reasoning models (Llama, etc.) are never sent the field.\n */\nfunction isReasoningModel(modelName: string | undefined): boolean {\n if (!modelName) return false;\n const m = modelName.toLowerCase();\n return (\n m.includes(\"gpt-oss\") ||\n m.includes(\"o1\") ||\n m.includes(\"o3\") ||\n m.includes(\"o4\") ||\n m.includes(\"deepseek-r1\") ||\n m.includes(\"thinking\") ||\n m.includes(\"reasoning\") ||\n m.includes(\"qwq\")\n );\n}\n\nfunction resolveReasoningEffort(\n runtime: IAgentRuntime,\n modelName?: string\n): ReasoningEffort | undefined {\n const raw = runtime.getSetting(\"OPENAI_REASONING_EFFORT\");\n const normalized = typeof raw === \"string\" ? raw.trim().toLowerCase() : \"\";\n if (normalized) {\n if ((VALID_REASONING_EFFORTS as readonly string[]).includes(normalized)) {\n return normalized as ReasoningEffort;\n }\n logger.warn(\n `[OpenAI] OPENAI_REASONING_EFFORT=${raw} is not a valid reasoning effort; ignoring. Expected one of: ${VALID_REASONING_EFFORTS.join(\", \")}.`\n );\n }\n // gpt-oss-120b on Cerebras returns empty content when reasoning runs\n // unbounded; default to \"low\" so a visible reply always fits — but only for\n // reasoning-capable models. Non-reasoning Cerebras models (Llama, etc.)\n // reject `reasoning_effort` and would break. An explicit valid value above\n // wins over this default.\n if (isCerebrasMode(runtime) && isReasoningModel(modelName)) {\n return \"low\";\n }\n return undefined;\n}\n\nfunction resolveProviderOptions(\n params: GenerateTextParams,\n runtime: IAgentRuntime,\n modelName?: string\n): Record<string, unknown> | undefined {\n const withOpenAIOptions = params as GenerateTextParamsWithOpenAIOptions;\n const rawProviderOptions = withOpenAIOptions.providerOptions;\n const promptCacheOptions = resolvePromptCacheOptions(params);\n const reasoningEffort = resolveReasoningEffort(runtime, modelName);\n\n if (\n !rawProviderOptions &&\n !promptCacheOptions.promptCacheKey &&\n !promptCacheOptions.promptCacheRetention &&\n !reasoningEffort\n ) {\n return undefined;\n }\n\n // Cerebras supports prompt caching on gpt-oss-120b — 128-token blocks,\n // default-on. The `prompt_cache_key` field IS accepted by Cerebras's\n // OpenAI-compatible endpoint and surfaces hit counts via\n // `usage.prompt_tokens_details.cached_tokens` (same shape as OpenAI), so\n // we keep it in the request body. Only `prompt_cache_retention` is an\n // OpenAI-direct-only field that Cerebras rejects with HTTP 400\n // (`wrong_api_format`), so we strip just that one when in Cerebras mode.\n const skipCacheRetention = isCerebrasMode(runtime);\n\n const { agentName: _agentName, openai: rawOpenAIOptions, ...rest } = rawProviderOptions ?? {};\n // When on Cerebras, scrub OpenAI-direct-only fields (e.g. `promptCacheRetention`)\n // from `rawOpenAIOptions` before they're spread; otherwise they reach the wire\n // and the Cerebras endpoint rejects with HTTP 400 `wrong_api_format`.\n const sanitizedRawOpenAIOptions = (() => {\n if (!rawOpenAIOptions || typeof rawOpenAIOptions !== \"object\") return rawOpenAIOptions;\n if (!skipCacheRetention) return rawOpenAIOptions;\n const { promptCacheRetention: _drop, ...rest2 } = rawOpenAIOptions as Record<string, unknown>;\n return rest2;\n })();\n const openaiOptions = {\n ...(sanitizedRawOpenAIOptions ?? {}),\n ...(promptCacheOptions.promptCacheKey\n ? { promptCacheKey: promptCacheOptions.promptCacheKey }\n : {}),\n ...(!skipCacheRetention && promptCacheOptions.promptCacheRetention\n ? { promptCacheRetention: promptCacheOptions.promptCacheRetention }\n : {}),\n // The caller's explicit `reasoningEffort` wins over the resolved default\n // (env var, or Cerebras \"low\") — same precedence pattern as promptCacheKey.\n ...((sanitizedRawOpenAIOptions as { reasoningEffort?: unknown } | undefined)\n ?.reasoningEffort === undefined && reasoningEffort\n ? { reasoningEffort }\n : {}),\n };\n\n const providerOptions = {\n ...rest,\n ...(Object.keys(openaiOptions).length > 0 ? { openai: openaiOptions } : {}),\n };\n\n return Object.keys(providerOptions).length > 0 ? providerOptions : undefined;\n}\n\nfunction buildStructuredOutput(responseSchema: unknown): NativeOutput {\n if (\n responseSchema &&\n typeof responseSchema === \"object\" &&\n \"responseFormat\" in responseSchema &&\n \"parseCompleteOutput\" in responseSchema\n ) {\n return responseSchema as NativeOutput;\n }\n\n const schemaOptions =\n responseSchema && typeof responseSchema === \"object\" && \"schema\" in responseSchema\n ? (responseSchema as { schema: unknown; name?: string; description?: string })\n : { schema: responseSchema };\n\n return Output.object({\n schema: jsonSchema(sanitizeJsonSchema(schemaOptions.schema, true)),\n ...(schemaOptions.name ? { name: schemaOptions.name } : {}),\n ...(schemaOptions.description ? { description: schemaOptions.description } : {}),\n }) as NativeOutput;\n}\n\nfunction normalizeNativeTools(\n tools: unknown,\n options: { cerebrasMode?: boolean } = {}\n): ToolSet | undefined {\n if (!tools) {\n return undefined;\n }\n\n // Existing AI SDK callers already pass a ToolSet keyed by tool name. Keep it\n // intact so custom tool instances, execute hooks, and dynamic tool metadata\n // are preserved.\n if (!Array.isArray(tools)) {\n return tools as ToolSet;\n }\n\n const toolSet: Record<string, unknown> = {};\n\n for (const rawTool of tools) {\n const tool = asRecord(rawTool);\n const functionTool = asRecord(tool.function);\n const name = firstString(tool.name, functionTool.name);\n\n if (!name) {\n throw new Error(\"[OpenAI] Native tool definition is missing a name.\");\n }\n\n const description = firstString(tool.description, functionTool.description);\n // Default to a permissive object schema. The empty-properties shape\n // (`{ type: \"object\", properties: {}, additionalProperties: false }`) is\n // accepted by OpenAI but rejected by strict-grammar providers like\n // Cerebras with `Object fields require at least one of: 'properties' or\n // 'anyOf' with a list of possible properties`.\n const rawSchema =\n tool.parameters ?? functionTool.parameters ?? ({ type: \"object\" } satisfies JSONSchema7);\n let inputSchema = sanitizeJsonSchema(rawSchema, true);\n if (options.cerebrasMode) {\n // User-supplied schemas may still contain empty-properties subobjects\n // even after sanitizeJsonSchema. Apply Cerebras-specific normalization\n // recursively so deep schemas are accepted by the grammar compiler.\n // Pass isRoot: true so the top-level invariant is enforced (must be\n // type:\"object\" with no root oneOf/anyOf/enum/not).\n inputSchema = normalizeSchemaForCerebras(inputSchema, true) as JSONSchema7;\n }\n\n // Cerebras's grammar compiler rejects function names containing characters\n // outside `[a-zA-Z0-9_-]` (e.g. `math.factorial`). The AI SDK looks up\n // tools by the registered key, so we register under the sanitized name AND\n // surface it to the model under that name. Tool calls come back with the\n // sanitized name, which the runtime resolves through its action registry —\n // any caller relying on dotted action names should pre-sanitize.\n const registeredName = options.cerebrasMode ? sanitizeFunctionNameForCerebras(name) : name;\n\n toolSet[registeredName] = {\n ...(description ? { description } : {}),\n inputSchema: jsonSchema(inputSchema as JSONSchema7),\n };\n }\n\n return Object.keys(toolSet).length > 0 ? (toolSet as ToolSet) : undefined;\n}\n\nfunction normalizeNativeMessages(messages: unknown): ModelMessage[] | undefined {\n if (!Array.isArray(messages)) {\n return undefined;\n }\n\n return messages.map((message) => normalizeNativeMessage(message));\n}\n\nfunction normalizeNativeMessage(message: unknown): ModelMessage {\n const raw = asRecord(message);\n const providerOptions = asOptionalRecord(raw.providerOptions);\n\n if (raw.role === \"system\") {\n return {\n role: \"system\",\n content: stringifyMessageContent(raw.content),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n if (raw.role === \"assistant\") {\n return {\n role: \"assistant\",\n content: normalizeAssistantContent(raw),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n if (raw.role === \"tool\") {\n return {\n role: \"tool\",\n content: normalizeToolContent(raw),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n }\n\n return {\n role: \"user\",\n content: normalizeUserContent(raw.content),\n ...(providerOptions ? { providerOptions } : {}),\n } as ModelMessage;\n}\n\n/**\n * Strip reasoning-only parts from outbound assistant content.\n *\n * OpenAI-spec reasoning models (Cerebras gpt-oss-120b, OpenAI o1/o3,\n * DeepSeek R1, and similar families) return reasoning in the assistant\n * response — either as a separate `reasoning` / `reasoning_content`\n * field, or as content parts with `type: \"reasoning\"`. Echoing those\n * back to the next turn is wrong on both ends:\n * - Cerebras returns HTTP 400 (`messages.X.assistant.reasoning_content:\n * property is unsupported`).\n * - OpenAI silently drops them, which wastes prompt tokens.\n *\n * The AI SDK upstream of this normalizer surfaces those reasoning blocks\n * as `{ type: \"reasoning\", ... }` content parts. We drop them here so\n * the wire stays spec-clean for the next turn. The reasoning itself\n * remains usable as a single-turn signal (still on the response object);\n * we only refuse to round-trip it.\n */\nfunction stripReasoningParts(content: unknown[]): unknown[] {\n return content.filter((part) => {\n if (!part || typeof part !== \"object\") return true;\n const type = (part as { type?: unknown }).type;\n return type !== \"reasoning\" && type !== \"thinking\";\n });\n}\n\nfunction normalizeAssistantContent(message: Record<string, unknown>): unknown {\n const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];\n\n if (toolCalls.length === 0) {\n if (Array.isArray(message.content)) {\n return stripReasoningParts(message.content);\n }\n if (typeof message.content === \"string\") {\n return message.content;\n }\n return \"\";\n }\n\n const parts: unknown[] = [];\n if (typeof message.content === \"string\" && message.content.length > 0) {\n parts.push({ type: \"text\", text: message.content });\n } else if (Array.isArray(message.content)) {\n parts.push(...stripReasoningParts(message.content));\n }\n\n for (const toolCall of toolCalls) {\n const rawCall = asRecord(toolCall);\n const rawFunction = asRecord(rawCall.function);\n const toolCallId = firstString(rawCall.toolCallId, rawCall.id);\n const toolName = firstString(rawCall.toolName, rawCall.name, rawFunction.name);\n\n if (!toolCallId || !toolName) {\n continue;\n }\n\n parts.push({\n type: \"tool-call\",\n toolCallId,\n toolName,\n input: parseToolCallInput(rawCall, rawFunction),\n });\n }\n\n return parts;\n}\n\nfunction normalizeToolContent(message: Record<string, unknown>): unknown[] {\n if (Array.isArray(message.content)) {\n return message.content;\n }\n\n const toolCallId = firstString(message.toolCallId, message.id) ?? \"tool-call\";\n const toolName = firstString(message.toolName, message.name) ?? \"tool\";\n const parsed = parseJsonIfPossible(message.content);\n\n return [\n {\n type: \"tool-result\",\n toolCallId,\n toolName,\n output:\n typeof parsed === \"string\"\n ? { type: \"text\", value: parsed }\n : { type: \"json\", value: parsed },\n },\n ];\n}\n\nfunction normalizeUserContent(content: unknown): UserContent {\n if (Array.isArray(content)) {\n return content as UserContent;\n }\n return stringifyMessageContent(content);\n}\n\nfunction stringifyMessageContent(content: unknown): string {\n if (typeof content === \"string\") {\n return content;\n }\n if (content == null) {\n return \"\";\n }\n return typeof content === \"object\" ? JSON.stringify(content) : String(content);\n}\n\nfunction parseToolCallInput(\n rawCall: Record<string, unknown>,\n rawFunction: Record<string, unknown>\n): unknown {\n if (\"input\" in rawCall) {\n return rawCall.input;\n }\n return parseJsonIfPossible(rawCall.arguments ?? rawFunction.arguments ?? {});\n}\n\nfunction parseJsonIfPossible(value: unknown): unknown {\n if (typeof value !== \"string\") {\n return value ?? \"\";\n }\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n}\n\nfunction normalizeToolChoice(toolChoice: unknown): ToolChoice<ToolSet> | undefined {\n if (!toolChoice) {\n return undefined;\n }\n\n if (\n typeof toolChoice === \"string\" &&\n (toolChoice === \"auto\" || toolChoice === \"none\" || toolChoice === \"required\")\n ) {\n return toolChoice;\n }\n\n const choice = asRecord(toolChoice);\n if (choice.type === \"tool\") {\n if (typeof choice.toolName === \"string\" && choice.toolName.length > 0) {\n return toolChoice as ToolChoice<ToolSet>;\n }\n const toolName = firstString(choice.toolName, choice.name);\n if (toolName) {\n return { type: \"tool\", toolName };\n }\n }\n\n if (choice.type === \"function\") {\n const fn = asRecord(choice.function);\n const toolName = firstString(fn.name);\n if (toolName) {\n return { type: \"tool\", toolName };\n }\n }\n\n const namedTool = firstString(choice.name);\n if (namedTool) {\n return { type: \"tool\", toolName: namedTool };\n }\n\n return toolChoice as ToolChoice<ToolSet>;\n}\n\nfunction hasIllegalStrictRoot(node: Record<string, unknown>): boolean {\n // Strict-mode JSON schema validators on OpenAI-compatible providers (Groq,\n // Cerebras, OpenAI strict tools) reject tool-parameters whose top level is\n // not `type: \"object\"` or carries `oneOf`/`anyOf`/`enum`/`not` at the root.\n // The error wording varies by provider but the constraint is uniform.\n if (node.type !== \"object\") return true;\n if (Array.isArray(node.oneOf) && node.oneOf.length > 0) return true;\n if (Array.isArray(node.anyOf) && node.anyOf.length > 0) return true;\n if (Array.isArray(node.enum)) return true;\n if (node.not !== undefined) return true;\n return false;\n}\n\nfunction sanitizeJsonSchema(schema: unknown, isRoot = false): JSONSchema7 {\n if (!schema || typeof schema !== \"object\" || Array.isArray(schema)) {\n // Permissive fallback: no `properties: {}`/`additionalProperties: false`\n // pair, which strict-grammar providers reject. See `normalizeSchemaForCerebras`\n // in @elizaos/core for the rationale.\n return { type: \"object\" };\n }\n\n const record = schema as Record<string, unknown>;\n let sanitized: Record<string, unknown> = { ...record };\n\n if (typeof sanitized.type !== \"string\") {\n const inferredType = inferJsonSchemaType(sanitized, isRoot);\n if (inferredType) {\n sanitized.type = inferredType;\n }\n }\n\n if (isRoot && hasIllegalStrictRoot(sanitized)) {\n // Wrap the original schema under properties.value. Strict-tool callers\n // that unwrap arguments will see `{ value: <original> }`. The recursion\n // below normalises the wrapped child like any other property.\n sanitized = {\n type: \"object\",\n properties: { value: { ...record } },\n required: [\"value\"],\n additionalProperties: false,\n };\n }\n\n if (\n sanitized.properties &&\n typeof sanitized.properties === \"object\" &&\n !Array.isArray(sanitized.properties)\n ) {\n const properties: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(sanitized.properties as Record<string, unknown>)) {\n properties[key] = sanitizeJsonSchema(value);\n }\n sanitized.properties = properties;\n\n const propertyKeys = Object.keys(properties);\n const existingRequired = Array.isArray(sanitized.required)\n ? sanitized.required.filter((key): key is string => typeof key === \"string\")\n : [];\n sanitized.required = [...new Set([...existingRequired, ...propertyKeys])];\n }\n\n if (sanitized.type === \"object\" && sanitized.additionalProperties !== false) {\n sanitized.additionalProperties = false;\n }\n\n if (sanitized.items) {\n sanitized.items = Array.isArray(sanitized.items)\n ? sanitized.items.map((item) => sanitizeJsonSchema(item))\n : sanitizeJsonSchema(sanitized.items);\n }\n\n for (const unionKey of [\"anyOf\", \"oneOf\", \"allOf\"] as const) {\n const value = sanitized[unionKey];\n if (Array.isArray(value)) {\n sanitized[unionKey] = value.map((item) => sanitizeJsonSchema(item));\n }\n }\n\n return sanitized as JSONSchema7;\n}\n\nfunction inferJsonSchemaType(schema: Record<string, unknown>, isRoot: boolean): string | undefined {\n if (\n \"properties\" in schema ||\n \"required\" in schema ||\n \"additionalProperties\" in schema ||\n isRoot\n ) {\n return \"object\";\n }\n if (\"items\" in schema) {\n return \"array\";\n }\n if (Array.isArray(schema.enum) && schema.enum.length > 0) {\n const types = new Set(schema.enum.map((value) => typeof value));\n if (types.size === 1) {\n const [type] = [...types];\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n return type;\n }\n }\n }\n return undefined;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction asOptionalRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n }\n return undefined;\n}\n\nfunction usesNativeTextResult(params: GenerateTextParamsWithOpenAIOptions): boolean {\n return Boolean(params.messages || params.tools || params.toolChoice || params.responseSchema);\n}\n\nfunction buildNativeTextResult(\n result: {\n text: string;\n toolCalls?: unknown[];\n finishReason?: string;\n usage?: LanguageModelUsage;\n providerMetadata?: unknown;\n },\n modelName?: string\n): NativeGenerateTextResult {\n return {\n text: result.text,\n toolCalls: result.toolCalls ?? [],\n finishReason: result.finishReason,\n usage: convertUsage(result.usage),\n providerMetadata: mergeProviderModelName(result.providerMetadata, modelName),\n };\n}\n\nfunction handledPromise<T>(value: T | PromiseLike<T>): Promise<T> {\n const promise = Promise.resolve(value);\n promise.catch(() => {\n // The streaming path primarily consumes `textStream`. AI SDK companion\n // promises such as `text` can reject later on empty streams even when no\n // caller requested them, which otherwise surfaces as an unhandled rejection.\n });\n return promise;\n}\n\nfunction handledMappedPromise<T, U>(\n value: T | PromiseLike<T>,\n mapper: (resolved: T) => U | PromiseLike<U>\n): Promise<U> {\n return handledPromise(handledPromise(value).then(mapper));\n}\n\nfunction mergeProviderModelName(providerMetadata: unknown, modelName?: string): unknown {\n if (!modelName) {\n return providerMetadata;\n }\n if (\n providerMetadata &&\n typeof providerMetadata === \"object\" &&\n !Array.isArray(providerMetadata)\n ) {\n return {\n ...(providerMetadata as Record<string, unknown>),\n modelName,\n };\n }\n return { modelName };\n}\n\nfunction createLlmCallDetails(\n modelName: string,\n params: GenerateTextParams,\n systemPrompt: string | undefined,\n actionType: string,\n modelType?: ModelTypeName,\n providerOptions?: Record<string, unknown>,\n generateParams?: NativeTextParams\n): RecordLlmCallDetails {\n const originalParams = params as GenerateTextParamsWithOpenAIOptions;\n const nativeParams = generateParams as\n | (NativeTextParams & {\n output?: unknown;\n maxOutputTokens?: unknown;\n })\n | undefined;\n const nativePrompt = nativeParams && \"prompt\" in nativeParams ? nativeParams.prompt : undefined;\n const nativeMessages =\n nativeParams && \"messages\" in nativeParams && Array.isArray(nativeParams.messages)\n ? nativeParams.messages\n : undefined;\n const nativeSystem =\n typeof nativeParams?.system === \"string\" ? nativeParams.system : systemPrompt;\n return {\n model: modelName,\n modelType,\n provider: \"vercel-ai-sdk\",\n systemPrompt: nativeSystem ?? \"\",\n userPrompt:\n typeof nativePrompt === \"string\"\n ? nativePrompt\n : typeof params.prompt === \"string\"\n ? params.prompt\n : \"\",\n prompt: typeof nativePrompt === \"string\" ? nativePrompt : undefined,\n messages: nativeMessages,\n tools: nativeParams?.tools ?? originalParams.tools,\n toolChoice: nativeParams?.toolChoice ?? originalParams.toolChoice,\n output:\n nativeParams?.output !== undefined\n ? buildTrajectoryOutputDescriptor(originalParams.responseSchema, nativeParams.output)\n : undefined,\n responseSchema: originalParams.responseSchema,\n providerOptions:\n providerOptions ?? nativeParams?.providerOptions ?? originalParams.providerOptions,\n temperature: params.temperature ?? 0,\n maxTokens:\n typeof nativeParams?.maxOutputTokens === \"number\"\n ? nativeParams.maxOutputTokens\n : params.omitMaxTokens\n ? 0\n : (params.maxTokens ?? 8192),\n maxTokensOmitted:\n params.omitMaxTokens && typeof nativeParams?.maxOutputTokens !== \"number\" ? true : undefined,\n purpose: \"external_llm\",\n actionType,\n };\n}\n\nfunction buildTrajectoryOutputDescriptor(responseSchema: unknown, output: unknown): unknown {\n if (responseSchema !== undefined) {\n return {\n type: \"object\",\n schema: responseSchema,\n };\n }\n return toTrajectoryJsonSafe(output);\n}\n\nfunction toTrajectoryJsonSafe(value: unknown): unknown {\n try {\n return JSON.parse(\n JSON.stringify(value, (_key, nested) => {\n if (typeof nested === \"function\") return undefined;\n if (typeof nested === \"bigint\") return nested.toString();\n return nested;\n })\n ) as unknown;\n } catch {\n return String(value);\n }\n}\n\nfunction applyUsageToDetails(\n details: RecordLlmCallDetails,\n usage: LanguageModelUsage | undefined\n): void {\n if (!usage) {\n return;\n }\n details.promptTokens = usage.inputTokens ?? 0;\n details.completionTokens = usage.outputTokens ?? 0;\n}\n\n// ============================================================================\n// Core Generation Function\n// ============================================================================\n\n/**\n * Generates text using the specified model type.\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @param modelType - The type of model (TEXT_SMALL or TEXT_LARGE)\n * @param getModelFn - Function to get the model name\n * @returns Generated text or stream result\n */\nasync function generateTextByModelType(\n runtime: IAgentRuntime,\n params: GenerateTextParams,\n modelType: ModelTypeName,\n getModelFn: ModelNameGetter\n): Promise<string | TextStreamResult> {\n const paramsWithAttachments = params as GenerateTextParamsWithOpenAIOptions;\n const openai = createOpenAIClient(runtime);\n const modelName = resolveRequestedModelName(paramsWithAttachments, runtime, getModelFn);\n\n logger.debug(`[OpenAI] Using ${modelType} model: ${modelName}`);\n const providerOptions = resolveProviderOptions(params, runtime, modelName);\n const hasAttachments = (paramsWithAttachments.attachments?.length ?? 0) > 0;\n const userContent = hasAttachments ? buildUserContent(paramsWithAttachments) : undefined;\n const shouldReturnNativeResult = usesNativeTextResult(paramsWithAttachments);\n\n const systemPrompt = resolveEffectiveSystemPrompt({\n params: paramsWithAttachments,\n fallback: buildCanonicalSystemPrompt({ character: runtime.character }),\n });\n const agentName = paramsWithAttachments.providerOptions?.agentName;\n const telemetryConfig: NativeTelemetrySettings = {\n isEnabled: getExperimentalTelemetry(runtime),\n functionId: agentName ? `agent:${agentName}` : undefined,\n metadata: agentName ? { agentName } : undefined,\n };\n\n // Chat Completions is the default: broadest compatibility, and it works\n // against every OpenAI-compatible endpoint (Cerebras, local servers, proxies).\n // gpt-5 / gpt-5-mini reasoning models ignore temperature/penalty/stop params.\n //\n const model = openai.chat(modelName);\n const cerebrasMode = isCerebrasMode(runtime);\n const normalizedTools = normalizeNativeTools(paramsWithAttachments.tools, {\n cerebrasMode,\n });\n const normalizedToolChoice = normalizeToolChoice(paramsWithAttachments.toolChoice);\n const normalizedMessages = normalizeNativeMessages(paramsWithAttachments.messages);\n const wireMessages = dropDuplicateLeadingSystemMessage(normalizedMessages, systemPrompt);\n const effectiveMessages =\n wireMessages && wireMessages.length > 0 ? wireMessages : normalizedMessages;\n const promptText =\n typeof params.prompt === \"string\" && params.prompt.length > 0 ? params.prompt : \"\";\n const promptOrMessages: NativePrompt =\n effectiveMessages && effectiveMessages.length > 0\n ? { messages: effectiveMessages }\n : userContent\n ? { messages: [{ role: \"user\" as const, content: userContent }] }\n : { prompt: promptText };\n // elizaOS callers pass `responseFormat: { type: \"json_object\" | \"text\" }`\n // (see `GenerateTextParams` in @elizaos/core). The AI SDK's equivalent\n // is `responseFormat: { type: \"json\" }` (which translates to\n // `response_format: { type: \"json_object\" }` at the OpenAI wire layer).\n // Translate the shape so the param actually reaches the API call —\n // before this, callers asking for json_object were silently ignored\n // and Cerebras returned plain text, dropping us into the simple-reply\n // fallback every turn.\n const callerResponseFormat = (paramsWithAttachments as { responseFormat?: unknown })\n .responseFormat;\n const responseFormatType =\n typeof callerResponseFormat === \"string\"\n ? callerResponseFormat\n : callerResponseFormat &&\n typeof callerResponseFormat === \"object\" &&\n \"type\" in callerResponseFormat\n ? (callerResponseFormat as { type: string }).type\n : undefined;\n const wireResponseFormat: { type: \"json\" } | { type: \"text\" } | undefined =\n responseFormatType === \"json_object\"\n ? { type: \"json\" }\n : responseFormatType === \"text\"\n ? { type: \"text\" }\n : undefined;\n\n const generateParams: NativeTextParams = {\n model,\n ...promptOrMessages,\n system: systemPrompt,\n allowSystemInMessages: true,\n // Omit the cap when the caller opted out (direct-channel Stage-1) so the\n // model's own max applies — a hardcoded value 400s when it exceeds the\n // model's limit. Other callers keep the 8192 default.\n ...(params.omitMaxTokens ? {} : { maxOutputTokens: params.maxTokens ?? 8192 }),\n experimental_telemetry: telemetryConfig,\n ...(normalizedTools ? { tools: normalizedTools } : {}),\n ...(normalizedToolChoice ? { toolChoice: normalizedToolChoice } : {}),\n // Cerebras's OpenAI-compatible endpoint does not accept the\n // `response_format: { type: \"json_schema\", ... }` payload that the AI SDK\n // emits when `output: Output.object(...)` is set. Fall back to relying on\n // `responseFormat: { type: \"json_object\" }` (already passed by callers)\n // plus the schema embedded in the prompt body.\n ...(paramsWithAttachments.responseSchema && !isCerebrasMode(runtime)\n ? { output: buildStructuredOutput(paramsWithAttachments.responseSchema) }\n : {}),\n ...(wireResponseFormat ? { responseFormat: wireResponseFormat } : {}),\n ...(providerOptions ? { providerOptions: providerOptions as NativeProviderOptions } : {}),\n };\n\n // Handle streaming mode\n if (params.stream) {\n const details = createLlmCallDetails(\n modelName,\n params,\n systemPrompt,\n \"ai.streamText\",\n modelType,\n providerOptions,\n generateParams\n );\n details.response = \"\";\n const result = await recordLlmCall(runtime, details, () => streamText(generateParams));\n\n return {\n textStream: (async function* textStreamWithCallback() {\n for await (const chunk of result.textStream) {\n params.onStreamChunk?.(chunk);\n yield chunk;\n }\n })(),\n text: handledPromise(result.text),\n ...(shouldReturnNativeResult ? { toolCalls: handledPromise(result.toolCalls) } : {}),\n usage: handledMappedPromise(result.usage, convertUsage),\n finishReason: handledMappedPromise(result.finishReason, (r) => r as string | undefined),\n };\n }\n\n // Non-streaming mode\n const details = createLlmCallDetails(\n modelName,\n params,\n systemPrompt,\n \"ai.generateText\",\n modelType,\n providerOptions,\n generateParams\n );\n const result = await recordLlmCall(runtime, details, async () => {\n const result = await generateText(generateParams);\n details.response = result.text;\n details.toolCalls = result.toolCalls;\n details.finishReason = result.finishReason as string | undefined;\n details.providerMetadata = result.providerMetadata;\n applyUsageToDetails(details, result.usage);\n return result;\n });\n\n if (result.usage) {\n emitModelUsageEvent(runtime, modelType, params.prompt ?? \"\", result.usage);\n }\n\n if (shouldReturnNativeResult) {\n return buildNativeTextResult(result, modelName) as NativeTextModelResult;\n }\n\n return result.text;\n}\n\n// ============================================================================\n// Public Handlers\n// ============================================================================\n\n/**\n * Handles TEXT_SMALL model requests.\n *\n * Uses the configured small model (default: gpt-5-mini).\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @returns Generated text or stream result\n */\nexport async function handleTextSmall(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ModelType.TEXT_SMALL, getSmallModel);\n}\n\nexport async function handleTextNano(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_NANO_MODEL_TYPE, getNanoModel);\n}\n\nexport async function handleTextMedium(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_MEDIUM_MODEL_TYPE, getMediumModel);\n}\n\n/**\n * Handles TEXT_LARGE model requests.\n *\n * Uses the configured large model (default: gpt-5).\n *\n * @param runtime - The agent runtime\n * @param params - Generation parameters\n * @returns Generated text or stream result\n */\nexport async function handleTextLarge(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ModelType.TEXT_LARGE, getLargeModel);\n}\n\nexport async function handleTextMega(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, TEXT_MEGA_MODEL_TYPE, getMegaModel);\n}\n\nexport async function handleResponseHandler(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(\n runtime,\n params,\n RESPONSE_HANDLER_MODEL_TYPE,\n getResponseHandlerModel\n );\n}\n\nexport async function handleActionPlanner(\n runtime: IAgentRuntime,\n params: GenerateTextParams\n): Promise<string | TextStreamResult> {\n return generateTextByModelType(runtime, params, ACTION_PLANNER_MODEL_TYPE, getActionPlannerModel);\n}\n\n// ─── Test-only exports ──────────────────────────────────────────────────────\n// These are exported for the shape tests in `__tests__/reasoning-effort.shape.test.ts`.\n// Not part of the public API; do not import outside tests.\n\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_resolveProviderOptions = resolveProviderOptions;\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_normalizeNativeMessages = normalizeNativeMessages;\n/** @internal — exported for unit tests only. */\nexport const __INTERNAL_stripReasoningParts = stripReasoningParts;\n",
|
|
15
15
|
"import { createOpenAI, type OpenAIProvider } from \"@ai-sdk/openai\";\nimport type { IAgentRuntime } from \"@elizaos/core\";\nimport { getApiKey, getBaseURL, isProxyMode } from \"../utils/config\";\n\nconst PROXY_API_KEY = \"sk-proxy\";\n\nexport function createOpenAIClient(runtime: IAgentRuntime): OpenAIProvider {\n const baseURL = getBaseURL(runtime);\n const apiKey = getApiKey(runtime);\n\n if (!apiKey && isProxyMode(runtime)) {\n return createOpenAI({\n apiKey: PROXY_API_KEY,\n baseURL,\n });\n }\n\n if (!apiKey) {\n throw new Error(\n \"OPENAI_API_KEY is required. Set it in your environment variables or runtime settings.\"\n );\n }\n\n return createOpenAI({\n apiKey,\n baseURL,\n });\n}\n",
|
|
16
16
|
"import type { IAgentRuntime, ModelTypeName } from \"@elizaos/core\";\nimport { ModelType } from \"@elizaos/core\";\nimport {\n encodingForModel,\n getEncoding,\n type Tiktoken,\n type TiktokenEncoding,\n type TiktokenModel,\n} from \"js-tiktoken\";\nimport { getLargeModel, getSmallModel } from \"./config\";\n\ntype SupportedEncoding = \"cl100k_base\" | \"o200k_base\";\n\nfunction resolveTokenizerEncoding(modelName: string): Tiktoken {\n const normalized = modelName.toLowerCase();\n const fallbackEncoding: SupportedEncoding = normalized.includes(\"4o\")\n ? \"o200k_base\"\n : \"cl100k_base\";\n try {\n return encodingForModel(modelName as TiktokenModel);\n } catch {\n return getEncoding(fallbackEncoding as TiktokenEncoding);\n }\n}\n\nfunction getModelName(runtime: IAgentRuntime, modelType: ModelTypeName): string {\n if (modelType === ModelType.TEXT_SMALL) {\n return getSmallModel(runtime);\n }\n return getLargeModel(runtime);\n}\n\nexport function tokenizeText(\n runtime: IAgentRuntime,\n modelType: ModelTypeName,\n text: string\n): number[] {\n const modelName = getModelName(runtime, modelType);\n const encoder = resolveTokenizerEncoding(modelName);\n return encoder.encode(text);\n}\n\nexport function detokenizeText(\n runtime: IAgentRuntime,\n modelType: ModelTypeName,\n tokens: number[]\n): string {\n const modelName = getModelName(runtime, modelType);\n const encoder = resolveTokenizerEncoding(modelName);\n return encoder.decode(tokens);\n}\n\nexport function countTokens(\n runtime: IAgentRuntime,\n modelType: ModelTypeName,\n text: string\n): number {\n const tokens = tokenizeText(runtime, modelType, text);\n return tokens.length;\n}\n\nexport function truncateToTokenLimit(\n runtime: IAgentRuntime,\n modelType: ModelTypeName,\n text: string,\n maxTokens: number\n): string {\n const tokens = tokenizeText(runtime, modelType, text);\n if (tokens.length <= maxTokens) {\n return text;\n }\n const truncatedTokens = tokens.slice(0, maxTokens);\n return detokenizeText(runtime, modelType, truncatedTokens);\n}\n",
|
|
17
17
|
"import type { DetokenizeTextParams, IAgentRuntime, TokenizeTextParams } from \"@elizaos/core\";\nimport { detokenizeText, tokenizeText } from \"../utils/tokenization\";\n\nexport async function handleTokenizerEncode(\n runtime: IAgentRuntime,\n params: TokenizeTextParams\n): Promise<number[]> {\n if (!params.prompt) {\n throw new Error(\"Tokenization requires a non-empty prompt\");\n }\n const modelType = params.modelType;\n return tokenizeText(runtime, modelType, params.prompt);\n}\n\nexport async function handleTokenizerDecode(\n runtime: IAgentRuntime,\n params: DetokenizeTextParams\n): Promise<string> {\n if (!params.tokens || !Array.isArray(params.tokens)) {\n throw new Error(\"Detokenization requires a valid tokens array\");\n }\n if (params.tokens.length === 0) {\n return \"\";\n }\n for (let i = 0; i < params.tokens.length; i++) {\n const token = params.tokens[i];\n if (typeof token !== \"number\" || !Number.isFinite(token)) {\n throw new Error(`Invalid token at index ${i}: expected number`);\n }\n }\n const modelType = params.modelType;\n return detokenizeText(runtime, modelType, params.tokens);\n}\n",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elizaos/plugin-openai",
|
|
3
|
-
"version": "2.0.3-beta.
|
|
3
|
+
"version": "2.0.3-beta.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/node/index.node.js",
|
|
6
6
|
"module": "dist/node/index.node.js",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@biomejs/biome": "^2.4.14",
|
|
63
|
-
"@elizaos/core": "2.0.3-beta.
|
|
64
|
-
"@elizaos/test-harness": "2.0.3-beta.
|
|
63
|
+
"@elizaos/core": "2.0.3-beta.5",
|
|
64
|
+
"@elizaos/test-harness": "2.0.3-beta.5",
|
|
65
65
|
"@types/bun": "^1.3.8",
|
|
66
66
|
"@types/json-schema": "^7.0.15",
|
|
67
67
|
"@types/node": "^25.0.3",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"vitest": "^4.0.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@elizaos/core": "2.0.3-beta.
|
|
73
|
+
"@elizaos/core": "2.0.3-beta.5",
|
|
74
74
|
"zod": "^4.4.3"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"dev": "bun --hot build.ts",
|
|
79
79
|
"lint": "bunx @biomejs/biome check --write --unsafe .",
|
|
80
80
|
"lint:check": "bunx @biomejs/biome check .",
|
|
81
|
-
"clean": "rm
|
|
81
|
+
"clean": "node ../../packages/scripts/rm-path-recursive.mjs dist .turbo",
|
|
82
82
|
"format": "bunx @biomejs/biome format --write .",
|
|
83
83
|
"format:check": "bunx @biomejs/biome format .",
|
|
84
84
|
"typecheck": "tsgo --noEmit",
|
|
@@ -245,7 +245,7 @@
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
},
|
|
248
|
-
"gitHead": "
|
|
248
|
+
"gitHead": "ff6157011c9459670021cc28a6797592a78b8817",
|
|
249
249
|
"eliza": {
|
|
250
250
|
"platforms": [
|
|
251
251
|
"browser",
|