/** * The `TEXT_EMBEDDING` model handler, POSTing directly to OpenRouter's * `/embeddings` endpoint. Validates the configured dimension against * `VECTOR_DIMS` and rejects a returned vector whose length disagrees — no silent * truncation. A `null` param yields a deterministic marker probe vector; oversized * input is truncated to ~8000 tokens with a warning. Emits a `MODEL_USED` event. */ import type { IAgentRuntime, TextEmbeddingParams } from "@elizaos/core"; import { logger, ModelType, VECTOR_DIMS } from "@elizaos/core"; import { getApiKey, getBaseURL, getEmbeddingModel, getSetting } from "../utils/config"; import { emitModelUsageEvent } from "../utils/events"; export async function handleTextEmbedding( runtime: IAgentRuntime, params: TextEmbeddingParams | string | null ): Promise { const embeddingModelName = getEmbeddingModel(runtime); const embeddingDimension = Number.parseInt( getSetting(runtime, "OPENROUTER_EMBEDDING_DIMENSIONS") ?? getSetting(runtime, "EMBEDDING_DIMENSIONS") ?? "1536", 10 ) as (typeof VECTOR_DIMS)[keyof typeof VECTOR_DIMS]; if (!Object.values(VECTOR_DIMS).includes(embeddingDimension)) { const errorMsg = `Invalid embedding dimension: ${embeddingDimension}. Must be one of: ${Object.values(VECTOR_DIMS).join(", ")}`; logger.error(errorMsg); throw new Error(errorMsg); } if (params === null) { const testVector = Array(embeddingDimension).fill(0) as number[]; testVector[0] = 0.1; return testVector; } let text: string; if (typeof params === "string") { text = params; } else if (typeof params === "object" && typeof params.text === "string") { text = params.text; } else { const errorMsg = "Invalid input format for embedding"; logger.error(errorMsg); throw new Error(errorMsg); } if (!text.trim()) { const errorMsg = "Empty text for embedding"; logger.error(errorMsg); throw new Error(errorMsg); } // Truncate to stay within embedding model token limits (~4 chars per token) const maxChars = 8_000 * 4; if (text.length > maxChars) { logger.warn( `[OpenRouter] Embedding input too long (~${Math.ceil(text.length / 4)} tokens), truncating to ~8000 tokens` ); text = text.slice(0, maxChars); } const apiKey = getApiKey(runtime); if (!apiKey) { const errorMsg = "OPENROUTER_API_KEY is not set"; logger.error(errorMsg); throw new Error(errorMsg); } const baseURL = getBaseURL(runtime); try { // @trajectory-allow Embeddings return numeric retrieval vectors, not generative LLM text. const response = await fetch(`${baseURL}/embeddings`, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "HTTP-Referer": getSetting(runtime, "OPENROUTER_HTTP_REFERER") || "", "X-Title": getSetting(runtime, "OPENROUTER_X_TITLE") || "ElizaOS", }, body: JSON.stringify({ model: embeddingModelName, input: text, }), }); if (!response.ok) { logger.error(`OpenRouter API error: ${response.status} - ${response.statusText}`); throw new Error(`OpenRouter API error: ${response.status} - ${response.statusText}`); } const data = (await response.json()) as { data?: Array<{ embedding?: number[] }>; usage?: { prompt_tokens: number; total_tokens: number }; }; const embedding = data.data?.[0]?.embedding; if (!embedding) { logger.error("API returned invalid structure"); throw new Error("API returned invalid structure"); } if (!Array.isArray(embedding) || embedding.length !== embeddingDimension) { const errorMsg = `Embedding length ${embedding.length} does not match configured dimension ${embeddingDimension}`; logger.error(errorMsg); throw new Error(errorMsg); } if (data.usage) { const usage = { inputTokens: data.usage.prompt_tokens, outputTokens: 0, totalTokens: data.usage.total_tokens, }; emitModelUsageEvent(runtime, ModelType.TEXT_EMBEDDING, text, usage, embeddingModelName); } return embedding; } catch (error: unknown) { // error-policy:J2 context-adding rethrow — never fabricate a vector; the // provider failure surfaces to the caller (#9324: throw, never fabricate). const message = error instanceof Error ? error.message : String(error); logger.error(`Error generating embedding: ${message}`); throw error instanceof Error ? error : new Error(message); } }