Embedding Chunk Planner — Swift source
Plan document chunking for RAG — chunk counts with overlap math, vector counts, and embedding costs per model.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Embedding Chunk Planner — pure chunking math for RAG pipelines.
//
// Language: Swift (Swift 5.9, standard library only)
// Source: CosmoDev polyglot showcase port of the Embedding Chunk Planner
// tool, ported from src/lib/embeddingPlanner.ts (the canonical
// TypeScript implementation).
// Tool page: https://dev.cosmolabs.org/tools/embedding-chunk-planner
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Design goals:
// - Pure + deterministic; never traps (nil instead of an error).
// - Functionally equivalent to the TS reference: same inputs -> same outputs.
// - Self-contained: the Swift standard library only (no SwiftPM packages;
// no need for Foundation). The model price table is inlined below, mirrored
// from src/lib/ai/embeddings.ts — prices NEVER live in the planner itself.
//
// Behavior (mirrors the TS source exactly):
// - chunkSize <= 0 or totalTokens <= 0 -> ChunkPlan.zero (nothing to embed).
// - Negative overlap is treated as 0; overlap then clamps to at most
// chunkSize / 2 so consecutive chunks always advance.
// - chunks = max(1, ceil((totalTokens - overlap) / (chunkSize - overlap)))
// — a tiny document still yields one chunk.
/// One embedding model's offered dimensions (ascending, Matryoshka shortening
/// included) and pricing: USD per 1M input tokens.
public struct EmbeddingModel: Equatable, Sendable {
public let id: String
public let vendor: String
public let dims: [Int]
public let inputPerM: Double
public init(id: String, vendor: String, dims: [Int], inputPerM: Double) {
self.id = id
self.vendor = vendor
self.dims = dims
self.inputPerM = inputPerM
}
}
/// How a document splits into overlapping chunks.
public struct ChunkPlan: Equatable, Sendable {
public let chunks: Int
public let totalTokensWithOverlap: Int
public let overheadTokens: Int
public init(chunks: Int, totalTokensWithOverlap: Int, overheadTokens: Int) {
self.chunks = chunks
self.totalTokensWithOverlap = totalTokensWithOverlap
self.overheadTokens = overheadTokens
}
/// The "nothing to embed" plan the TS source returns for zero/negative
/// input or a non-positive chunk size.
public static let zero = ChunkPlan(chunks: 0, totalTokensWithOverlap: 0, overheadTokens: 0)
}
/// Chunking knobs, in tokens. Mirrors the TS `Partial<ChunkOptions>`: each
/// field is independently optional — nil falls back to the 512 / 64 default,
/// and setting one leaves the other at its default.
public struct ChunkOptions: Sendable {
public var chunkSize: Int?
public var overlap: Int?
public init(chunkSize: Int? = nil, overlap: Int? = nil) {
self.chunkSize = chunkSize
self.overlap = overlap
}
}
/// Chunk plan plus pricing for one embedding call. The three chunk fields are
/// flattened in (the TS `...plan` spread) so the struct reads like the TS
/// `EmbeddingPlan extends ChunkPlan`.
public struct EmbeddingPlan: Equatable, Sendable {
public let chunks: Int
public let totalTokensWithOverlap: Int
public let overheadTokens: Int
public let model: EmbeddingModel
/// One vector per chunk.
public let vectors: Int
/// USD: totalTokensWithOverlap / 1e6 * model.inputPerM.
public let cost: Double
public init(chunks: Int, totalTokensWithOverlap: Int, overheadTokens: Int,
model: EmbeddingModel, vectors: Int, cost: Double) {
self.chunks = chunks
self.totalTokensWithOverlap = totalTokensWithOverlap
self.overheadTokens = overheadTokens
self.model = model
self.vectors = vectors
self.cost = cost
}
}
public enum EmbeddingChunkPlanner {
/// Embedding model price table — the SSOT for pricing, mirrored from
/// src/lib/ai/embeddings.ts. Refresh both files together.
public static let embeddingModels: [EmbeddingModel] = [
EmbeddingModel(id: "text-embedding-3-small", vendor: "OpenAI", dims: [512, 1536], inputPerM: 0.02),
EmbeddingModel(id: "text-embedding-3-large", vendor: "OpenAI", dims: [256, 1024, 3072], inputPerM: 0.13),
EmbeddingModel(id: "embed-english-v3.0", vendor: "Cohere", dims: [512, 1024, 1536], inputPerM: 0.1),
EmbeddingModel(id: "voyage-3-lite", vendor: "Voyage AI", dims: [512, 1024], inputPerM: 0.02),
]
/// Default knobs: 512-token chunks, 64-token overlap (TS DEFAULT_CHUNK_OPTIONS).
public static let defaultChunkSize = 512
public static let defaultOverlap = 64
/// Look up an embedding model by id. Returns nil for unknown ids.
public static func getEmbeddingModel(_ id: String) -> EmbeddingModel? {
embeddingModels.first { $0.id == id }
}
/// Plan how `totalTokens` split into overlapping chunks. `opts` may be nil
/// (both defaults), mirroring the TS optional parameter; each nil field
/// falls back to the 512 / 64 default independently.
public static func planChunks(_ totalTokens: Int, opts: ChunkOptions? = nil) -> ChunkPlan {
let chunkSize = opts?.chunkSize ?? defaultChunkSize
let overlapRaw = opts?.overlap ?? defaultOverlap
if chunkSize <= 0 || totalTokens <= 0 {
return .zero
}
// min(max(overlapRaw, 0), chunkSize / 2) — the TS clamp. Overlap that
// large would never advance, so consecutive chunks always gain at least
// half a chunk. (chunkSize >= 1 here, so chunkSize - overlap is never zero.)
let overlap = min(max(overlapRaw, 0), chunkSize / 2)
// Float division + ceil mirrors TS's Math.ceil exactly (a tiny document
// lands the quotient just below zero; ceil brings it to 0 and max(1, ..)
// lifts it back to one chunk).
let quotient = Double(totalTokens - overlap) / Double(chunkSize - overlap)
let chunks = max(1, Int(quotient.rounded(.up)))
let totalTokensWithOverlap = totalTokens + (chunks - 1) * overlap
return ChunkPlan(chunks: chunks,
totalTokensWithOverlap: totalTokensWithOverlap,
overheadTokens: totalTokensWithOverlap - totalTokens)
}
/// Chunk a document AND price its embedding for `modelId` at `dims`
/// dimensions. Unknown model, or dims the model does not offer -> nil.
public static func planEmbedding(_ totalTokens: Int, _ modelId: String, dims: Int,
opts: ChunkOptions? = nil) -> EmbeddingPlan? {
guard let model = getEmbeddingModel(modelId), model.dims.contains(dims) else {
return nil
}
let plan = planChunks(totalTokens, opts: opts)
return EmbeddingPlan(chunks: plan.chunks,
totalTokensWithOverlap: plan.totalTokensWithOverlap,
overheadTokens: plan.overheadTokens,
model: model,
vectors: plan.chunks,
cost: Double(plan.totalTokensWithOverlap) / 1e6 * model.inputPerM)
}
}
// ---------- showcase examples (the canonical suite lives in src/lib) ----------
func runShowcaseExamples() {
// 1,000 tokens: ceil((1000-64)/(512-64)) = 3 chunks, 2 seams x 64.
let a = EmbeddingChunkPlanner.planChunks(1000)
precondition(a == ChunkPlan(chunks: 3, totalTokensWithOverlap: 1128, overheadTokens: 128))
// A document that fits one chunk has no seam overhead.
precondition(EmbeddingChunkPlanner.planChunks(512)
== ChunkPlan(chunks: 1, totalTokensWithOverlap: 512, overheadTokens: 0))
// Zero/negative input or non-positive chunk size -> the zero plan.
precondition(EmbeddingChunkPlanner.planChunks(0) == .zero)
precondition(EmbeddingChunkPlanner.planChunks(-100) == .zero)
precondition(EmbeddingChunkPlanner.planChunks(1000, opts: ChunkOptions(chunkSize: 0)) == .zero)
precondition(EmbeddingChunkPlanner.planChunks(1000, opts: ChunkOptions(chunkSize: -8)) == .zero)
// overlap 600 > floor(512/2) = 256 -> clamped to 256.
precondition(EmbeddingChunkPlanner.planChunks(1000, opts: ChunkOptions(overlap: 600))
== ChunkPlan(chunks: 3, totalTokensWithOverlap: 1512, overheadTokens: 512))
// Negative overlap clamps to 0: 1000 tokens -> ceil(1000/512) = 2 chunks.
precondition(EmbeddingChunkPlanner.planChunks(1000, opts: ChunkOptions(overlap: -5))
== ChunkPlan(chunks: 2, totalTokensWithOverlap: 1000, overheadTokens: 0))
// chunkSize without overlap: ceil((1000-64)/192) = 5 chunks.
precondition(EmbeddingChunkPlanner.planChunks(1000, opts: ChunkOptions(chunkSize: 256))
== ChunkPlan(chunks: 5, totalTokensWithOverlap: 1256, overheadTokens: 256))
// Shorter than the overlap still yields one chunk.
precondition(EmbeddingChunkPlanner.planChunks(50, opts: ChunkOptions(overlap: 64))
== ChunkPlan(chunks: 1, totalTokensWithOverlap: 50, overheadTokens: 0))
// chunkSize of 1 clamps overlap to 0: ceil(3/1) = 3 chunks.
precondition(EmbeddingChunkPlanner.planChunks(3, opts: ChunkOptions(chunkSize: 1))
== ChunkPlan(chunks: 3, totalTokensWithOverlap: 3, overheadTokens: 0))
// Pricing: 1,000 tokens on text-embedding-3-small @ 1536 dims.
let priced = EmbeddingChunkPlanner.planEmbedding(1000, "text-embedding-3-small", dims: 1536)
precondition(priced?.chunks == 3 && priced?.totalTokensWithOverlap == 1128 && priced?.vectors == 3)
precondition(abs((priced?.cost ?? -1) - 0.00002256) < 1e-12) // 1128 / 1e6 * $0.02
// A single-chunk document on voyage-3-lite @ 512 dims.
let single = EmbeddingChunkPlanner.planEmbedding(512, "voyage-3-lite", dims: 512)
precondition(single?.vectors == 1)
precondition(abs((single?.cost ?? -1) - 0.00001024) < 1e-12) // 512 / 1e6 * $0.02
// Unknown model or unoffered dims -> nil.
precondition(EmbeddingChunkPlanner.planEmbedding(1000, "text-embedding-3-small", dims: 999) == nil)
precondition(EmbeddingChunkPlanner.planEmbedding(1000, "ghost", dims: 1536) == nil)
// Zero tokens price out to a zero-cost plan.
let zero = EmbeddingChunkPlanner.planEmbedding(0, "text-embedding-3-small", dims: 1536)
precondition(zero?.chunks == 0 && zero?.vectors == 0 && zero?.cost == 0.0)
}
Also available in 12 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →