displace / ai-contracts
Stable, dependency-free PHP interfaces for local-first AI primitives: embeddings, generation, vector search, reranking, transcription.
Requires
- php: ^8.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-29 18:40:39 UTC
README
Stable PHP interfaces for local-first AI primitives.
Embeddings, generation, vector search, reranking, transcription — zero dependencies, zero implementations.
Status
Feature-complete for its scope. Maintained: dependency updates and compatibility fixes ship as patch/minor releases — see the CHANGELOG. Last verified: PHP 8.3/8.4/8.5 on Linux x86_64 and macOS arm64, no runtime dependencies, 2026-10-29.
Remaining ideas are tracked as future issues.
Co-maintainers welcome — if you run this in production and want a
say in where it goes, open an issue or a PR.
What is ai-contracts?
Five interfaces under Displace\AI\Contracts that describe what AI
primitives do, without saying how:
| Interface | Contract |
|---|---|
Embedder |
text in → packed float32 vector out |
VectorIndex |
id-addressed similarity search with allowlist filtering |
Generator |
prompt in → completion out |
Reranker |
query + candidates → best-first relevance scores |
Transcriber |
audio file in → transcript + timestamped segments |
The package contains no implementations and no dependencies — it is the integration surface between things that provide AI primitives (ext-infer, ext-turbovec, hosted APIs, anything else) and things that consume them (LLPhant, NeuronAI, Prism, your application). Frameworks ship the drivers; applications code against the interfaces; providers can be swapped without touching either.
composer require displace/ai-contracts
The packed-vector contract
Every vector crossing these interfaces is a packed little-endian
float32 binary string — the output of pack('g*', ...$floats) — never
a PHP float array. Arrays inflate every coordinate into a zval; packed
strings are a single contiguous buffer that moves through FFI and
extension boundaries with zero per-element overhead, and batches compose
by plain string concatenation:
use Displace\AI\Contracts\Embedder; use Displace\AI\Contracts\VectorIndex; function indexPosts(Embedder $embedder, VectorIndex $index, array $posts): void { // One packed buffer for the whole batch, one add() call. $index->add( $embedder->embedBatch(array_column($posts, 'content')), array_column($posts, 'id'), ); } function searchPosts(Embedder $embedder, VectorIndex $index, string $query, array $visibleIds): array { // The allowlist composes with a SQL pre-filter: // SELECT id FROM posts WHERE status = 'publish' → $visibleIds return $index->search($embedder->embed($query), k: 10, allowlist: $visibleIds); }
Nothing above names a concrete engine. Wire in a llama.cpp-backed embedder and an in-process quantized index today, swap either side tomorrow.
Writing an adapter
Implementations are intentionally easy to write — here is a complete
Embedder over ext-infer:
use Displace\AI\Contracts\Embedder; use Displace\Infer\Model; final class InferEmbedder implements Embedder { public function __construct(private readonly Model $model) {} public function embed(string $text): string { // ext-infer ≥ 0.2 emits the packed contract natively; on 0.1, // bridge with pack('g*', ...$vector) instead. return $this->model->embed($text)->normalize()->packed(); } public function embedBatch(array $texts): string { return implode('', array_map($this->embed(...), $texts)); } public function dimensions(): int { return $this->model->embed('')->dimensions(); } }
The test suite ships in-memory reference fakes
(tests/Fake/) that double as executable documentation
of each contract's semantics — the InMemoryVectorIndex is a
brute-force oracle you can test your own adapter against.
Versioning
Interfaces are forever-contracts: methods are never removed or re-signatured within a major version, and new methods only arrive with a major bump (an interface addition is a BC break for every implementor). Pre-1.0, minor versions may still adjust the surface — pin accordingly.
Deliberately out of scope
Implementations (this package never gains a class) · an
orchestration framework — chains, agents, pipelines, prompt templates
belong to the frameworks integrating these contracts · chat-message
abstractions — every framework already has one; Generator is the
lowest common denominator they adapt down to · streaming
interfaces — premature until the underlying local engines ship
streaming · training / fine-tuning.
Part of a stack
Transcribe → chunk → embed → search → answer, entirely on your hardware:
| Stage | Package | What it does |
|---|---|---|
| Transcribe | ext-whisper | Speech-to-text over whisper.cpp |
| Chunk | ai-toolkit | Text chunkers and packed-vector math, pure PHP |
| Embed · rerank · answer | ext-infer | Local LLM chat, embeddings and reranking over llama.cpp |
| Search | ext-turbovec | Compressed in-memory vector search (TurboQuant) |
| Integrate | ai-contracts (this repo) | The interfaces frameworks code against |
| Reference app | rag-over-markdown | The whole pipeline over a folder of markdown |
Every package links the others; the extensions share one scaffold, ext-template.
Writing about this
- ai-contracts & ai-toolkit: The Primitives a PHP AI Ecosystem Needs
- RAG Over Markdown: An End-to-End Local Pipeline in Pure PHP
The stack's launch series ran Sept 22 – Oct 1, 2026; everything else is at eric.mann.blog/writing.
License
MIT © 2026 Eric Mann / Displace Technologies