Skip to content

AI4_filestore.kt

src/jvmMain/kotlin/afield/ai/knowledge/AI4_filestore.kt · 821 lines

@file:Suppress("MemberVisibilityCanBePrivate", "unused")

package afield.ai.knowledge

import afield.ai.conversation.*
import afield.ai.models.*
import afield.document.workspace.VirtualTextFile
import afield.graphics.render.*
import afield.app.diagnostics.logging.lprintln
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.Instant
import java.util.*
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write

A local cache that maps file contents to OpenAI file IDs, so the same bytes are uploaded once per API key.

OpenAiFileStoreCache keys each record by a SHA-256 of the content and a peppered hash of the API key (the key itself is never stored), keeps the database as JSON, and writes it atomically under a read-write lock. uploadIfNeeded returns an existing file ID when the content is already known, and DuplicatePolicy decides what a second upload of known content does. OpenAiFileUploader is the interface an upload goes through; OkHttpOpenAiFileUploader is a minimal multipart client for /v1/files, and uploadVirtualFiles pushes Field virtual files through the same path.

OpenAiKnowledgeStore is a second, registry-style store over the Files and Vector Stores REST endpoints. The command-line driver for the cache is AI4FilestoreCli.kt.

Hash utilities

object Sha256

SHA-256 digests as lowercase hex strings.

fun hex(bytes: ByteArray): String

    fun hex(bytes: ByteArray): String {
        val md = MessageDigest.getInstance("SHA-256")
        val digest = md.digest(bytes)
        return digest.joinToString("") { "%02x".format(it) }
    }

fun hexUtf8(s: String): String = hex(s.toByteArray(StandardCharsets.UTF_8))

Persistence model

enum class DuplicatePolicy

enum class DuplicatePolicy {

If contentHash already exists for this keyHash, return existing mapping (do not add new record).

    REUSE_EXISTING,

If contentHash already exists for this keyHash, append another record (keeps multiple fileIds).

    APPEND,

If contentHash already exists for this keyHash, replace existing records with the new record.

    OVERWRITE,

If contentHash already exists for this keyHash, throw.

    ERROR
}

data class CachePutResult

data class CachePutResult(
    val contentHash: String,
    val keyHash: String,
    val record: FileRecord,
    val action: String // "added", "reused", "appended", "overwritten"
)

data class CacheLookupResult

data class CacheLookupResult(
    val contentHash: String, val keyHash: String, val records: List<FileRecord>
)

data class FileRecord

@JsonIgnoreProperties(ignoreUnknown = true)
data class FileRecord(
    val fileId: String,
    val fileName: String,
    val purpose: String,
    val byteCount: Long,
    val createdAtEpochMs: Long = Instant.now().toEpochMilli()
)

data class FileStoreCacheDb

@JsonIgnoreProperties(ignoreUnknown = true)
data class FileStoreCacheDb(
    val version: Int = 1,

    /**
     * Optional "pepper id" so you can rotate peppers if you want.
     * Keep it stable if you want stable key hashes.
     */
    val pepperId: String = "v1",

    /**
     * byKey[keyHash][contentHash] = list of file records (most recent last).
     */
    val byKey: MutableMap<String, MutableMap<String, MutableList<FileRecord>>> = mutableMapOf()
)

FileStore cache (thread-safe + atomic writes)

class OpenAiFileStoreCache

class OpenAiFileStoreCache(
    private val dbPath: Path,
    private val pepper: String = "afield-openai-keyhash-pepper:v1",
    private val mapper: ObjectMapper = defaultMapper()
) {
    private val lock = ReentrantReadWriteLock()

    @Volatile
    private var db: FileStoreCacheDb = loadOrCreate()

fun keyHash(openAiApiKey: String): String = Sha256.hexUtf8("$pepper|$openAiApiKey")

fun contentHash(contentBytes: ByteArray): String = Sha256.hex(contentBytes)

fun lookup(openAiApiKey: String, contentBytes: ByteArray): CacheLookupResult

    fun lookup(openAiApiKey: String, contentBytes: ByteArray): CacheLookupResult {
        val kHash = keyHash(openAiApiKey)
        val cHash = contentHash(contentBytes)
        val records = lock.read {
            db.byKey[kHash]?.get(cHash)?.toList().orEmpty()
        }
        return CacheLookupResult(contentHash = cHash, keyHash = kHash, records = records)
    }

fun fileIdsForContent(openAiApiKey: String, contentBytes: ByteArray): List<String>

    fun fileIdsForContent(openAiApiKey: String, contentBytes: ByteArray): List<String> =
        lookup(openAiApiKey, contentBytes).records.map { it.fileId }

fun newestFileIdForContent(openAiApiKey: String, contentBytes: ByteArray): String?

    fun newestFileIdForContent(openAiApiKey: String, contentBytes: ByteArray): String? =
        lookup(openAiApiKey, contentBytes).records.lastOrNull()?.fileId

fun put

Record a mapping (content -> fileId) without uploading anything.

body — 54 lines, from line 157
    fun put(
        openAiApiKey: String,
        contentBytes: ByteArray,
        fileId: String,
        fileName: String,
        purpose: String,
        duplicatePolicy: DuplicatePolicy = DuplicatePolicy.REUSE_EXISTING
    ): CachePutResult {
        val kHash = keyHash(openAiApiKey)
        val cHash = contentHash(contentBytes)
        val newRecord = FileRecord(
            fileId = fileId, fileName = fileName, purpose = purpose, byteCount = contentBytes.size.toLong()
        )

        val result = lock.write {
            val byContent = db.byKey.getOrPut(kHash) { mutableMapOf() }
            val existing = byContent[cHash]

            when {
                existing == null -> {
                    byContent[cHash] = mutableListOf(newRecord)
                    persistLocked()
                    CachePutResult(cHash, kHash, newRecord, action = "added")
                }

                else -> when (duplicatePolicy) {
                    DuplicatePolicy.REUSE_EXISTING -> {
                        // return the newest existing record
                        CachePutResult(cHash, kHash, existing.last(), action = "reused")
                    }

                    DuplicatePolicy.APPEND -> {
                        existing += newRecord
                        persistLocked()
                        CachePutResult(cHash, kHash, newRecord, action = "appended")
                    }

                    DuplicatePolicy.OVERWRITE -> {
                        byContent[cHash] = mutableListOf(newRecord)
                        persistLocked()
                        CachePutResult(cHash, kHash, newRecord, action = "overwritten")
                    }

                    DuplicatePolicy.ERROR -> {
                        throw IllegalStateException(
                            "Content already exists for this keyHash. keyHash=$kHash contentHash=$cHash " + "existingFileIds=${existing.map { it.fileId }}"
                        )
                    }
                }
            }
        }

        return result
    }

fun uploadIfNeeded

High-level helper: - if content already uploaded for this key (per cache), return existing fileId - otherwise uploads then caches the returned fileId

body — 54 lines, from line 217
    fun uploadIfNeeded(
        openAiApiKey: String,
        fileName: String,
        contentBytes: ByteArray,
        purpose: String,
        uploader: OpenAiFileUploader,
        duplicatePolicy: DuplicatePolicy = DuplicatePolicy.REUSE_EXISTING
    ): CachePutResult {
        // Fast path: see if we already have something
        if (duplicatePolicy == DuplicatePolicy.REUSE_EXISTING) {
            val existing = newestFileIdForContent(openAiApiKey, contentBytes)
            if (existing != null) {
                val kHash = keyHash(openAiApiKey)
                val cHash = contentHash(contentBytes)

                lprintln("debug,filestore", "checking cache $fileName key=$kHash content=$cHash")
                val rec = lock.read {
                    db.byKey[kHash]?.get(cHash)?.lastOrNull()
                } ?: FileRecord(existing, fileName, purpose, contentBytes.size.toLong())
                return CachePutResult(cHash, kHash, rec, action = "reused")
            }
        }

        lprintln("info,filestore", "uploading $fileName")
        // Upload then record
        val fileId = uploader.upload(
            openAiApiKey = openAiApiKey, fileName = fileName, contentBytes = contentBytes, purpose = purpose
        )

        val kHash = keyHash(openAiApiKey)
        val cHash = contentHash(contentBytes)
        val rec = FileRecord(fileId, fileName, purpose, contentBytes.size.toLong())

        return try {
            put(
                openAiApiKey = openAiApiKey,
                contentBytes = contentBytes,
                fileId = fileId,
                fileName = fileName,
                purpose = purpose,
                duplicatePolicy = when (duplicatePolicy) {
                    DuplicatePolicy.REUSE_EXISTING -> DuplicatePolicy.OVERWRITE // after upload, prefer to store it
                    else -> duplicatePolicy
                }
            )
        } catch (e: Exception) {
            lock.write {
                val byContent = db.byKey.getOrPut(kHash) { mutableMapOf() }
                byContent[cHash] = mutableListOf(rec)
            }
            lprintln("error,filestore", "cache persist failed for $fileName: ${e.message}")
            CachePutResult(cHash, kHash, rec, action = "added-unpersisted")
        }
    }

fun removeByContent(openAiApiKey: String, contentBytes: ByteArray): Boolean

    fun removeByContent(openAiApiKey: String, contentBytes: ByteArray): Boolean {
        val kHash = keyHash(openAiApiKey)
        val cHash = contentHash(contentBytes)
        return lock.write {
            val removed = db.byKey[kHash]?.remove(cHash) != null
            if (removed) persistLocked()
            removed
        }
    }

fun snapshot(): FileStoreCacheDb = lock.read { db.copy(byKey = deepCopy(db.byKey)) }

fun flush()

    fun flush() {
        lock.write { persistLocked() }
    }

persistence

    private fun loadOrCreate(): FileStoreCacheDb {
        if (!Files.exists(dbPath)) return FileStoreCacheDb()
        return try {
            Files.newBufferedReader(dbPath, StandardCharsets.UTF_8).use { r ->
                mapper.readValue(r, FileStoreCacheDb::class.java)
            }
        } catch (e: Exception) {
            e.printStackTrace()
            // If corrupt, keep a backup and start fresh.
            val backup = dbPath.resolveSibling(dbPath.fileName.toString() + ".corrupt")
            try {
                Files.copy(dbPath, backup, StandardCopyOption.REPLACE_EXISTING)
            } catch (_: Exception) {
                // ignore
            }
            FileStoreCacheDb()
        }
    }

    private fun persistLocked() {
        // Atomic write: write temp then move/replace.
        Files.createDirectories(dbPath.parent)
        val tmp = dbPath.resolveSibling(dbPath.fileName.toString() + ".tmp")

        Files.newBufferedWriter(tmp, StandardCharsets.UTF_8).use { w ->
            mapper.writerWithDefaultPrettyPrinter().writeValue(w, db)
        }

        lprintln("debug,filestore", "persistLocked $tmp -> $dbPath entries=${db.byKey.size}")
        try {
            Files.move(
                tmp, dbPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE
            )
        } catch (_: Exception) {
            Files.move(tmp, dbPath, StandardCopyOption.REPLACE_EXISTING)
        }
    }

companion object

fun defaultMapper(): ObjectMapper

        fun defaultMapper(): ObjectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build())

        private fun deepCopy(
            src: MutableMap<String, MutableMap<String, MutableList<FileRecord>>>
        ): MutableMap<String, MutableMap<String, MutableList<FileRecord>>> {
            val out = mutableMapOf<String, MutableMap<String, MutableList<FileRecord>>>()
            for ((k, byContent) in src) {
                val byContentCopy = mutableMapOf<String, MutableList<FileRecord>>()
                for ((cHash, records) in byContent) {
                    byContentCopy[cHash] = records.toMutableList()
                }
                out[k] = byContentCopy
            }
            return out
        }
    }
}

Upload interface + OkHttp implementation (POST /v1/files)

fun interface OpenAiFileUploader

fun interface OpenAiFileUploader {

fun upload

Upload bytes as a File and return the OpenAI file id (e.g. "file-...").

    fun upload(
        openAiApiKey: String, fileName: String, contentBytes: ByteArray, purpose: String
    ): String
}

class OkHttpOpenAiFileUploader

Minimal multipart uploader for /v1/files.

  • purpose is typically "assistants" when you intend to attach to vector stores / assistants.
  • You can swap this for an openai-java SDK uploader later without touching the cache.
class OkHttpOpenAiFileUploader(
    private val http: OkHttpClient = OkHttpClient(),
    private val baseUrl: String = "https://api.openai.com/v1",
    private val mapper: ObjectMapper = OpenAiFileStoreCache.defaultMapper()
) : OpenAiFileUploader {

override fun upload

    override fun upload(
        openAiApiKey: String, fileName: String, contentBytes: ByteArray, purpose: String
    ): String {
        val fileBody = contentBytes.toRequestBody("application/octet-stream".toMediaType())

        val multipart = MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("purpose", purpose)
            .addFormDataPart("file", fileName, fileBody).build()

        val req =
            Request.Builder().url("$baseUrl/files").header("Authorization", "Bearer $openAiApiKey").post(multipart)
                .build()

        http.newCall(req).execute().use { resp ->
            val body = resp.body.string()
            if (!resp.isSuccessful) {
                throw IOException("File upload failed HTTP ${resp.code}. body=$body")
            }

            val json = mapper.readTree(body)
            val id = json["id"]?.asText()
            if (id.isNullOrBlank()) {
                throw IOException("File upload response missing 'id'. body=$body")
            }
            return id
        }
    }
}

data class VirtualUploadResult

data class VirtualUploadResult(
    val file: VirtualTextFile,
    val result: CachePutResult
)

fun uploadVirtualFiles

fun uploadVirtualFiles(
    cache: OpenAiFileStoreCache,
    uploader: OpenAiFileUploader,
    openAiApiKey: String,
    purpose: String,
    files: List<VirtualTextFile>,
    duplicatePolicy: DuplicatePolicy = DuplicatePolicy.REUSE_EXISTING
): List<VirtualUploadResult> {
    return files.map { file ->
        val bytes = file.content.toByteArray(StandardCharsets.UTF_8)
        val result = cache.uploadIfNeeded(
            openAiApiKey = openAiApiKey,
            fileName = file.ref.fileName,
            contentBytes = bytes,
            purpose = purpose,
            uploader = uploader,
            duplicatePolicy = duplicatePolicy
        )
        VirtualUploadResult(file, result)
    }
}

Public model: canonical bundle

data class CanonicalDoc

data class CanonicalDoc(
    val fileName: String, val content: String, val contentType: String = "text/markdown; charset=utf-8"
)

data class CanonicalBundle

data class CanonicalBundle(
    val name: String, val version: String, val docs: List<CanonicalDoc>
)

Disk cache schema

data class OpenAiLocalRegistry

@JsonIgnoreProperties(ignoreUnknown = true)
data class OpenAiLocalRegistry(
    val schemaVersion: Int = 1, val saltB64: String, val keys: MutableMap<String, KeyState> = mutableMapOf()
)

data class KeyState

@JsonIgnoreProperties(ignoreUnknown = true)
data class KeyState(
    val filesBySha256: MutableMap<String, UploadedFileRecord> = mutableMapOf(),
    val vectorStoresByBundle: MutableMap<String, VectorStoreRecord> = mutableMapOf()
)

data class UploadedFileRecord

@JsonIgnoreProperties(ignoreUnknown = true)
data class UploadedFileRecord(
    val fileId: String, val fileName: String, val purpose: String, val bytes: Int, val createdAtEpochSec: Long
)

data class VectorStoreRecord

@JsonIgnoreProperties(ignoreUnknown = true)
data class VectorStoreRecord(
    val vectorStoreId: String, val createdAtEpochSec: Long
)

OpenAI REST helpers (Files + Vector Stores)

class OpenAiKnowledgeStore

class OpenAiKnowledgeStore(
    private val cachePath: Path, private val apiBase: URI = URI.create("https://api.openai.com/v1")
) {
    private val http: HttpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build()

    private val mapper: ObjectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build())
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

    private val lock = ReentrantReadWriteLock()
    private var registry: OpenAiLocalRegistry = lock.write { loadOrInit() }

Hashing

fun sha256Hex(bytes: ByteArray): String = sha256(bytes).toHex()

fun sha256Hex(text: String): String = sha256Hex(text.toByteArray(StandardCharsets.UTF_8))

fun hashApiKey(apiKey: String): String

    fun hashApiKey(apiKey: String): String {
        // Hash = SHA256(salt || apiKey)
        val salt = Base64.getDecoder().decode(registry.saltB64)
        val md = MessageDigest.getInstance("SHA-256")
        md.update(salt)
        md.update(apiKey.toByteArray(StandardCharsets.UTF_8))
        return md.digest().toHex()
    }

Public: file id cache

fun ensureFileUploaded

If content already uploaded for this key (by sha256), returns existing file_id. Otherwise uploads to OpenAI Files API and records mapping.

    fun ensureFileUploaded(
        apiKey: String,
        fileName: String,
        content: ByteArray,
        purpose: String = "user_data",
        ifAlreadyUploaded: AlreadyUploaded = AlreadyUploaded.ReturnExisting
    ): UploadedFileRecord {
        val keyHash = hashApiKey(apiKey)
        val contentHash = sha256Hex(content)

        lock.read {
            val existing = registry.keys[keyHash]?.filesBySha256?.get(contentHash)
            if (existing != null) {
                if (ifAlreadyUploaded== AlreadyUploaded.ReturnExisting) return existing
            }
        }

        val created = uploadFile(apiKey, fileName, content, purpose)
        val record = UploadedFileRecord(
            fileId = created.id,
            fileName = created.filename ?: fileName,
            purpose = created.purpose ?: purpose,
            bytes = created.bytes ?: content.size,
            createdAtEpochSec = created.createdAt ?: Instant.now().epochSecond
        )

        lock.write {
            val ks = registry.keys.getOrPut(keyHash) { KeyState() }
            ks.filesBySha256[contentHash] = record
            saveLocked()
        }
        return record
    }

fun lookupFileIdForContent(apiKey: String, content: ByteArray): String?

Look up a cached file_id for this (key, content) without uploading.

    fun lookupFileIdForContent(apiKey: String, content: ByteArray): String? {
        val keyHash = hashApiKey(apiKey)
        val contentHash = sha256Hex(content)
        return lock.read { registry.keys[keyHash]?.filesBySha256?.get(contentHash)?.fileId }
    }

enum class AlreadyUploaded { ReturnExisting, Reupload }

Public: vector store cache

fun ensureCanonicalBundleReady

Ensures a vector store exists for (key, bundleName@bundleVersion) and that it contains all docs in the bundle (uploaded as files).

Returns vector_store_id you should pass to Responses via file_search.

body — 47 lines, from line 552
    fun ensureCanonicalBundleReady(
        apiKey: String, bundle: CanonicalBundle, purposeForDocs: String = "user_data"
    ): String {
        val keyHash = hashApiKey(apiKey)
        val bundleKey = "${bundle.name}@${bundle.version}"

        val existingVsId = lock.read {
            registry.keys[keyHash]?.vectorStoresByBundle?.get(bundleKey)?.vectorStoreId
        }

        val vsId = existingVsId ?: run {
            val created = createVectorStore(
                apiKey = apiKey, name = "afield:${bundle.name}", metadata = mapOf(
                    "bundle" to bundle.name, "version" to bundle.version, "kind" to "canonical"
                )
            )
            val rec = VectorStoreRecord(created.vectorStoreId, Instant.now().epochSecond)
            lock.write {
                val ks = registry.keys.getOrPut(keyHash) { KeyState() }
                ks.vectorStoresByBundle[bundleKey] = rec
                saveLocked()
            }
            created.vectorStoreId
        }

        // 1) Ensure docs are uploaded (deduped by content hash per key)
        val wantedFileIds = bundle.docs.map { doc ->
            ensureFileUploaded(
                apiKey = apiKey,
                fileName = doc.fileName,
                content = doc.content.toByteArray(StandardCharsets.UTF_8),
                purpose = purposeForDocs,
                ifAlreadyUploaded = AlreadyUploaded.ReturnExisting
            ).fileId
        }.toSet()

        // 2) Ensure vector store contains them
        val existingFileIds = listVectorStoreFileIds(apiKey, vsId).toSet()
        val missing = wantedFileIds - existingFileIds
        if (missing.isNotEmpty()) {
            val batch = createVectorStoreFileBatch(apiKey, vsId, missing.toList())
            // Poll a bit so it becomes queryable quickly; if you’d rather not block, delete this.
            waitForFileBatch(apiKey, vsId, batch.batchId, maxWaitMs = 60_000)
        }

        return vsId
    }

Low-level REST calls

    @JsonIgnoreProperties(ignoreUnknown = true)
    private data class FileCreateResponse(
        val id: String,
        val filename: String? = null,
        val purpose: String? = null,
        val bytes: Int? = null,
        @JsonProperty("created_at") val createdAt: Long? = null
    )

    @JsonIgnoreProperties(ignoreUnknown = true)
    private data class VectorStoreCreateResponse(val id: String)

    @JsonIgnoreProperties(ignoreUnknown = true)
    private data class VectorStoreFilesListResponse(
        val data: List<VectorStoreFileItem> = emptyList()
    )

data class VectorStoreFileItem

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class VectorStoreFileItem(
        @JsonProperty("file_id") val fileId: String? = null
    )

data class VectorStoreFileBatchCreateResponse(val id: String)

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class VectorStoreFileBatchCreateResponse(val id: String)

data class VectorStoreFileBatchGetResponse

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class VectorStoreFileBatchGetResponse(
        val id: String, val status: String? = null, @JsonProperty("last_error") val lastError: LastError? = null
    )

data class LastError(val message: String? = null)

body — 98 lines, from line 634
    @JsonIgnoreProperties(ignoreUnknown = true)
    data class LastError(val message: String? = null)

    private data class CreatedVectorStore(val vectorStoreId: String)
    private data class CreatedBatch(val batchId: String)

    private fun endpoint(path: String): URI =
        URI.create(apiBase.toString().trimEnd('/') + "/" + path.trimStart('/'))

    private fun uploadFile(apiKey: String, fileName: String, content: ByteArray, purpose: String): FileCreateResponse {
        val boundary = "----afield-${UUID.randomUUID()}"
        val body = buildMultipart(
            boundary = boundary, fields = listOf("purpose" to purpose), files = listOf(
                MultipartFile(
                    fieldName = "file", fileName = fileName, contentType = "application/octet-stream", content = content
                )
            )
        )

        val req = HttpRequest.newBuilder(endpoint("files")).header("Authorization", "Bearer $apiKey")
            .header("Content-Type", "multipart/form-data; boundary=$boundary")
            .POST(HttpRequest.BodyPublishers.ofByteArray(body)).build()

        val resp = http.send(req, HttpResponse.BodyHandlers.ofByteArray())
        if (resp.statusCode() !in 200..299) {
            throw RuntimeException("Files.create failed: HTTP ${resp.statusCode()} ${resp.body().decodeToString()}")
        }
        return mapper.readValue(resp.body())
    }

    private fun createVectorStore(apiKey: String, name: String, metadata: Map<String, String>): CreatedVectorStore {
        val payload = mapOf(
            "name" to name, "metadata" to metadata
        )
        val req = HttpRequest.newBuilder(endpoint("vector_stores")).header("Authorization", "Bearer $apiKey")
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(payload))).build()

        val resp = http.send(req, HttpResponse.BodyHandlers.ofString())
        if (resp.statusCode() !in 200..299) {
            throw RuntimeException("VectorStores.create failed: HTTP ${resp.statusCode()} ${resp.body()}")
        }
        val created = mapper.readValue<VectorStoreCreateResponse>(resp.body())
        return CreatedVectorStore(created.id)
    }

    private fun listVectorStoreFileIds(apiKey: String, vectorStoreId: String, limit: Int = 100): List<String> {
        val uri = endpoint("vector_stores/$vectorStoreId/files?limit=$limit")
        val req = HttpRequest.newBuilder(uri).header("Authorization", "Bearer $apiKey").GET().build()

        val resp = http.send(req, HttpResponse.BodyHandlers.ofString())
        if (resp.statusCode() !in 200..299) {
            throw RuntimeException("VectorStores.files.list failed: HTTP ${resp.statusCode()} ${resp.body()}")
        }
        val parsed = mapper.readValue<VectorStoreFilesListResponse>(resp.body())
        return parsed.data.mapNotNull { it.fileId }
    }

    private fun createVectorStoreFileBatch(apiKey: String, vectorStoreId: String, fileIds: List<String>): CreatedBatch {
        val payload = mapOf("file_ids" to fileIds)
        val req = HttpRequest.newBuilder(endpoint("vector_stores/$vectorStoreId/file_batches"))
            .header("Authorization", "Bearer $apiKey").header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(payload))).build()

        val resp = http.send(req, HttpResponse.BodyHandlers.ofString())
        if (resp.statusCode() !in 200..299) {
            throw RuntimeException("VectorStores.fileBatches.create failed: HTTP ${resp.statusCode()} ${resp.body()}")
        }
        val created = mapper.readValue<VectorStoreFileBatchCreateResponse>(resp.body())
        return CreatedBatch(created.id)
    }

    private fun waitForFileBatch(
        apiKey: String, vectorStoreId: String, batchId: String, maxWaitMs: Long
    ) {
        val deadline = System.currentTimeMillis() + maxWaitMs
        var sleepMs = 250L

        while (System.currentTimeMillis() < deadline) {
            val uri = endpoint("vector_stores/$vectorStoreId/file_batches/$batchId")
            val req = HttpRequest.newBuilder(uri).header("Authorization", "Bearer $apiKey").GET().build()

            val resp = http.send(req, HttpResponse.BodyHandlers.ofString())
            if (resp.statusCode() !in 200..299) {
                throw RuntimeException("VectorStores.fileBatches.get failed: HTTP ${resp.statusCode()} ${resp.body()}")
            }
            val got = mapper.readValue<VectorStoreFileBatchGetResponse>(resp.body())
            when (got.status?.lowercase()) {
                "completed" -> return
                "failed" -> throw RuntimeException("Vector store file batch failed: ${got.lastError?.message ?: "unknown"}")
            }

            Thread.sleep(sleepMs)
            sleepMs = (sleepMs * 1.6).toLong().coerceAtMost(2_000L)
        }

        // Not fatal; indexing may still complete later.
    }

Multipart builder

    private data class MultipartFile(
        val fieldName: String, val fileName: String, val contentType: String, val content: ByteArray
    )

    private fun buildMultipart(
        boundary: String, fields: List<Pair<String, String>>, files: List<MultipartFile>
    ): ByteArray {
        val crlf = "\r\n"
        val out = ArrayList<ByteArray>(fields.size + files.size + 8)

        fun add(s: String) = out.add(s.toByteArray(StandardCharsets.UTF_8))
        fun addBytes(b: ByteArray) = out.add(b)

        for ((k, v) in fields) {
            add("--$boundary$crlf")
            add("Content-Disposition: form-data; name=\"$k\"$crlf$crlf")
            add(v)
            add(crlf)
        }

        for (f in files) {
            add("--$boundary$crlf")
            add("Content-Disposition: form-data; name=\"${f.fieldName}\"; filename=\"${f.fileName}\"$crlf")
            add("Content-Type: ${f.contentType}$crlf$crlf")
            addBytes(f.content)
            add(crlf)
        }

        add("--$boundary--$crlf")

        val total = out.sumOf { it.size }
        val joined = ByteArray(total)
        var p = 0
        for (chunk in out) {
            System.arraycopy(chunk, 0, joined, p, chunk.size)
            p += chunk.size
        }
        return joined
    }

Disk I/O

    private fun loadOrInit(): OpenAiLocalRegistry {
        Files.createDirectories(cachePath.parent)
        if (!Files.exists(cachePath)) {
            val salt = ByteArray(32).also { SecureRandom().nextBytes(it) }
            val fresh = OpenAiLocalRegistry(
                schemaVersion = 1, saltB64 = Base64.getEncoder().encodeToString(salt), keys = mutableMapOf()
            )
            writeAtomic(cachePath, mapper.writeValueAsBytes(fresh))
            return fresh
        }

        return try {
            mapper.readValue(Files.readAllBytes(cachePath))
        } catch (e: Exception) {
            // Corrupt cache: keep a backup and start fresh
            val backup = cachePath.resolveSibling(cachePath.fileName.toString() + ".corrupt-" + UUID.randomUUID())
            Files.move(cachePath, backup, StandardCopyOption.REPLACE_EXISTING)
            val salt = ByteArray(32).also { SecureRandom().nextBytes(it) }
            val fresh = OpenAiLocalRegistry(1, Base64.getEncoder().encodeToString(salt), mutableMapOf())
            writeAtomic(cachePath, mapper.writeValueAsBytes(fresh))
            fresh
        }
    }

    private fun saveLocked() {
        writeAtomic(cachePath, mapper.writeValueAsBytes(registry))
    }

    private fun writeAtomic(path: Path, bytes: ByteArray) {
        lprintln("debug,filestore", "writing atomically to $path bytes=${bytes.size}")
        Files.createDirectories(path.parent)
        val tmp = path.resolveSibling(path.fileName.toString() + ".tmp-" + UUID.randomUUID())
        Files.write(tmp, bytes)
        Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE)
    }

    private fun sha256(bytes: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(bytes)

    private fun ByteArray.toHex(): String = joinToString("") { b -> "%02x".format(b) }
}