Own provider file IDs and their cleanup
There is no provider-neutral files API in this release. File IDs, uploads, retention, and prompt references belong to the provider. Never reuse a file ID from one backend on another.
Use the narrow Codex file entry on your server. Configure a reusable CodexFilesClient with your server-owned API key and request timeout, then pass it to these helpers. They upload prepared OpenAI batch JSONL and remove a saved file ID in a later worker, without relying on an in-memory cleanup callback.
Download batch-files.ts. Both helpers borrow the file client; they do not create or dispose a runtime client.
import { ApiClientError, ApiClientErrorCode, ApiClientErrorType } from "@cavi-ai/api-client";
import type { CodexFilesClient } from "@cavi-ai/api-client/providers/codex/files";
export async function uploadBatchInput(files: CodexFilesClient, jsonl: string) {
if (!jsonl.trim()) throw new ApiClientError("Batch input is required.", {
type: ApiClientErrorType.Validation, code: ApiClientErrorCode.InvalidRequest,
});
const file = await files.uploadFile(jsonl, "batch", "input.jsonl");
if (typeof file?.id !== "string" || !file.id.trim()) {
throw new ApiClientError("Upload returned no usable file ID.", {
code: ApiClientErrorCode.ProtocolMismatch, cause: file,
});
}
return file;
}
export async function removeBatchInput(files: CodexFilesClient, fileId: string) {
if (!fileId.trim()) throw new ApiClientError("A saved file ID is required.", {
type: ApiClientErrorType.Validation, code: ApiClientErrorCode.InvalidRequest,
});
const receipt = await files.deleteFile(fileId);
if (receipt?.id !== fileId || receipt?.deleted !== true) {
throw new ApiClientError("Deletion was not confirmed for the saved file.", {
code: ApiClientErrorCode.ProtocolMismatch, cause: { fileId, receipt },
});
}
return { id: fileId, deleted: true as const };
}The JSONL must follow the provider's batch wire format; a serialized RuntimeBatchRequest[] is not that format. Prefer the facade's batch submission for its built-in request conversion.
Retain the ID with the owning job. Delete only after the provider no longer needs the file and your retention policy allows it. Disposing a runtime client does not delete uploaded files.
Keep cleanup explicit and durable
| Step | Application responsibility |
|---|---|
| Upload | Save the returned file ID and metadata with the job, provider, credential scope, and application owner |
| Use | Keep the file while the provider still needs it; runtime client shutdown is separate |
| Authorize cleanup | Load the saved record and check ownership and retention before passing its ID to removeBatchInput |
| Delete | Mark cleanup confirmed only after the helper returns its receipt |
| Cleanup failure | Retain the ID and failure state for reconciliation; do not mark it deleted |
The deletion helper requires deleted: true for the same saved ID. An absent, false, or mismatched acknowledgment raises ApiClientError with ProtocolMismatch, retaining the file ID and receipt in cause. Blank input or IDs raise InvalidRequest before a provider request. These are application checks; the underlying SDK file methods remain unchanged.
File methods are provider-specific raw APIs. HTTP, authentication, and unknown failures reject rather than returning facade gaps. Keep original diagnostics in protected telemetry and show application-owned messages to callers. A 404 does not satisfy this helper's confirmation policy; decide separately how your job system reconciles a file that is already absent.
Do not delete in an unconditional finally immediately after uploading input for an asynchronous batch. The remote job can outlive that request. If an upload response is lost or has no usable ID, do not replay it automatically: an uploaded file may already exist and require provider-side reconciliation. Failure to persist an acknowledged ID also needs reconciliation.
Run the file lifecycle tests
Download file-tests.ts beside the helpers in examples/. Use the ESM workspace and installation from consumer testing, then run:
npx tsc --target ES2022 --module NodeNext --moduleResolution NodeNext \
--strict --skipLibCheck --types node --outDir .file-tests \
examples/file-tests.ts examples/batch-files.ts
node --test .file-tests/file-tests.jsExpected result: seven passing tests without real credentials or network calls. HTTP fixtures exercise the installed public file client, multipart input, metadata retention, cleanup from a saved ID, invalid acknowledgments, and authentication/availability failures. The file client has no owned socket to close; each fixture creates an isolated client and finite HTTP responses.
Codex file methods · Gateway workspace access
Referenced examples
Verified example · TypeScript
Batch files
examples/batch-files.ts
import { ApiClientError, ApiClientErrorCode, ApiClientErrorType } from "@cavi-ai/api-client";
import type { CodexFilesClient } from "@cavi-ai/api-client/providers/codex/files";
export async function uploadBatchInput(files: CodexFilesClient, jsonl: string) {
if (!jsonl.trim()) throw new ApiClientError("Batch input is required.", {
type: ApiClientErrorType.Validation, code: ApiClientErrorCode.InvalidRequest,
});
const file = await files.uploadFile(jsonl, "batch", "input.jsonl");
if (typeof file?.id !== "string" || !file.id.trim()) {
throw new ApiClientError("Upload returned no usable file ID.", {
code: ApiClientErrorCode.ProtocolMismatch, cause: file,
});
}
return file;
}
export async function removeBatchInput(files: CodexFilesClient, fileId: string) {
if (!fileId.trim()) throw new ApiClientError("A saved file ID is required.", {
type: ApiClientErrorType.Validation, code: ApiClientErrorCode.InvalidRequest,
});
const receipt = await files.deleteFile(fileId);
if (receipt?.id !== fileId || receipt?.deleted !== true) {
throw new ApiClientError("Deletion was not confirmed for the saved file.", {
code: ApiClientErrorCode.ProtocolMismatch, cause: { fileId, receipt },
});
}
return { id: fileId, deleted: true as const };
}
Package imports
Referenced by
Artifact provenance
- Package
- @cavi-ai/api-client@0.21.0
- Release
- v0.21.0
- Commit
07f0ee45424c267278f7b0c89a7dcea4e2492cc1- Source
examples/batch-files.ts- Tarball SHA-256
0924b5d3a7a40869bd44fedc47a9b8eb9844c2af1fd5c7ca77469d8b7c0b8558- Content SHA-256
24eff463c41582eaf5e70c58f42e5ae13bd12d02fa14ae2a886afa3abf5cdf48
Verified example · TypeScript
File tests
examples/file-tests.ts
import assert from "node:assert/strict";
import { test } from "node:test";
import { isApiClientError, isAuthError } from "@cavi-ai/api-client";
import { CodexFilesClient } from "@cavi-ai/api-client/providers/codex/files";
import { uploadBatchInput, removeBatchInput } from "./batch-files.js";
function fixture(reply: (url: string, init?: RequestInit) => Response) {
const calls: { url: string; init?: RequestInit }[] = [];
const files = new CodexFilesClient({ apiKey: "fixture-key", defaultTimeoutMs: 1_000,
fetchImpl: async (url, init) => {
calls.push({ url: String(url), init });
return reply(String(url), init);
},
});
return { files, calls };
}
test("uploads provider JSONL as multipart and retains file metadata without deleting", async () => {
const app = fixture(() => Response.json({ id: "file-1", object: "file", bytes: 12 }));
const jsonl = '{"custom_id":"one","method":"POST","url":"/v1/responses","body":{"model":"server-model","input":"Question"}}';
const file = await uploadBatchInput(app.files, jsonl);
assert.deepEqual(file, { id: "file-1", object: "file", bytes: 12 });
assert.equal(app.calls.length, 1);
const { url, init } = app.calls[0];
assert.equal(url, "https://api.openai.com/v1/files");
assert.equal(init?.method, "POST");
assert.equal(new Headers(init?.headers).get("Authorization"), "Bearer fixture-key");
assert.ok(init?.body instanceof FormData);
assert.equal(init.body.get("purpose"), "batch");
const content = init.body.get("file");
assert.ok(content instanceof File);
assert.equal(content.name, "input.jsonl");
assert.equal(await content.text(), jsonl);
});
test("cleans up a persisted ID through a newly configured worker client", async () => {
const app = fixture(() => Response.json({ id: "file-1", deleted: true }));
const receipt = await removeBatchInput(app.files, "file-1");
assert.deepEqual(receipt, { id: "file-1", deleted: true });
assert.equal(app.calls.length, 1);
assert.equal(app.calls[0].url, "https://api.openai.com/v1/files/file-1");
assert.equal(app.calls[0].init?.method, "DELETE");
});
test("rejects blank input or IDs before provider work", async () => {
const app = fixture(() => { throw new Error("Invalid input reached the provider"); });
for (const value of ["", " ", "\n"]) {
await assert.rejects(uploadBatchInput(app.files, value), { code: "invalid_request" });
await assert.rejects(removeBatchInput(app.files, value), { code: "invalid_request" });
}
assert.equal(app.calls.length, 0);
});
test("rejects upload responses without a usable file ID", async () => {
for (const reply of [null, {}, { id: "" }, { id: " " }]) {
const app = fixture(() => Response.json(reply));
await assert.rejects(uploadBatchInput(app.files, "{}"), { code: "protocol_mismatch" });
assert.equal(app.calls.length, 1, "an ambiguous upload must not be replayed or deleted automatically");
}
});
test("requires deletion confirmation for the same saved ID", async () => {
for (const reply of [null, { id: "file-1", deleted: false }, { id: "foreign", deleted: true }, { deleted: true }]) {
const app = fixture(() => Response.json(reply));
await assert.rejects(removeBatchInput(app.files, "file-1"), (error) => {
assert.ok(isApiClientError(error));
assert.equal(error.code, "protocol_mismatch");
assert.equal((error.cause as { fileId: string }).fileId, "file-1");
return true;
});
assert.equal(app.calls.length, 1);
}
});
test("retains failed cleanup as an exception rather than reporting deletion", async () => {
const app = fixture(() => Response.json({}, { status: 503 }));
await assert.rejects(removeBatchInput(app.files, "file-1"), isApiClientError);
assert.equal(app.calls.length, 1);
});
test("keeps authentication failures as exceptions", async () => {
const app = fixture(() => Response.json({}, { status: 401 }));
await assert.rejects(uploadBatchInput(app.files, "{}"), isAuthError);
assert.equal(app.calls.length, 1);
});
Package imports
Referenced by
Artifact provenance
- Package
- @cavi-ai/api-client@0.21.0
- Release
- v0.21.0
- Commit
07f0ee45424c267278f7b0c89a7dcea4e2492cc1- Source
examples/file-tests.ts- Tarball SHA-256
0924b5d3a7a40869bd44fedc47a9b8eb9844c2af1fd5c7ca77469d8b7c0b8558- Content SHA-256
24eff463c41582eaf5e70c58f42e5ae13bd12d02fa14ae2a886afa3abf5cdf48