|
@@ -3,7 +3,9 @@
|
|
|
*
|
|
*
|
|
|
* Talks to any endpoint that implements `POST /v1/embeddings` with the OpenAI
|
|
* Talks to any endpoint that implements `POST /v1/embeddings` with the OpenAI
|
|
|
* shape: request `{model, input: string|string[]}`, response
|
|
* shape: request `{model, input: string|string[]}`, response
|
|
|
- * `{data: [{embedding: number[], index: number}, ...]}`.
|
|
|
|
|
|
|
+ * `{data: [{embedding: number[], index?: number}, ...]}`.
|
|
|
|
|
+ * `index` is optional: Gemini's OpenAI-compat layer omits it and returns
|
|
|
|
|
+ * embeddings in input order (session 7fcfc297). Missing index → array position.
|
|
|
*
|
|
*
|
|
|
* Used by qmd to delegate embeddings to an approved commercial HTTPS API.
|
|
* Used by qmd to delegate embeddings to an approved commercial HTTPS API.
|
|
|
*
|
|
*
|
|
@@ -78,6 +80,14 @@ export const CIRCUIT_WINDOW_MS = 60_000;
|
|
|
export const CIRCUIT_OPEN_DURATION_MS = 5 * 60_000;
|
|
export const CIRCUIT_OPEN_DURATION_MS = 5 * 60_000;
|
|
|
export const CIRCUIT_FAILURE_RATE_THRESHOLD = 0.5;
|
|
export const CIRCUIT_FAILURE_RATE_THRESHOLD = 0.5;
|
|
|
export const CIRCUIT_MIN_SAMPLES = 4;
|
|
export const CIRCUIT_MIN_SAMPLES = 4;
|
|
|
|
|
+/** Map an OpenAI-compat embedding row onto the input slot. Missing `index` = row order. */
|
|
|
|
|
+export function resolveOpenAIEmbeddingIndex(index, position, inputCount) {
|
|
|
|
|
+ const idx = typeof index === "number" ? index : position;
|
|
|
|
|
+ if (!Number.isInteger(idx) || idx < 0 || idx >= inputCount) {
|
|
|
|
|
+ throw new Error(`OpenAIEmbeddingsProvider: data item index out of range (${String(index)}, expected 0..${inputCount - 1})`);
|
|
|
|
|
+ }
|
|
|
|
|
+ return idx;
|
|
|
|
|
+}
|
|
|
// ─────────────────────────── Helpers ─────────────────────────────────────────
|
|
// ─────────────────────────── Helpers ─────────────────────────────────────────
|
|
|
function defaultSleep(ms) {
|
|
function defaultSleep(ms) {
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -823,18 +833,16 @@ export class OpenAIEmbeddingsProvider {
|
|
|
if (!parsed || !Array.isArray(parsed.data)) {
|
|
if (!parsed || !Array.isArray(parsed.data)) {
|
|
|
throw new Error(`OpenAIEmbeddingsProvider: response missing "data" array (got ${typeof parsed})`);
|
|
throw new Error(`OpenAIEmbeddingsProvider: response missing "data" array (got ${typeof parsed})`);
|
|
|
}
|
|
}
|
|
|
- // Sort by index to match input order (in case server returns out-of-order).
|
|
|
|
|
|
|
+ // Prefer `index` when present (out-of-order servers). Gemini's
|
|
|
|
|
+ // OpenAI-compat layer omits `index` and returns rows in input order.
|
|
|
const out = new Array(texts.length);
|
|
const out = new Array(texts.length);
|
|
|
- for (const item of parsed.data) {
|
|
|
|
|
- if (typeof item.index !== "number" ||
|
|
|
|
|
- item.index < 0 ||
|
|
|
|
|
- item.index >= texts.length) {
|
|
|
|
|
- throw new Error(`OpenAIEmbeddingsProvider: data item index out of range (${item.index}, expected 0..${texts.length - 1})`);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ for (let i = 0; i < parsed.data.length; i++) {
|
|
|
|
|
+ const item = parsed.data[i];
|
|
|
|
|
+ const idx = resolveOpenAIEmbeddingIndex(item.index, i, texts.length);
|
|
|
if (!Array.isArray(item.embedding)) {
|
|
if (!Array.isArray(item.embedding)) {
|
|
|
- throw new Error(`OpenAIEmbeddingsProvider: data[${item.index}].embedding is not an array`);
|
|
|
|
|
|
|
+ throw new Error(`OpenAIEmbeddingsProvider: data[${idx}].embedding is not an array`);
|
|
|
}
|
|
}
|
|
|
- out[item.index] = item.embedding;
|
|
|
|
|
|
|
+ out[idx] = item.embedding;
|
|
|
}
|
|
}
|
|
|
// Sanity check — every slot must be filled
|
|
// Sanity check — every slot must be filled
|
|
|
for (let i = 0; i < texts.length; i++) {
|
|
for (let i = 0; i < texts.length; i++) {
|