refactor(core): extract shared dedup helpers
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
60
src/memory/batch-status.test.ts
Normal file
60
src/memory/batch-status.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
69
src/memory/batch-status.ts
Normal file
69
src/memory/batch-status.ts
Normal 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;
|
||||
}
|
||||
@@ -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[] = [];
|
||||
|
||||
Reference in New Issue
Block a user