Embedding Chunk Planner — PHP source
Plan document chunking for RAG — chunk counts with overlap math, vector counts, and embedding costs per model.
This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* Embedding Chunk Planner — pure chunking math for RAG pipelines.
*
* Language: PHP (8.1+, 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 throws.
* - Functionally equivalent to the TS reference: same inputs -> same outputs.
* - Self-contained: stdlib only (no Composer packages). 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 -> {0, 0, 0} (nothing to embed).
* - Negative overlap is treated as 0; overlap then clamps to at most
* floor(chunkSize / 2) so consecutive chunks always advance.
* - chunks = max(1, ceil((totalTokens - overlap) / (chunkSize - overlap)))
* — a tiny document still yields one chunk.
*/
declare(strict_types=1);
/**
* Embedding model price table — the SSOT for pricing, mirrored from
* src/lib/ai/embeddings.ts. Refresh both files together.
*
* Each entry: id, vendor, dims (offered dimensions, ascending), inputPerM
* (USD per 1M input tokens).
*/
const EMBEDDING_MODELS = [
['id' => 'text-embedding-3-small', 'vendor' => 'OpenAI', 'dims' => [512, 1536], 'inputPerM' => 0.02],
['id' => 'text-embedding-3-large', 'vendor' => 'OpenAI', 'dims' => [256, 1024, 3072], 'inputPerM' => 0.13],
['id' => 'embed-english-v3.0', 'vendor' => 'Cohere', 'dims' => [512, 1024, 1536], 'inputPerM' => 0.1],
['id' => 'voyage-3-lite', 'vendor' => 'Voyage AI', 'dims' => [512, 1024], 'inputPerM' => 0.02],
];
/**
* Options for plan_chunks() / plan_embedding(). Each key is optional; defaults
* match the TS DEFAULT_CHUNK_OPTIONS (512-token chunks, 64-token overlap).
*
* - chunkSize: int default 512
* - overlap: int default 64
*/
function embedding_planner_default_options(): array
{
return [
'chunkSize' => 512,
'overlap' => 64,
];
}
/**
* Look up an embedding model by id.
*
* @return array{id: string, vendor: string, dims: int[], inputPerM: float}|null
*/
function get_embedding_model(string $id): ?array
{
foreach (EMBEDDING_MODELS as $model) {
if ($model['id'] === $id) {
return $model;
}
}
return null;
}
/**
* Plan how $totalTokens split into overlapping chunks.
*
* @return array{chunks: int, totalTokensWithOverlap: int, overheadTokens: int}
*/
function plan_chunks(int $totalTokens, array $options = []): array
{
$options = array_merge(embedding_planner_default_options(), $options);
$chunkSize = (int) $options['chunkSize'];
$overlapRaw = (int) $options['overlap'];
if ($chunkSize <= 0 || $totalTokens <= 0) {
return ['chunks' => 0, 'totalTokensWithOverlap' => 0, 'overheadTokens' => 0];
}
// min(max(overlap, 0), floor(chunkSize / 2)) — the TS clamp.
$overlap = min(max($overlapRaw, 0), intdiv($chunkSize, 2));
// Float division + (int)ceil mirrors TS's Math.ceil exactly; max(1, ...)
// lifts a tiny document back up to one chunk.
$chunks = max(1, (int) ceil(($totalTokens - $overlap) / ($chunkSize - $overlap)));
$totalTokensWithOverlap = $totalTokens + ($chunks - 1) * $overlap;
return [
'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 -> null. On success:
* the chunk plan plus model, vectors (one per chunk) and cost (USD:
* totalTokensWithOverlap / 1e6 * inputPerM).
*
* @return array{chunks: int, totalTokensWithOverlap: int, overheadTokens: int,
* model: array{id: string, vendor: string, dims: int[], inputPerM: float},
* vectors: int, cost: float}|null
*/
function plan_embedding(int $totalTokens, string $modelId, int $dims, array $options = []): ?array
{
$model = get_embedding_model($modelId);
if ($model === null || !in_array($dims, $model['dims'], true)) {
return null;
}
$plan = plan_chunks($totalTokens, $options);
$plan['model'] = $model;
$plan['vectors'] = $plan['chunks'];
$plan['cost'] = ($plan['totalTokensWithOverlap'] / 1e6) * $model['inputPerM'];
return $plan;
}
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 →