refactor(core): extract shared dedup helpers

This commit is contained in:
Peter Steinberger
2026-03-07 10:40:49 +00:00
parent 14c61bb33f
commit 3c71e2bd48
114 changed files with 3400 additions and 2040 deletions

View File

@@ -1,6 +1,12 @@
export { extractBatchErrorMessage, formatUnavailableBatchError } from "./batch-error-utils.js";
export { postJsonWithRetry } from "./batch-http.js";
export { applyEmbeddingBatchOutputLine } from "./batch-output.js";
export {
resolveBatchCompletionFromStatus,
resolveCompletedBatchResult,
throwIfBatchTerminalFailure,
type BatchCompletionResult,
} from "./batch-status.js";
export {
EMBEDDING_BATCH_ENDPOINT,
type EmbeddingBatchStatus,

View File

@@ -7,9 +7,13 @@ import {
formatUnavailableBatchError,
normalizeBatchBaseUrl,
postJsonWithRetry,
resolveBatchCompletionFromStatus,
resolveCompletedBatchResult,
runEmbeddingBatchGroups,
throwIfBatchTerminalFailure,
type EmbeddingBatchExecutionParams,
type EmbeddingBatchStatus,
type BatchCompletionResult,
type ProviderBatchOutputLine,
uploadBatchJsonlFile,
withRemoteHttpResponse,
@@ -144,7 +148,7 @@ async function waitForOpenAiBatch(params: {
timeoutMs: number;
debug?: (message: string, data?: Record<string, unknown>) => void;
initial?: OpenAiBatchStatus;
}): Promise<{ outputFileId: string; errorFileId?: string }> {
}): Promise<BatchCompletionResult> {
const start = Date.now();
let current: OpenAiBatchStatus | undefined = params.initial;
while (true) {
@@ -156,21 +160,21 @@ async function waitForOpenAiBatch(params: {
}));
const state = status.status ?? "unknown";
if (state === "completed") {
if (!status.output_file_id) {
throw new Error(`openai batch ${params.batchId} completed without output file`);
}
return {
outputFileId: status.output_file_id,
errorFileId: status.error_file_id ?? undefined,
};
}
if (["failed", "expired", "cancelled", "canceled"].includes(state)) {
const detail = status.error_file_id
? await readOpenAiBatchError({ openAi: params.openAi, errorFileId: status.error_file_id })
: undefined;
const suffix = detail ? `: ${detail}` : "";
throw new Error(`openai batch ${params.batchId} ${state}${suffix}`);
return resolveBatchCompletionFromStatus({
provider: "openai",
batchId: params.batchId,
status,
});
}
await throwIfBatchTerminalFailure({
provider: "openai",
status: { ...status, id: params.batchId },
readError: async (errorFileId) =>
await readOpenAiBatchError({
openAi: params.openAi,
errorFileId,
}),
});
if (!params.wait) {
throw new Error(`openai batch ${params.batchId} still ${state}; wait disabled`);
}
@@ -204,6 +208,7 @@ export async function runOpenAiEmbeddingBatches(
if (!batchInfo.id) {
throw new Error("openai batch create failed: missing batch id");
}
const batchId = batchInfo.id;
params.debug?.("memory embeddings: openai batch created", {
batchId: batchInfo.id,
@@ -213,30 +218,21 @@ export async function runOpenAiEmbeddingBatches(
requests: group.length,
});
if (!params.wait && batchInfo.status !== "completed") {
throw new Error(
`openai batch ${batchInfo.id} submitted; enable remote.batch.wait to await completion`,
);
}
const completed =
batchInfo.status === "completed"
? {
outputFileId: batchInfo.output_file_id ?? "",
errorFileId: batchInfo.error_file_id ?? undefined,
}
: await waitForOpenAiBatch({
openAi: params.openAi,
batchId: batchInfo.id,
wait: params.wait,
pollIntervalMs: params.pollIntervalMs,
timeoutMs: params.timeoutMs,
debug: params.debug,
initial: batchInfo,
});
if (!completed.outputFileId) {
throw new Error(`openai batch ${batchInfo.id} completed without output file`);
}
const completed = await resolveCompletedBatchResult({
provider: "openai",
status: batchInfo,
wait: params.wait,
waitForBatch: async () =>
await waitForOpenAiBatch({
openAi: params.openAi,
batchId,
wait: params.wait,
pollIntervalMs: params.pollIntervalMs,
timeoutMs: params.timeoutMs,
debug: params.debug,
initial: batchInfo,
}),
});
const content = await fetchOpenAiFileContent({
openAi: params.openAi,

View File

@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import {
resolveBatchCompletionFromStatus,
resolveCompletedBatchResult,
throwIfBatchTerminalFailure,
} from "./batch-status.js";
describe("batch-status helpers", () => {
it("resolves completion payload from completed status", () => {
expect(
resolveBatchCompletionFromStatus({
provider: "openai",
batchId: "b1",
status: {
output_file_id: "out-1",
error_file_id: "err-1",
},
}),
).toEqual({
outputFileId: "out-1",
errorFileId: "err-1",
});
});
it("throws for terminal failure states", async () => {
await expect(
throwIfBatchTerminalFailure({
provider: "voyage",
status: { id: "b2", status: "failed", error_file_id: "err-file" },
readError: async () => "bad input",
}),
).rejects.toThrow("voyage batch b2 failed: bad input");
});
it("returns completed result directly without waiting", async () => {
const waitForBatch = async () => ({ outputFileId: "out-2" });
const result = await resolveCompletedBatchResult({
provider: "openai",
status: {
id: "b3",
status: "completed",
output_file_id: "out-3",
},
wait: false,
waitForBatch,
});
expect(result).toEqual({ outputFileId: "out-3", errorFileId: undefined });
});
it("throws when wait disabled and batch is not complete", async () => {
await expect(
resolveCompletedBatchResult({
provider: "openai",
status: { id: "b4", status: "pending" },
wait: false,
waitForBatch: async () => ({ outputFileId: "out" }),
}),
).rejects.toThrow("openai batch b4 submitted; enable remote.batch.wait to await completion");
});
});

View File

@@ -0,0 +1,69 @@
const TERMINAL_FAILURE_STATES = new Set(["failed", "expired", "cancelled", "canceled"]);
type BatchStatusLike = {
id?: string;
status?: string;
output_file_id?: string | null;
error_file_id?: string | null;
};
export type BatchCompletionResult = {
outputFileId: string;
errorFileId?: string;
};
export function resolveBatchCompletionFromStatus(params: {
provider: string;
batchId: string;
status: BatchStatusLike;
}): BatchCompletionResult {
if (!params.status.output_file_id) {
throw new Error(`${params.provider} batch ${params.batchId} completed without output file`);
}
return {
outputFileId: params.status.output_file_id,
errorFileId: params.status.error_file_id ?? undefined,
};
}
export async function throwIfBatchTerminalFailure(params: {
provider: string;
status: BatchStatusLike;
readError: (errorFileId: string) => Promise<string | undefined>;
}): Promise<void> {
const state = params.status.status ?? "unknown";
if (!TERMINAL_FAILURE_STATES.has(state)) {
return;
}
const detail = params.status.error_file_id
? await params.readError(params.status.error_file_id)
: undefined;
const suffix = detail ? `: ${detail}` : "";
throw new Error(`${params.provider} batch ${params.status.id ?? "<unknown>"} ${state}${suffix}`);
}
export async function resolveCompletedBatchResult(params: {
provider: string;
status: BatchStatusLike;
wait: boolean;
waitForBatch: () => Promise<BatchCompletionResult>;
}): Promise<BatchCompletionResult> {
const batchId = params.status.id ?? "<unknown>";
if (!params.wait && params.status.status !== "completed") {
throw new Error(
`${params.provider} batch ${batchId} submitted; enable remote.batch.wait to await completion`,
);
}
const completed =
params.status.status === "completed"
? resolveBatchCompletionFromStatus({
provider: params.provider,
batchId,
status: params.status,
})
: await params.waitForBatch();
if (!completed.outputFileId) {
throw new Error(`${params.provider} batch ${batchId} completed without output file`);
}
return completed;
}

View File

@@ -9,9 +9,13 @@ import {
formatUnavailableBatchError,
normalizeBatchBaseUrl,
postJsonWithRetry,
resolveBatchCompletionFromStatus,
resolveCompletedBatchResult,
runEmbeddingBatchGroups,
throwIfBatchTerminalFailure,
type EmbeddingBatchExecutionParams,
type EmbeddingBatchStatus,
type BatchCompletionResult,
type ProviderBatchOutputLine,
uploadBatchJsonlFile,
withRemoteHttpResponse,
@@ -146,7 +150,7 @@ async function waitForVoyageBatch(params: {
timeoutMs: number;
debug?: (message: string, data?: Record<string, unknown>) => void;
initial?: VoyageBatchStatus;
}): Promise<{ outputFileId: string; errorFileId?: string }> {
}): Promise<BatchCompletionResult> {
const start = Date.now();
let current: VoyageBatchStatus | undefined = params.initial;
while (true) {
@@ -158,21 +162,21 @@ async function waitForVoyageBatch(params: {
}));
const state = status.status ?? "unknown";
if (state === "completed") {
if (!status.output_file_id) {
throw new Error(`voyage batch ${params.batchId} completed without output file`);
}
return {
outputFileId: status.output_file_id,
errorFileId: status.error_file_id ?? undefined,
};
}
if (["failed", "expired", "cancelled", "canceled"].includes(state)) {
const detail = status.error_file_id
? await readVoyageBatchError({ client: params.client, errorFileId: status.error_file_id })
: undefined;
const suffix = detail ? `: ${detail}` : "";
throw new Error(`voyage batch ${params.batchId} ${state}${suffix}`);
return resolveBatchCompletionFromStatus({
provider: "voyage",
batchId: params.batchId,
status,
});
}
await throwIfBatchTerminalFailure({
provider: "voyage",
status: { ...status, id: params.batchId },
readError: async (errorFileId) =>
await readVoyageBatchError({
client: params.client,
errorFileId,
}),
});
if (!params.wait) {
throw new Error(`voyage batch ${params.batchId} still ${state}; wait disabled`);
}
@@ -206,6 +210,7 @@ export async function runVoyageEmbeddingBatches(
if (!batchInfo.id) {
throw new Error("voyage batch create failed: missing batch id");
}
const batchId = batchInfo.id;
params.debug?.("memory embeddings: voyage batch created", {
batchId: batchInfo.id,
@@ -215,30 +220,21 @@ export async function runVoyageEmbeddingBatches(
requests: group.length,
});
if (!params.wait && batchInfo.status !== "completed") {
throw new Error(
`voyage batch ${batchInfo.id} submitted; enable remote.batch.wait to await completion`,
);
}
const completed =
batchInfo.status === "completed"
? {
outputFileId: batchInfo.output_file_id ?? "",
errorFileId: batchInfo.error_file_id ?? undefined,
}
: await waitForVoyageBatch({
client: params.client,
batchId: batchInfo.id,
wait: params.wait,
pollIntervalMs: params.pollIntervalMs,
timeoutMs: params.timeoutMs,
debug: params.debug,
initial: batchInfo,
});
if (!completed.outputFileId) {
throw new Error(`voyage batch ${batchInfo.id} completed without output file`);
}
const completed = await resolveCompletedBatchResult({
provider: "voyage",
status: batchInfo,
wait: params.wait,
waitForBatch: async () =>
await waitForVoyageBatch({
client: params.client,
batchId,
wait: params.wait,
pollIntervalMs: params.pollIntervalMs,
timeoutMs: params.timeoutMs,
debug: params.debug,
initial: batchInfo,
}),
});
const baseUrl = normalizeBatchBaseUrl(params.client);
const errors: string[] = [];