{"version":3,"file":"gemini-embeddings.cjs","names":["flattenHeaders","getContext","getTestId","matchFixtureDiagnostic","applyChaos","resolveResponse","isErrorResponse","isEmbeddingResponse","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","generateDeterministicEmbedding"],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n  ChatCompletionRequest,\n  Fixture,\n  HandlerDefaults,\n  RecordProviderKey,\n} from \"./types.js\";\nimport {\n  isEmbeddingResponse,\n  isErrorResponse,\n  generateDeterministicEmbedding,\n  flattenHeaders,\n  getTestId,\n  resolveResponse,\n  resolveStrictMode,\n  strictOverrideField,\n  getContext,\n  strictNoMatchMessage,\n  strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n  text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n  content?: { parts?: GeminiEmbedContentPart[] };\n  model?: string;\n  taskType?: string;\n  title?: string;\n  outputDimensionality?: number;\n  [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n  embedding: {\n    values: number[];\n  };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n  req: http.IncomingMessage,\n  res: http.ServerResponse,\n  raw: string,\n  model: string,\n  fixtures: Fixture[],\n  journal: Journal,\n  defaults: HandlerDefaults,\n  setCorsHeaders: (res: http.ServerResponse) => void,\n  providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n  const { logger } = defaults;\n  setCorsHeaders(res);\n\n  let embedReq: GeminiEmbedContentRequest;\n  try {\n    embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n  } catch (parseErr) {\n    const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n    journal.add({\n      method: req.method ?? \"POST\",\n      path: req.url ?? `/v1beta/models/${model}:embedContent`,\n      headers: flattenHeaders(req.headers),\n      body: null,\n      response: { status: 400, fixture: null },\n    });\n    writeErrorResponse(\n      res,\n      400,\n      JSON.stringify({\n        error: {\n          message: `Malformed JSON body: ${detail}`,\n          code: 400,\n          status: \"INVALID_ARGUMENT\",\n        },\n      }),\n    );\n    return;\n  }\n\n  // Extract text from content.parts\n  const parts = embedReq.content?.parts ?? [];\n  const inputText = parts\n    .filter((p) => p.text !== undefined)\n    .map((p) => p.text!)\n    .join(\" \");\n\n  // Build a synthetic ChatCompletionRequest for the fixture router.\n  // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n  // with OpenAI embeddings.\n  const syntheticReq: ChatCompletionRequest = {\n    model: embedReq.model ?? model,\n    messages: [],\n    embeddingInput: inputText,\n    _endpointType: \"embedding\",\n    _context: getContext(req),\n  };\n\n  const testId = getTestId(req);\n  const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n    fixtures,\n    syntheticReq,\n    journal.getFixtureMatchCountsForTest(testId),\n    defaults.requestTransform,\n  );\n  const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n  if (fixture) {\n    journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n    logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n  } else {\n    logger.debug(`No fixture matched for request`);\n  }\n\n  if (\n    applyChaos(\n      res,\n      fixture,\n      defaults.chaos,\n      req.headers,\n      journal,\n      {\n        method: req.method ?? \"POST\",\n        path,\n        headers: flattenHeaders(req.headers),\n        body: syntheticReq,\n      },\n      fixture ? \"fixture\" : \"proxy\",\n      defaults.registry,\n      defaults.logger,\n    )\n  )\n    return;\n\n  if (fixture) {\n    const response = await resolveResponse(fixture, syntheticReq);\n\n    // Error response\n    if (isErrorResponse(response)) {\n      const status = response.status ?? 500;\n      journal.add({\n        method: req.method ?? \"POST\",\n        path,\n        headers: flattenHeaders(req.headers),\n        body: syntheticReq,\n        response: { status, fixture },\n      });\n      const geminiError = {\n        error: {\n          code: status,\n          message: response.error.message,\n          status: response.error.type ?? \"ERROR\",\n        },\n      };\n      writeErrorResponse(res, status, JSON.stringify(geminiError), {\n        retryAfter: response.retryAfter,\n      });\n      return;\n    }\n\n    // Embedding response — use the fixture's embedding values\n    if (isEmbeddingResponse(response)) {\n      journal.add({\n        method: req.method ?? \"POST\",\n        path,\n        headers: flattenHeaders(req.headers),\n        body: syntheticReq,\n        response: { status: 200, fixture },\n      });\n      const body: GeminiEmbedContentResponse = {\n        embedding: {\n          values: [...response.embedding],\n        },\n      };\n      res.writeHead(200, { \"Content-Type\": \"application/json\" });\n      res.end(JSON.stringify(body));\n      return;\n    }\n\n    // Fixture matched but response type is not compatible with embeddings\n    journal.add({\n      method: req.method ?? \"POST\",\n      path,\n      headers: flattenHeaders(req.headers),\n      body: syntheticReq,\n      response: { status: 500, fixture },\n    });\n    writeErrorResponse(\n      res,\n      500,\n      JSON.stringify({\n        error: {\n          message:\n            \"Fixture response did not match any known embedding type (must have embedding or error)\",\n          code: 500,\n          status: \"INTERNAL\",\n        },\n      }),\n    );\n    return;\n  }\n\n  // No fixture match — check strict mode first, then try proxy\n  const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n  if (effectiveStrict) {\n    const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n    logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n    journal.add({\n      method: req.method ?? \"POST\",\n      path,\n      headers: flattenHeaders(req.headers),\n      body: syntheticReq,\n      response: {\n        status: 503,\n        fixture: null,\n        ...strictOverrideField(defaults.strict, req.headers),\n      },\n    });\n    writeErrorResponse(\n      res,\n      503,\n      JSON.stringify({\n        error: {\n          message: strictMessage,\n          code: 503,\n          status: \"UNAVAILABLE\",\n        },\n      }),\n    );\n    return;\n  }\n\n  if (defaults.record) {\n    const outcome = await proxyAndRecord(\n      req,\n      res,\n      syntheticReq,\n      providerKey,\n      path,\n      fixtures,\n      defaults,\n      raw,\n    );\n    if (outcome === \"handled_by_hook\") return;\n    if (outcome !== \"not_configured\") {\n      journal.add({\n        method: req.method ?? \"POST\",\n        path,\n        headers: flattenHeaders(req.headers),\n        body: syntheticReq,\n        response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n      });\n      return;\n    }\n  }\n\n  // No fixture match — generate deterministic embedding from input text\n  const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n  const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n  logger.warn(\n    `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n  );\n\n  journal.add({\n    method: req.method ?? \"POST\",\n    path,\n    headers: flattenHeaders(req.headers),\n    body: syntheticReq,\n    response: { status: 200, fixture: null },\n  });\n\n  const body: GeminiEmbedContentResponse = {\n    embedding: {\n      values: embedding,\n    },\n  };\n  res.writeHead(200, { \"Content-Type\": \"application/json\" });\n  res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}