openai.d.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /**
  2. * openai.ts - OpenAI-compatible HTTP embedding provider
  3. *
  4. * Talks to any endpoint that implements `POST /v1/embeddings` with the OpenAI
  5. * shape: request `{model, input: string|string[]}`, response
  6. * `{data: [{embedding: number[], index: number}, ...]}`.
  7. *
  8. * Used by qmd to delegate embeddings to an approved commercial HTTPS API.
  9. *
  10. * Features:
  11. * - Batches input in groups of ≤64 (configurable via QMD_EMBED_BATCH_SIZE)
  12. * - Retries 429 / 503 with exponential backoff (1s, 4s, 16s)
  13. * - 4xx (non-429) → no retry, count as failure
  14. * - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min,
  15. * callers receive failures; local fallback is forbidden
  16. * - Per-call timeout via AbortSignal (default QMD_EMBED_TIMEOUT_MS=30000)
  17. * - Healthcheck via `GET /health` if available, else a probe embed call
  18. */
  19. import type { EmbeddingProvider, ProviderEmbedOptions, ProviderEmbedding, ProviderHealth, ProviderKind } from "./provider.js";
  20. /**
  21. * Default batch size — most OpenAI-compatible embedding endpoints accept up to
  22. * 2048 inputs per call but for memory and latency we cap at 64.
  23. */
  24. export declare const DEFAULT_BATCH_SIZE = 64;
  25. /**
  26. * Default in-flight concurrency cap for `embedBatch`. The qmd-embed-worker
  27. * exposes a 4-way semaphore (`MAX_CONCURRENT_REQUESTS=4`) and idles at
  28. * queue-depth 1.0 under sequential clients (i-fkpnar9i baseline). Defaulting
  29. * to 4 matches the worker's advertised concurrency without overshooting the
  30. * GPU. Override per-deploy via `QMD_EMBED_CONCURRENCY`. Setting to 1 reverts
  31. * to the legacy sequential dispatch.
  32. */
  33. export declare const DEFAULT_CONCURRENCY = 4;
  34. /**
  35. * Default per-request timeout (30 s). embeddinggemma-300M on RTX 4090 takes
  36. * <500ms per batch of 64 in practice; 30s is a safe upper bound.
  37. */
  38. export declare const DEFAULT_TIMEOUT_MS = 30000;
  39. /**
  40. * Retry backoff schedule (ms) for 429/503 responses. 3 attempts total
  41. * (initial + 2 retries) — aligns with issue spec "1s/4s/16s".
  42. */
  43. export declare const RETRY_BACKOFFS_MS: readonly number[];
  44. /**
  45. * Circuit breaker — flips OPEN when error rate exceeds threshold within
  46. * window. While OPEN, every call fails fast so the caller can fall back.
  47. */
  48. export declare const CIRCUIT_WINDOW_MS = 60000;
  49. export declare const CIRCUIT_OPEN_DURATION_MS: number;
  50. export declare const CIRCUIT_FAILURE_RATE_THRESHOLD = 0.5;
  51. export declare const CIRCUIT_MIN_SAMPLES = 4;
  52. export type OpenAIProviderConfig = {
  53. /** Endpoint base URL — e.g. "https://ai.mm.mk" (no trailing slash) */
  54. endpoint: string;
  55. /** Optional bearer token sent as `Authorization: Bearer ...` */
  56. apiKey?: string;
  57. /**
  58. * Stable model identifier to report up via `getModelId()`.
  59. * Defaults to "embeddinggemma" to match qmd's existing DB rows.
  60. */
  61. modelId?: string;
  62. /**
  63. * Upstream model name sent in the HTTP request body. Often differs from
  64. * `modelId` (e.g. modelId="embeddinggemma" but upstream model="embeddinggemma:300m").
  65. */
  66. upstreamModel?: string;
  67. /** Batch size cap (default DEFAULT_BATCH_SIZE = 64) */
  68. batchSize?: number;
  69. /**
  70. * Max in-flight HTTP requests during a single `embedBatch` call. Default
  71. * `DEFAULT_CONCURRENCY=4` matches the worker semaphore. Set to 1 to force
  72. * legacy sequential dispatch (useful for benchmarks / regression bisect).
  73. */
  74. concurrency?: number;
  75. /** Per-request timeout in ms (default DEFAULT_TIMEOUT_MS = 30_000) */
  76. timeoutMs?: number;
  77. /** Custom fetch (for testing). Defaults to global `fetch`. */
  78. fetchImpl?: typeof fetch;
  79. /** Custom retry schedule (for testing). Defaults to RETRY_BACKOFFS_MS. */
  80. retryBackoffsMs?: readonly number[];
  81. /** Custom sleep impl (for testing). Defaults to setTimeout. */
  82. sleep?: (ms: number) => Promise<void>;
  83. /** Custom clock (for testing). Defaults to Date.now. */
  84. now?: () => number;
  85. };
  86. export type OpenAIEmbeddingsResponse = {
  87. object?: string;
  88. model?: string;
  89. data: Array<{
  90. object?: string;
  91. index: number;
  92. embedding: number[];
  93. }>;
  94. usage?: {
  95. prompt_tokens?: number;
  96. total_tokens?: number;
  97. };
  98. };
  99. /**
  100. * Circuit breaker state — exported for tests
  101. */
  102. export type CircuitState = "closed" | "open" | "half-open";
  103. /**
  104. * Determine whether an HTTP status is retryable. 429 (Too Many Requests)
  105. * and 503 (Service Unavailable) are retried; 4xx (other than 429) are not.
  106. */
  107. export declare function isRetryableStatus(status: number): boolean;
  108. /**
  109. * Chunk an array into pieces of ≤ size each. `size` MUST be ≥ 1.
  110. */
  111. export declare function chunkArray<T>(items: T[], size: number): T[][];
  112. /**
  113. * Sliding-window circuit breaker. Tracks the last N samples (min 4) over a
  114. * 60-second window; flips OPEN when failure rate exceeds 50%, then auto-
  115. * resets to HALF-OPEN after 5 minutes — at which point the next probe
  116. * decides whether to close (success) or re-open (failure).
  117. */
  118. export declare class CircuitBreaker {
  119. private samples;
  120. private state;
  121. private openedAt;
  122. private readonly windowMs;
  123. private readonly openDurationMs;
  124. private readonly threshold;
  125. private readonly minSamples;
  126. private readonly now;
  127. constructor(opts?: {
  128. windowMs?: number;
  129. openDurationMs?: number;
  130. threshold?: number;
  131. minSamples?: number;
  132. now?: () => number;
  133. });
  134. getState(): CircuitState;
  135. /**
  136. * Returns true when calls should be short-circuited (skip HTTP, fall back).
  137. * Side-effects: may transition OPEN → HALF-OPEN if the open window expired.
  138. */
  139. shouldFailFast(): boolean;
  140. /** Record a successful call. */
  141. recordSuccess(): void;
  142. /** Record a failed call. May trigger OPEN. */
  143. recordFailure(): void;
  144. /** Force-reset the breaker (used by tests / admin) */
  145. reset(): void;
  146. private pushSample;
  147. private evaluate;
  148. private tickAutoReset;
  149. }
  150. /**
  151. * Raised when the circuit breaker is OPEN and a call is short-circuited.
  152. * Callers (e.g. fallback wrapper) can catch this to switch to local provider.
  153. */
  154. export declare class CircuitOpenError extends Error {
  155. constructor(message?: string);
  156. }
  157. /**
  158. * Persistent (non-retryable) HTTP error from upstream. Includes status code.
  159. */
  160. export declare class HttpError extends Error {
  161. readonly status: number;
  162. readonly bodyPreview: string;
  163. constructor(status: number, bodyPreview: string);
  164. }
  165. export declare class OpenAIEmbeddingsProvider implements EmbeddingProvider {
  166. readonly kind: ProviderKind;
  167. private readonly endpoint;
  168. private readonly apiKey?;
  169. private readonly modelId;
  170. private readonly upstreamModel;
  171. private readonly batchSize;
  172. private readonly concurrency;
  173. private readonly timeoutMs;
  174. private readonly fetchImpl;
  175. private readonly retryBackoffsMs;
  176. private readonly sleep;
  177. private readonly now;
  178. private dimensions;
  179. private lastError;
  180. readonly breaker: CircuitBreaker;
  181. constructor(config: OpenAIProviderConfig);
  182. getModelId(): string;
  183. getDimensions(): number | undefined;
  184. /**
  185. * Most recent per-chunk failure message (HTTP status + body preview, malformed
  186. * JSON, timeout, abort reason). Returns `undefined` after a successful call
  187. * or before the first call. See `EmbeddingProvider.getLastError`.
  188. */
  189. getLastError(): string | undefined;
  190. /** Endpoint URL configured at construction time — used by callers when
  191. * building error messages for failed first-chunk probes. */
  192. getEndpoint(): string;
  193. healthcheck(signal?: AbortSignal): Promise<ProviderHealth>;
  194. embed(text: string, options?: ProviderEmbedOptions): Promise<ProviderEmbedding | null>;
  195. embedBatch(texts: string[], options?: ProviderEmbedOptions): Promise<(ProviderEmbedding | null)[]>;
  196. dispose(): Promise<void>;
  197. /**
  198. * Format a request-failure context string for `lastError`. Includes endpoint
  199. * + HTTP status + body preview when the error was an `HttpError`, otherwise
  200. * falls back to the message of the underlying error (or the value itself
  201. * when not an Error). Kept short — body preview is already capped at 1024
  202. * chars by `HttpError`, but we trim further here for the dimension-probe
  203. * thrown error which surfaces directly to users.
  204. */
  205. private formatErrorContext;
  206. private buildHeaders;
  207. /**
  208. * Single HTTP request with retry on 429/503. Returns embeddings indexed
  209. * the same as `texts`. Throws on non-retryable failure or all attempts
  210. * exhausted.
  211. */
  212. private requestWithRetry;
  213. /**
  214. * Issue one HTTP attempt to `POST /v1/embeddings`. Does NOT retry.
  215. */
  216. private requestOnce;
  217. }