
//#region src/components/execution/otel/aiExtractor.ts
function textField(field, source, fallbackSources) {
	return {
		field,
		source,
		fallbackSources,
		valueType: "text",
		merge: "replace"
	};
}
function numberField(field, source, merge) {
	return {
		field,
		source,
		valueType: "number",
		merge
	};
}
function stringListField(field, source) {
	return {
		field,
		source,
		valueType: "stringList",
		merge: "replace"
	};
}
/**
* Every `gen_ai.*` attribute we capture, mapped to its canonical field.
*
* Anything not listed here (prompt/response content, messages, tool calls,
* unknown keys) is ignored.
*/
const FIELD_SPECS = [
	textField("requestModel", "gen_ai.request.model"),
	textField("responseModel", "gen_ai.response.model"),
	textField("provider", "gen_ai.provider.name", ["gen_ai.system"]),
	textField("responseId", "gen_ai.response.id"),
	stringListField("finishReasons", "gen_ai.response.finish_reasons"),
	numberField("inputTokens", "gen_ai.usage.input_tokens", "sum"),
	numberField("outputTokens", "gen_ai.usage.output_tokens", "sum"),
	numberField("totalTokens", "gen_ai.usage.total_tokens", "sum"),
	numberField("cacheReadTokens", "gen_ai.usage.cache_read.input_tokens", "sum"),
	numberField("cacheCreationTokens", "gen_ai.usage.cache_creation.input_tokens", "sum"),
	numberField("reasoningTokens", "gen_ai.usage.reasoning.output_tokens", "sum"),
	numberField("temperature", "gen_ai.request.temperature", "replace"),
	numberField("topP", "gen_ai.request.top_p", "replace"),
	numberField("maxTokens", "gen_ai.request.max_tokens", "replace"),
	numberField("frequencyPenalty", "gen_ai.request.frequency_penalty", "replace"),
	numberField("presencePenalty", "gen_ai.request.presence_penalty", "replace"),
	numberField("seed", "gen_ai.request.seed", "replace")
];
function parseNumericAttribute(value) {
	if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
	if (typeof value !== "string" || value === "") return;
	const num = Number(value);
	return Number.isFinite(num) ? num : void 0;
}
/**
* Coerces an attribute to a list of non-empty strings, or `undefined` when it
* yields none. OTLP arrays may carry non-string or empty entries; those are
* dropped, and an array that reduces to nothing is treated as absent.
*/
function parseStringListAttribute(value) {
	const strings = (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string" && item !== "");
	return strings.length > 0 ? strings : void 0;
}
/**
* Extracts {@link AIMetadata} from a span's attributes.
*
* Only the allowlisted {@link FIELD_SPECS} are read. Every other attribute is
* ignored. Numeric fields accept numbers and quoted numeric strings and are
* otherwise dropped, as OTLP/JSON may encode int64 as a quoted string. Text
* fields are dropped when empty. String-list fields keep only non-empty string
* entries and are dropped when none remain.
*
* Returns an empty object for spans carrying no recognised `gen_ai.*`
* attributes.
*
* @param attributes - The span attributes, as exposed by
* `ReadableSpan.attributes`.
*/
const extractAIMetadataFromAttributes = (attributes) => {
	const metadata = {};
	for (const spec of FIELD_SPECS) {
		const sources = [spec.source, ...spec.fallbackSources ?? []];
		for (const source of sources) {
			const value = attributes[source];
			if (value === void 0) continue;
			if (spec.valueType === "text") {
				const text = typeof value === "string" ? value : String(value);
				if (text !== "") {
					metadata[spec.field] = text;
					break;
				}
			} else if (spec.valueType === "stringList") {
				const list = parseStringListAttribute(value);
				if (list !== void 0) {
					metadata[spec.field] = list;
					break;
				}
			} else {
				const num = parseNumericAttribute(value);
				if (num !== void 0) {
					metadata[spec.field] = num;
					break;
				}
			}
		}
	}
	return metadata;
};
/**
* Aggregates two {@link AIMetadata} values, folding a later call's metadata into
* an earlier accumulator for a single step.
*
* - Token-count fields (`merge: "sum"`) are **summed**, so a step that makes
*   several LLM calls reports their combined usage.
* - Every other field uses `merge: "replace"` (`b` overwrites `a`)
*
* A field is present in the result only when at least one input supplies it.
*
* @param a - The accumulator (earlier calls).
* @param b - The later metadata, whose values win on conflict (and add for sums).
*/
const aggregate = (a, b) => {
	const out = { ...a };
	for (const spec of FIELD_SPECS) if (spec.valueType === "number") {
		const bValue = b[spec.field];
		if (bValue === void 0) continue;
		if (spec.merge === "sum") out[spec.field] = (out[spec.field] ?? 0) + bValue;
		else out[spec.field] = bValue;
	} else if (spec.valueType === "text") {
		const bValue = b[spec.field];
		if (bValue === void 0) continue;
		out[spec.field] = bValue;
	} else {
		const bValue = b[spec.field];
		if (bValue === void 0) continue;
		out[spec.field] = bValue;
	}
	return out;
};
/** Converts a canonical camelCase field name to the server's snake_case key. */
const toSnakeCase = (field) => field.replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`);
/**
* Maps an {@link AIMetadata} value onto the server's `inngest.ai` metadata
* values, converting each canonical camelCase field to snake_case (e.g.
* `inputTokens` → `input_tokens`). Returns `undefined` when there is nothing to
* emit so callers can skip stamping metadata entirely.
*/
const toInngestAIMetadataValues = (metadata) => {
	const entries = Object.entries(metadata);
	if (entries.length === 0) return;
	const values = {};
	for (const [field, value] of entries) values[toSnakeCase(field)] = value;
	return values;
};

//#endregion
exports.aggregate = aggregate;
exports.extractAIMetadataFromAttributes = extractAIMetadataFromAttributes;
exports.toInngestAIMetadataValues = toInngestAIMetadataValues;
//# sourceMappingURL=aiExtractor.cjs.map