import { A as getEnvString, E as state, O as getEnvFloat, k as getEnvInt, o as logger } from "./logger-CfDSdx1q.js";
import { A as parseChatPrompt, M as transformToolChoice, N as transformTools, g as formatRateLimitErrorMessage, p as HttpRateLimitError, w as getRequestTimeoutMs } from "./fetch-CHx36OdV.js";
import { r as importModule } from "./esm-B1AS3f6u.js";
import { Mt as withGenAISpan, _t as parseFileUrl, dt as maybeLoadToolsFromExternalFile, lt as maybeLoadFromExternalFileWithVars, ut as maybeLoadResponseFormatFromExternalFile, yt as renderVarsInObject } from "./util-DqNaBNjn.js";
import { r as fetchWithCache } from "./cache-IpWZkF7H.js";
import { d as validateFunctionCall, r as OPENAI_CHAT_MODELS, u as getTokenUsage } from "./util-BkKdKgf7.js";
import { n as calculateOpenAIUsageCost } from "./billing-Bz85gwea.js";
import { i as getMcpErrorMessage, s as isMcpErrorResult } from "./util-C1ik7aZ7.js";
import { t as MCPClient } from "./client-kU_wEA_c.js";
import { i as transformMCPToolsToOpenAi } from "./transform-DJmC4Vxn.js";
import { t as OpenAiGenericProvider } from "./openai-BbztqDfK.js";
import path from "path";
//#region src/util/finishReason.ts
/**
* Mapping of provider-specific finish/stop reasons to standardized OpenAI-compatible values.
*
* This normalization allows consistent finish reason handling across different LLM providers:
*
* **OpenAI Standard Values:**
* - `stop`: Natural completion (reached end_of_turn, stop sequence, etc.)
* - `length`: Token limit reached (max_tokens, context length, etc.)
* - `content_filter`: Content filtering triggered
* - `tool_calls`: Model made function/tool calls
*
* **Provider Mappings:**
* - OpenAI: `function_call` (legacy) → `tool_calls` (current)
* - Anthropic: `end_turn` → `stop`, `stop_sequence` → `stop`, `max_tokens` → `length`, `tool_use` → `tool_calls`, `refusal` → `content_filter`, `pause_turn` → `pause_turn`
*
* @example
* ```typescript
* normalizeFinishReason('end_turn')     // Returns: 'stop'
* normalizeFinishReason('max_tokens')   // Returns: 'length'
* normalizeFinishReason('tool_use')     // Returns: 'tool_calls'
* normalizeFinishReason('refusal')      // Returns: 'content_filter'
* normalizeFinishReason('function_call') // Returns: 'tool_calls'
* normalizeFinishReason('unknown')      // Returns: 'unknown' (passthrough)
* ```
*/
const FINISH_REASON_MAP = {
	stop: "stop",
	length: "length",
	content_filter: "content_filter",
	tool_calls: "tool_calls",
	function_call: "tool_calls",
	end_turn: "stop",
	stop_sequence: "stop",
	max_tokens: "length",
	tool_use: "tool_calls",
	refusal: "content_filter",
	pause_turn: "pause_turn"
};
/**
* Normalize a provider-specific finish or stop reason to a standard OpenAI-compatible value.
*
* This function standardizes finish reasons across different LLM providers to enable
* consistent handling in assertions and application logic. Unknown values are passed
* through unchanged to preserve provider-specific reasons.
*
* @param raw - The raw finish_reason/stop_reason from the provider response
* @returns A normalized finish reason string, or undefined if input is invalid
*
* @example Basic usage
* ```typescript
* const result = await provider.callApi('Hello world');
* const normalized = normalizeFinishReason(result.finishReason);
* // normalized will be one of: 'stop', 'length', 'content_filter', 'tool_calls', or original value
* ```
*
* @example With finish-reason assertion
* ```yaml
* assert:
*   - type: finish-reason
*     value: stop  # Expects natural completion (works for both 'stop' and 'end_turn')
* ```
*/
function normalizeFinishReason(raw) {
	if (raw == null) return;
	if (typeof raw !== "string") return;
	const trimmed = raw.trim();
	if (trimmed === "") return;
	const key = trimmed.toLowerCase();
	return FINISH_REASON_MAP[key] ?? key;
}
//#endregion
//#region src/providers/openai/chat.ts
var OpenAiChatCompletionProvider = class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
	static OPENAI_CHAT_MODELS = OPENAI_CHAT_MODELS;
	static OPENAI_CHAT_MODEL_NAMES = OPENAI_CHAT_MODELS.map((model) => model.id);
	config;
	mcpClient = null;
	initializationPromise = null;
	loadedFunctionCallbacks = {};
	constructor(modelName, options = {}) {
		if (!OpenAiChatCompletionProvider.OPENAI_CHAT_MODEL_NAMES.includes(modelName)) logger.debug(`Using unknown chat model: ${modelName}`);
		super(modelName, options);
		this.config = options.config ? { ...options.config } : {};
		if (this.config.mcp?.enabled) this.initializationPromise = this.initializeMCP();
	}
	validateFunctionToolCall(output, vars) {
		validateFunctionCall(output, this.config.functions, vars);
	}
	async initializeMCP() {
		this.mcpClient = new MCPClient(this.config.mcp);
		await this.mcpClient.initialize();
	}
	async cleanup() {
		if (this.mcpClient) {
			await this.initializationPromise;
			await this.mcpClient.cleanup();
			this.mcpClient = null;
		}
	}
	/**
	* Loads a function from an external file
	* @param fileRef The file reference in the format 'file://path/to/file:functionName'
	* @returns The loaded function
	*/
	async loadExternalFunction(fileRef) {
		const { filePath, functionName } = parseFileUrl(fileRef);
		try {
			const resolvedPath = path.resolve(state.basePath || "", filePath);
			logger.debug(`Loading function from ${resolvedPath}${functionName ? `:${functionName}` : ""}`);
			const requiredModule = await importModule(resolvedPath, functionName);
			if (typeof requiredModule === "function") return requiredModule;
			else if (requiredModule && typeof requiredModule === "object" && functionName && functionName in requiredModule) {
				const fn = requiredModule[functionName];
				if (typeof fn === "function") return fn;
			}
			throw new Error(`Function callback malformed: ${filePath} must export ${functionName ? `a named function '${functionName}'` : "a function or have a default export as a function"}`);
		} catch (error) {
			throw new Error(`Error loading function from ${filePath}: ${error.message || String(error)}`);
		}
	}
	/**
	* Executes a function callback with proper error handling
	*/
	async executeFunctionCallback(functionName, args, config) {
		try {
			let callback = this.loadedFunctionCallbacks[functionName];
			if (!callback) {
				const callbackRef = config.functionToolCallbacks?.[functionName];
				if (callbackRef && typeof callbackRef === "string") {
					const callbackStr = callbackRef;
					if (callbackStr.startsWith("file://")) callback = await this.loadExternalFunction(callbackStr);
					else callback = new Function("return " + callbackStr)();
					this.loadedFunctionCallbacks[functionName] = callback;
				} else if (typeof callbackRef === "function") {
					callback = callbackRef;
					this.loadedFunctionCallbacks[functionName] = callback;
				}
			}
			if (!callback) throw new Error(`No callback found for function '${functionName}'`);
			logger.debug(`Executing function '${functionName}' with args: ${args}`);
			const result = await callback(args);
			if (result === void 0 || result === null) return "";
			else if (typeof result === "object") try {
				return JSON.stringify(result);
			} catch (error) {
				logger.warn(`Error stringifying result from function '${functionName}': ${error}`);
				return String(result);
			}
			else return String(result);
		} catch (error) {
			logger.error(`Error executing function '${functionName}': ${error.message || String(error)}`);
			throw error;
		}
	}
	isGPT5Model() {
		return this.modelName.startsWith("gpt-5") || this.modelName.includes("/gpt-5");
	}
	isReasoningModel() {
		return this.modelName.startsWith("o1") || this.modelName.startsWith("o3") || this.modelName.startsWith("o4") || this.modelName.includes("/o1") || this.modelName.includes("/o3") || this.modelName.includes("/o4") || this.isGPT5Model();
	}
	supportsTemperature() {
		return !this.isReasoningModel();
	}
	getBillingModelName(_config) {
		return this.modelName;
	}
	getGenAISystem() {
		return "openai";
	}
	async getOpenAiBody(prompt, context, callApiOptions) {
		const config = {
			...this.config,
			...context?.prompt?.config
		};
		const messages = parseChatPrompt(prompt, [{
			role: "user",
			content: prompt
		}]);
		const isReasoningModel = this.isReasoningModel();
		const isGPT5Model = this.isGPT5Model();
		const maxCompletionTokens = isReasoningModel ? config.max_completion_tokens ?? getEnvInt("OPENAI_MAX_COMPLETION_TOKENS") : void 0;
		const maxTokensDefault = config.omitDefaults ? getEnvString("OPENAI_MAX_TOKENS") === void 0 ? void 0 : getEnvInt("OPENAI_MAX_TOKENS") : getEnvInt("OPENAI_MAX_TOKENS", 1024);
		const maxTokens = isReasoningModel || isGPT5Model ? void 0 : config.max_tokens ?? maxTokensDefault;
		const temperatureDefault = config.omitDefaults ? getEnvString("OPENAI_TEMPERATURE") === void 0 ? void 0 : getEnvFloat("OPENAI_TEMPERATURE") : getEnvFloat("OPENAI_TEMPERATURE", 0);
		const temperature = this.supportsTemperature() ? config.temperature ?? temperatureDefault : void 0;
		const reasoningEffort = isReasoningModel ? renderVarsInObject(config.reasoning_effort, context?.vars) : void 0;
		const mcpTools = this.mcpClient ? transformMCPToolsToOpenAi(this.mcpClient.getAllTools()) : [];
		const fileTools = transformTools(config.tools ? await maybeLoadToolsFromExternalFile(config.tools, context?.vars) || [] : [], "openai");
		const allTools = [...mcpTools, ...fileTools];
		const body = {
			model: this.modelName,
			messages,
			seed: config.seed,
			...maxTokens === void 0 ? {} : { max_tokens: maxTokens },
			...maxCompletionTokens === void 0 ? {} : { max_completion_tokens: maxCompletionTokens },
			...reasoningEffort ? { reasoning_effort: reasoningEffort } : {},
			...temperature === void 0 ? {} : { temperature },
			...config.top_p !== void 0 || getEnvString("OPENAI_TOP_P") ? { top_p: config.top_p ?? getEnvFloat("OPENAI_TOP_P", 1) } : {},
			...config.presence_penalty !== void 0 || getEnvString("OPENAI_PRESENCE_PENALTY") ? { presence_penalty: config.presence_penalty ?? getEnvFloat("OPENAI_PRESENCE_PENALTY", 0) } : {},
			...config.frequency_penalty !== void 0 || getEnvString("OPENAI_FREQUENCY_PENALTY") ? { frequency_penalty: config.frequency_penalty ?? getEnvFloat("OPENAI_FREQUENCY_PENALTY", 0) } : {},
			...config.functions ? { functions: maybeLoadFromExternalFileWithVars(config.functions, context?.vars) } : {},
			...config.function_call ? { function_call: config.function_call } : {},
			...allTools.length > 0 ? { tools: allTools } : {},
			...config.tool_choice ? { tool_choice: transformToolChoice(config.tool_choice, "openai") } : {},
			...config.tool_resources ? { tool_resources: config.tool_resources } : {},
			...config.response_format ? { response_format: maybeLoadResponseFormatFromExternalFile(config.response_format, context?.vars) } : {},
			...callApiOptions?.includeLogProbs ? { logprobs: callApiOptions.includeLogProbs } : {},
			...config.stop ? { stop: config.stop } : {},
			...config.prompt_cache_key === void 0 ? {} : { prompt_cache_key: config.prompt_cache_key },
			...config.prompt_cache_retention === void 0 ? {} : { prompt_cache_retention: config.prompt_cache_retention },
			...config.passthrough || {},
			...this.modelName.includes("audio") ? {
				modalities: config.modalities || ["text", "audio"],
				audio: config.audio || {
					voice: "alloy",
					format: "wav"
				}
			} : {},
			...isGPT5Model && config.verbosity ? { verbosity: config.verbosity } : {}
		};
		if (config.reasoning_effort && (isReasoningModel || this.modelName.includes("gpt-oss"))) body.reasoning_effort = config.reasoning_effort;
		if (config.reasoning && (this.modelName.startsWith("o1") || this.modelName.startsWith("o3") || this.modelName.startsWith("o4") || this.modelName.includes("/o1") || this.modelName.includes("/o3") || this.modelName.includes("/o4"))) body.reasoning = config.reasoning;
		if (config.service_tier) body.service_tier = config.service_tier;
		if (config.user) body.user = config.user;
		if (config.metadata) body.metadata = config.metadata;
		if (config.store !== void 0) body.store = config.store;
		if ((isReasoningModel || isGPT5Model) && "max_tokens" in body) delete body.max_tokens;
		return {
			body,
			config
		};
	}
	async callApi(prompt, context, callApiOptions) {
		if (this.initializationPromise != null) await this.initializationPromise;
		if (this.requiresApiKey() && !this.getApiKey()) throw new Error(this.getMissingApiKeyErrorMessage());
		const spanContext = {
			system: this.getGenAISystem(),
			operationName: "chat",
			model: this.modelName,
			providerId: this.id(),
			maxTokens: this.config.max_tokens,
			temperature: this.config.temperature,
			topP: this.config.top_p,
			stopSequences: this.config.stop,
			evalId: context?.evaluationId || context?.test?.metadata?.evaluationId,
			testIndex: context?.test?.vars?.__testIdx,
			promptLabel: context?.prompt?.label,
			traceparent: context?.traceparent,
			requestBody: prompt
		};
		const resultExtractor = (response) => {
			const result = {};
			if (response.tokenUsage) result.tokenUsage = {
				prompt: response.tokenUsage.prompt,
				completion: response.tokenUsage.completion,
				total: response.tokenUsage.total,
				cached: response.tokenUsage.cached,
				completionDetails: {
					reasoning: response.tokenUsage.completionDetails?.reasoning,
					acceptedPrediction: response.tokenUsage.completionDetails?.acceptedPrediction,
					rejectedPrediction: response.tokenUsage.completionDetails?.rejectedPrediction
				}
			};
			if (response.finishReason) result.finishReasons = [response.finishReason];
			if (response.cached !== void 0) result.cacheHit = response.cached;
			if (response.output !== void 0) result.responseBody = typeof response.output === "string" ? response.output : JSON.stringify(response.output);
			return result;
		};
		return withGenAISpan(spanContext, () => this.callApiInternal(prompt, context, callApiOptions), resultExtractor);
	}
	/**
	* Internal implementation of callApi without tracing wrapper.
	* This is called by callApi after setting up the tracing span.
	*/
	async callApiInternal(prompt, context, callApiOptions) {
		const { body, config } = await this.getOpenAiBody(prompt, context, callApiOptions);
		let data;
		let status;
		let statusText;
		let cached = false;
		let latencyMs;
		let deleteFromCache;
		let responseHeaders;
		try {
			({data, cached, status, statusText, latencyMs, deleteFromCache, headers: responseHeaders} = await fetchWithCache(`${this.getApiUrl()}/chat/completions`, {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					...this.getApiKey() ? { Authorization: `Bearer ${this.getApiKey()}` } : {},
					...this.getOpenAiRequestHeaders(config.headers)
				},
				body: JSON.stringify(body)
			}, getRequestTimeoutMs(), "json", context?.bustCache ?? context?.debug, this.config.maxRetries));
			if (status < 200 || status >= 300) {
				const errorMessage = `API error: ${status} ${statusText}\n${typeof data === "string" ? data : JSON.stringify(data)}`;
				if (typeof data === "object" && data?.error?.code === "invalid_prompt") return {
					output: errorMessage,
					tokenUsage: data?.usage ? getTokenUsage(data, cached) : void 0,
					latencyMs,
					isRefusal: true,
					guardrails: {
						flagged: true,
						flaggedInput: true
					},
					metadata: { http: {
						status,
						statusText,
						headers: responseHeaders ?? {}
					} }
				};
				return {
					error: errorMessage,
					metadata: { http: {
						status,
						statusText,
						headers: responseHeaders ?? {}
					} }
				};
			}
		} catch (err) {
			logger.error(`API call error: ${String(err)}`);
			await deleteFromCache?.();
			if (err instanceof HttpRateLimitError) return {
				error: formatRateLimitErrorMessage(err),
				metadata: {
					rateLimitKind: err.kind,
					http: {
						status: err.status,
						statusText: err.statusText,
						headers: err.headers ?? responseHeaders ?? {}
					}
				}
			};
			return {
				error: `API call error: ${String(err)}`,
				metadata: { http: {
					status: 0,
					statusText: "Error",
					headers: responseHeaders ?? {}
				} }
			};
		}
		try {
			const message = data.choices[0].message;
			const finishReason = normalizeFinishReason(data.choices[0].finish_reason);
			const contentFiltered = finishReason === FINISH_REASON_MAP.content_filter;
			if (message.refusal) return {
				output: message.refusal,
				tokenUsage: getTokenUsage(data, cached),
				cached,
				latencyMs,
				isRefusal: true,
				...finishReason && { finishReason },
				guardrails: { flagged: true },
				metadata: { http: {
					status,
					statusText,
					headers: responseHeaders ?? {}
				} }
			};
			if (contentFiltered) return {
				output: message.content || "Content filtered by provider",
				tokenUsage: getTokenUsage(data, cached),
				cached,
				latencyMs,
				isRefusal: true,
				finishReason: FINISH_REASON_MAP.content_filter,
				guardrails: { flagged: true },
				metadata: { http: {
					status,
					statusText,
					headers: responseHeaders ?? {}
				} }
			};
			let reasoning = "";
			let output = "";
			if (message.reasoning) {
				reasoning = message.reasoning;
				output = message.content;
			} else if (message.content && (message.function_call || message.tool_calls)) if (Array.isArray(message.tool_calls) && message.tool_calls.length === 0) output = message.content;
			else output = message;
			else if (message.content === null || message.content === void 0 || message.content === "" && message.tool_calls) output = message.function_call || message.tool_calls;
			else output = message.content;
			const logProbs = data.choices[0].logprobs?.content?.map((logProbObj) => logProbObj.logprob);
			if (config.response_format?.type === "json_schema" && typeof output === "string") try {
				output = JSON.parse(output);
			} catch (error) {
				logger.error(`Failed to parse JSON output: ${error}`);
			}
			if (reasoning && typeof output === "string" && (this.config.showThinking ?? true)) output = `Thinking: ${reasoning}\n\n${output}`;
			const functionCalls = message.function_call ? [message.function_call] : message.tool_calls;
			if (functionCalls && (config.functionToolCallbacks || this.mcpClient)) {
				const results = [];
				let hasSuccessfulCallback = false;
				for (const functionCall of functionCalls) {
					const functionName = functionCall.name || functionCall.function?.name;
					if (this.mcpClient) {
						if (this.mcpClient.getAllTools().find((tool) => tool.name === functionName)) try {
							const args = functionCall.arguments || functionCall.function?.arguments || "{}";
							const parsedArgs = typeof args === "string" ? JSON.parse(args) : args;
							const mcpResult = await this.mcpClient.callTool(functionName, parsedArgs);
							if (isMcpErrorResult(mcpResult)) results.push(`MCP Tool Error (${functionName}): ${getMcpErrorMessage(mcpResult)}`);
							else {
								const normalizeContent = (content) => {
									if (content == null) return "";
									if (typeof content === "string") return content;
									if (Array.isArray(content)) return content.map((part) => {
										if (typeof part === "string") return part;
										if (part && typeof part === "object") {
											if ("text" in part && part.text != null) return String(part.text);
											if ("json" in part) return JSON.stringify(part.json);
											if ("data" in part) return JSON.stringify(part.data);
											return JSON.stringify(part);
										}
										return String(part);
									}).join("\n");
									return JSON.stringify(content);
								};
								const content = normalizeContent(mcpResult?.content);
								results.push(`MCP Tool Result (${functionName}): ${content}`);
							}
							hasSuccessfulCallback = true;
							continue;
						} catch (error) {
							logger.debug(`MCP tool execution failed for ${functionName}: ${error}`);
							results.push(`MCP Tool Error (${functionName}): ${error}`);
							hasSuccessfulCallback = true;
							continue;
						}
					}
					if (config.functionToolCallbacks && config.functionToolCallbacks[functionName]) try {
						const functionResult = await this.executeFunctionCallback(functionName, functionCall.arguments || functionCall.function?.arguments, config);
						results.push(functionResult);
						hasSuccessfulCallback = true;
					} catch (error) {
						logger.debug(`Function callback failed for ${functionName} with error ${error}, falling back to original output`);
						hasSuccessfulCallback = false;
						break;
					}
				}
				if (hasSuccessfulCallback && results.length > 0) return {
					output: results.join("\n"),
					tokenUsage: getTokenUsage(data, cached),
					cached,
					latencyMs,
					logProbs,
					...finishReason && { finishReason },
					cost: calculateOpenAIUsageCost(this.getBillingModelName(config), config, data.usage, {
						cachedResponse: cached,
						serviceTier: data.service_tier ?? config.service_tier
					}),
					guardrails: { flagged: contentFiltered },
					metadata: { http: {
						status,
						statusText,
						headers: responseHeaders ?? {}
					} }
				};
			}
			if (message.reasoning_content && typeof message.reasoning_content === "string" && typeof output === "string" && (this.config.showThinking ?? true)) output = `Thinking: ${message.reasoning_content}\n\n${output}`;
			if (message.audio) return {
				output: message.audio.transcript || "",
				audio: {
					id: message.audio.id,
					expiresAt: message.audio.expires_at,
					data: message.audio.data,
					transcript: message.audio.transcript,
					format: message.audio.format || "wav"
				},
				tokenUsage: getTokenUsage(data, cached),
				cached,
				latencyMs,
				logProbs,
				...finishReason && { finishReason },
				cost: calculateOpenAIUsageCost(this.getBillingModelName(config), config, data.usage, {
					cachedResponse: cached,
					serviceTier: data.service_tier ?? config.service_tier
				}),
				guardrails: { flagged: contentFiltered },
				metadata: { http: {
					status,
					statusText,
					headers: responseHeaders ?? {}
				} }
			};
			return {
				output,
				tokenUsage: getTokenUsage(data, cached),
				cached,
				latencyMs,
				logProbs,
				...finishReason && { finishReason },
				cost: calculateOpenAIUsageCost(this.getBillingModelName(config), config, data.usage, {
					cachedResponse: cached,
					serviceTier: data.service_tier ?? config.service_tier
				}),
				guardrails: { flagged: contentFiltered },
				metadata: {
					http: {
						status,
						statusText,
						headers: responseHeaders ?? {}
					},
					...data.choices.length > 1 && { choices: data.choices }
				}
			};
		} catch (err) {
			await deleteFromCache?.();
			return {
				error: `API error: ${String(err)}: ${JSON.stringify(data)}`,
				metadata: { http: {
					status,
					statusText,
					headers: responseHeaders ?? {}
				} }
			};
		}
	}
};
//#endregion
export { FINISH_REASON_MAP as n, normalizeFinishReason as r, OpenAiChatCompletionProvider as t };

//# sourceMappingURL=chat-yK5XvXCV.js.map