Skip to content

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 →