{"version":3,"file":"adapter.mjs","names":["#options"],"sources":["../../../../src/batteries/embeddings/openai/adapter.ts"],"sourcesContent":["/**\n * Cross-environment OpenAI Embeddings adapter battery.\n *\n * @module @nhtio/adk/batteries/embeddings/openai/adapter\n *\n * @remarks\n * Opinionated embeddings battery for the OpenAI `/v1/embeddings` wire shape. Ships an\n * {@link OpenAIEmbeddingsAdapter} that targets any OpenAI-`/v1/embeddings`-compatible endpoint\n * (OpenAI proper, Azure-behind-proxy, vLLM, Together, a local gateway, etc.) over raw `fetch` —\n * no SDK dependency, so it runs unchanged in Node, the browser, edge runtimes, and workers.\n *\n * The class shares its method surface, return types, prefix handling, and option base with the\n * WebLLM Embeddings battery: the two differ only in their engine. See\n * {@link @nhtio/adk/batteries/embeddings/openai/types!BaseEmbeddingsAdapterOptions}.\n *\n * Construction validates options eagerly via {@link @nhtio/adk/batteries/embeddings/openai/validation!validateOptions} and throws\n * {@link @nhtio/adk/batteries/embeddings/openai/exceptions!E_INVALID_OPENAI_EMBEDDINGS_OPTIONS} on failure — config bugs fail loud, not at embed time.\n */\n\nimport { isError } from '@nhtio/adk/guards'\nimport { validateOptions } from './validation'\nimport { applyEmbeddingPrefix } from './helpers'\n// Accepted-shared-runtime tier (see CONTRIBUTING.md → Design Decisions → #13 Battery design):\n// pure, class-free retry primitives shared with the Chat Completions LLM batteries. No core class\n// coupling — same tier as guards/createException — so this deep relative reach is accepted as-is,\n// not re-exported through a shim.\nimport { computeBackoff, sleepWithJitter, parseRetryAfter } from '../../../lib/utils/retry'\nimport {\n  E_OPENAI_EMBEDDINGS_HTTP_ERROR,\n  E_OPENAI_EMBEDDINGS_REQUEST_TIMEOUT,\n  E_OPENAI_EMBEDDINGS_MALFORMED_RESPONSE,\n} from './exceptions'\nimport type {\n  EmbedOptions,\n  EmbeddingsRetryConfig,\n  OpenAIEmbeddingsAdapterOptions,\n  OpenAIEmbeddingsRequestBody,\n  OpenAIEmbeddingsResponseBody,\n} from './types'\n\n// ─── Adapter class ────────────────────────────────────────────────────────────\n\n/**\n * Embeddings adapter for the OpenAI `/v1/embeddings` wire shape.\n *\n * @remarks\n * Reusable: construct once, call {@link OpenAIEmbeddingsAdapter.embed} / {@link embedMany} as many\n * times as needed. `embedMany` issues one request per call (OpenAI embeds a batch in a single\n * round-trip); `embed` is sugar over `embedMany([text])`.\n */\nexport class OpenAIEmbeddingsAdapter {\n  readonly #options: OpenAIEmbeddingsAdapterOptions\n\n  /**\n   * Whether this battery can run in the current environment. For the HTTP-backed OpenAI battery\n   * this is always `true` (a `fetch` is always resolvable); present for surface-parity with the\n   * WebLLM battery's WebGPU gate.\n   */\n  public static isAvailable(): boolean {\n    return true\n  }\n\n  /**\n   * @param options - Constructor options. Validated eagerly.\n   * @throws {@link @nhtio/adk/batteries/embeddings/openai/exceptions!E_INVALID_OPENAI_EMBEDDINGS_OPTIONS} when `options` does not satisfy\n   *   {@link @nhtio/adk/batteries/embeddings/openai/validation!openAIEmbeddingsOptionsSchema} (e.g. missing `model`).\n   */\n  constructor(options: unknown) {\n    this.#options = validateOptions(options)\n  }\n\n  /** Declared output dimensionality (from options), or `undefined` if not configured. */\n  get dimensions(): number | undefined {\n    return this.#options.dimensions\n  }\n\n  /** See {@link OpenAIEmbeddingsAdapter.isAvailable}. Instance alias for surface-parity. */\n  isAvailable(): boolean {\n    return OpenAIEmbeddingsAdapter.isAvailable()\n  }\n\n  /**\n   * No-op warm-up. The OpenAI battery has no engine to preload; present for surface-parity with\n   * the WebLLM battery so callers can treat the two interchangeably.\n   */\n  async preload(): Promise<void> {\n    // intentionally empty — nothing to warm for an HTTP-backed battery\n  }\n\n  /**\n   * No-op state reset. Present for surface-parity with the WebLLM battery.\n   */\n  reset(): void {\n    // intentionally empty — the OpenAI battery holds no engine state\n  }\n\n  /**\n   * Embeds a single string.\n   *\n   * @param text - The input text.\n   * @param opts - Per-call options (`kind`).\n   * @returns The embedding vector as a plain `number[]`.\n   */\n  async embed(text: string, opts?: EmbedOptions): Promise<number[]> {\n    const [vec] = await this.embedMany([text], opts)\n    return vec\n  }\n\n  /**\n   * Embeds a batch of strings in a single request.\n   *\n   * @param texts - The input texts.\n   * @param opts - Per-call options (`kind`). Defaults to `kind: 'document'`.\n   * @returns One embedding vector per input, in input order, each a plain `number[]`.\n   * @throws {@link @nhtio/adk/batteries/embeddings/openai/exceptions!E_OPENAI_EMBEDDINGS_HTTP_ERROR} on a non-2xx response or transport failure.\n   * @throws {@link @nhtio/adk/batteries/embeddings/openai/exceptions!E_OPENAI_EMBEDDINGS_REQUEST_TIMEOUT} when the handshake exceeds `requestTimeoutMs`.\n   * @throws {@link @nhtio/adk/batteries/embeddings/openai/exceptions!E_OPENAI_EMBEDDINGS_MALFORMED_RESPONSE} when the 2xx body is not the expected shape.\n   */\n  async embedMany(texts: string[], opts?: EmbedOptions): Promise<number[][]> {\n    if (texts.length === 0) return []\n    const kind = opts?.kind ?? 'document'\n    const input = applyEmbeddingPrefix(texts, kind, this.#options)\n\n    const body: OpenAIEmbeddingsRequestBody = {\n      model: this.#options.model,\n      input,\n      encoding_format: 'float',\n      ...(this.#options.dimensions !== undefined ? { dimensions: this.#options.dimensions } : {}),\n    }\n\n    const rawBase = this.#options.baseURL ?? 'https://api.openai.com/v1'\n    const baseURL = rawBase.endsWith('/') ? rawBase.slice(0, -1) : rawBase\n    const url = `${baseURL}/embeddings`\n\n    const headers: Record<string, string> = { 'Content-Type': 'application/json' }\n    if (this.#options.apiKey) {\n      headers['Authorization'] = `Bearer ${this.#options.apiKey}`\n    }\n    if (this.#options.headers) {\n      Object.assign(headers, this.#options.headers)\n    }\n\n    const retryCfg: Required<EmbeddingsRetryConfig> = {\n      maxAttempts: this.#options.retry?.maxAttempts ?? 1,\n      baseDelayMs: this.#options.retry?.baseDelayMs ?? 500,\n      maxDelayMs: this.#options.retry?.maxDelayMs ?? 30_000,\n      retriableStatuses: this.#options.retry?.retriableStatuses ?? [429, 500, 502, 503, 504],\n      honorRetryAfter: this.#options.retry?.honorRetryAfter ?? true,\n    }\n\n    const fetchFn = this.#options.fetch ?? globalThis.fetch\n    const requestTimeoutMs = this.#options.requestTimeoutMs ?? 0\n    const maxAttempts = retryCfg.maxAttempts\n\n    let attempt = 1\n    while (attempt <= maxAttempts) {\n      const controller = new AbortController()\n      let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n      if (requestTimeoutMs > 0) {\n        timeoutHandle = setTimeout(() => controller.abort(), requestTimeoutMs)\n      }\n\n      let response: Response\n      try {\n        response = await fetchFn(url, {\n          method: 'POST',\n          headers,\n          body: JSON.stringify(body),\n          signal: controller.signal,\n        })\n      } catch (err) {\n        if (timeoutHandle !== undefined) clearTimeout(timeoutHandle)\n        if (controller.signal.aborted) {\n          // Timed out before headers — retry if attempts remain.\n          if (attempt < maxAttempts) {\n            await sleepWithJitter(computeBackoff(attempt, retryCfg))\n            attempt += 1\n            continue\n          }\n          throw new E_OPENAI_EMBEDDINGS_REQUEST_TIMEOUT([requestTimeoutMs])\n        }\n        // Generic transport failure — retry if attempts remain, else surface as status 0.\n        if (attempt < maxAttempts) {\n          await sleepWithJitter(computeBackoff(attempt, retryCfg))\n          attempt += 1\n          continue\n        }\n        throw new E_OPENAI_EMBEDDINGS_HTTP_ERROR([0, isError(err) ? err.message : String(err)])\n      }\n      if (timeoutHandle !== undefined) clearTimeout(timeoutHandle)\n\n      if (!response.ok) {\n        const status = response.status\n        const retriable = retryCfg.retriableStatuses.includes(status)\n        if (retriable && attempt < maxAttempts) {\n          let delay = computeBackoff(attempt, retryCfg)\n          if (retryCfg.honorRetryAfter) {\n            const ra = response.headers.get('Retry-After')\n            if (ra) {\n              const raMs = parseRetryAfter(ra)\n              if (raMs > 0) delay = Math.min(Math.max(delay, raMs), retryCfg.maxDelayMs)\n            }\n          }\n          await sleepWithJitter(delay)\n          attempt += 1\n          continue\n        }\n        const detail = await response.text().catch(() => '')\n        throw new E_OPENAI_EMBEDDINGS_HTTP_ERROR([status, detail])\n      }\n\n      let parsed: OpenAIEmbeddingsResponseBody\n      try {\n        parsed = (await response.json()) as OpenAIEmbeddingsResponseBody\n      } catch (err) {\n        throw new E_OPENAI_EMBEDDINGS_MALFORMED_RESPONSE([isError(err) ? err.message : String(err)])\n      }\n      if (!parsed || !Array.isArray(parsed.data) || parsed.data.length !== input.length) {\n        throw new E_OPENAI_EMBEDDINGS_MALFORMED_RESPONSE([\n          `expected ${input.length} vectors, got ${parsed?.data?.length ?? 'none'}`,\n        ])\n      }\n      // OpenAI may return data out of order; sort by `index` to restore input order.\n      return parsed.data\n        .slice()\n        .sort((a, b) => a.index - b.index)\n        .map((d) => d.embedding)\n    }\n\n    // Unreachable: the loop either returns or throws. Satisfies the type checker.\n    throw new E_OPENAI_EMBEDDINGS_HTTP_ERROR([0, 'retry loop exhausted without a response'])\n  }\n}\n"],"mappings":"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"}