citomni / vectorembedding
Lean vector embedding provider package for CitOmni: Unified embedding request/response format, provider adapters, and reuse of the existing Curl infrastructure service.
Requires
- php: ^8.2
- ext-json: *
- citomni/infrastructure: ^1.0
- citomni/kernel: ^1.0
Requires (Dev)
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^11.0
Suggests
- ext-opcache: Better performance in production.
- citomni/cli: Optional. Install when using VectorEmbedding from CLI applications or package-provided commands.
- citomni/http: Optional. Install when using VectorEmbedding from HTTP applications.
README
citomni/vectorembedding is a reusable CitOmni provider package for semantic embedding integrations. It offers one compact, stable internal contract for embedding-style requests and responses, so application code can work against a consistent structure rather than provider-specific JSON payloads. The package is intentionally narrow in scope: It is designed to be deterministic, explicit, and operationally useful, not a speculative "AI toolbox" bucket.
At its core, VectorEmbedding separates application intent from provider transport. The application submits a normalized request, VectorEmbedding resolves a profile, delegates provider translation to an adapter, performs the outbound HTTP call through the existing CitOmni cURL service, parses the provider response back into a common format, and returns one stable internal embedding response. This design keeps the public surface area small while preserving room for provider-specific capabilities through explicit escape hatches.
Highlights
- Unified embedding contract for CitOmni applications with one compact internal request/response format across providers.
- Profile-driven provider selection so applications target stable profile ids rather than provider-specific payload logic.
- Adapter-based translation layer that keeps provider-specific JSON, headers, URLs, and response parsing out of application code.
- Explicit support for package-level embedding options such as dimensions and task type, translated by adapters into provider-native semantics.
- Operationally explicit logging with useful execution context and no parallel transport stack.
- Reuse of the existing CitOmni cURL service instead of introducing a second HTTP abstraction.
- CLI integration out of the box through
vectorembedding:embedfor diagnostics, development, and scripting.
Why VectorEmbedding
Embedding APIs differ in endpoint structure, request shape, option semantics, authentication headers, output dimensionality handling, and response envelopes. Those differences are operationally real, but they should not leak into every controller, command, or application service. VectorEmbedding exists to absorb that variability behind a single internal request/response model and a profile-based configuration layer. Profiles select concrete adapters and models; adapters perform provider-specific translation; the application stays focused on intent.
In practical terms, this yields four advantages:
- A single internal request format across providers.
- A single normalized response format for downstream application code.
- Explicit profile-driven configuration rather than implicit provider branching.
- Provider-specific flexibility without collapsing into an unstructured catch-all abstraction.
Design principles
VectorEmbedding follows a deliberately conservative architecture:
- Profiles, not providers, are selected by the application. A profile defines the adapter, model, endpoint base, API credentials, and timeout policy.
- Adapters own translation. They build provider URLs, payloads, headers, and response normalization.
- The existing CitOmni cURL service remains the transport. VectorEmbedding does not introduce its own generic transport layer.
- Validation is structural and explicit. The package validates request shape, known package-level option keys, and required fields. Provider-specific compatibility stays in adapters.
- The response contract is stable. Applications read one normalized
vectors[]structure regardless of provider. - Logging is explicit. Useful operational context is logged without inventing a second observability layer.
Requirements
- PHP 8.2+
ext-jsoncitomni/kernelcitomni/infrastructure- A CitOmni application with the existing cURL service available
OPcache is strongly recommended in production.
Installation
composer require citomni/vectorembedding composer dump-autoload -o
Register the package provider in config/providers.php:
<?php declare(strict_types=1); return [ \CitOmni\VectorEmbedding\Boot\Registry::class, ];
Once registered, the package exposes the vectorEmbedder service and the vectorembedding:embed CLI command.
Public service
The primary entry point is the VectorEmbedding service:
$this->app->vectorEmbedder->embed(array $request): array
The service is responsible for:
- Validating raw input shape.
- Normalizing the internal request.
- Resolving the selected profile.
- Validating structural request shape.
- Instantiating the configured adapter.
- Building the provider URL, payload, and headers.
- Sending the HTTP request via
$this->app->curl->execute(...). - Parsing and normalizing the provider response.
- Returning the common response format.
Internal request format
VectorEmbedding uses a compact internal request model centered on profile, items, options, provider_options, and debug.
[ 'profile' => 'openai-text-embedding-3-small', 'items' => [ [ 'type' => 'text', 'text' => 'Lejelovens regler om depositum', ], ], 'options' => [ 'dimensions' => null, 'task_type' => null, ], 'provider_options' => [], 'debug' => [ 'include_raw_response' => false, 'include_built_request' => false, ], ]
Items
The request is intentionally item-based rather than text-shortcut-based. This keeps the internal contract structurally consistent and leaves room for broader modality support later.
Text is the canonical baseline in V1:
[ 'type' => 'text', 'text' => 'Lejelovens regler om depositum', ]
V1 is honest about what it supports: The package is multimodal-ready in contract shape, but current adapters and CLI usage are text-first.
Options and provider-specific escape hatch
VectorEmbedding uses an explicit package-level whitelist for options. The currently recognized package-level keys are:
dimensionstask_type
Unknown option keys fail fast. This is deliberate and avoids silently swallowing typos such as dimensons.
Provider-specific deviations belong in provider_options, which acts as the sanctioned escape hatch for capabilities that do not fit the cross-provider abstraction cleanly. In V1, adapters may choose to reject unsupported provider_options entirely.
Debug flags
The request may also include a small debug subtree:
include_raw_responseinclude_built_request
These control whether raw provider response data and the built provider payload are attached to the normalized response.
Normalized response format
Adapters normalize provider responses into a common structure:
[ 'profile' => 'openai-text-embedding-3-small', 'provider' => 'openai', 'model' => 'text-embedding-3-small', 'vectors' => [ [ 'index' => 0, 'vector' => [0.123, -0.456, 0.789], 'meta' => [ 'input_type' => 'text', ], ], ], 'usage' => [ 'input_tokens' => 10, 'total_tokens' => 10, ], 'raw' => null, 'meta' => [ 'cached' => false, 'cache_key' => null, 'duration_ms' => 123, ], ]
This response model gives application code one stable place to read:
- the resolved profile,
- the provider name,
- the model name,
- the returned vectors,
- usage information where the provider supplies it,
- optional raw provider data,
- and operational metadata such as duration.
Response guarantees
profileis always present.provideris always present.modelis always present.vectorsis always an array.usageis always an array.rawis always present, defaulting tonull.metais always an array.
Configuration
VectorEmbedding is configured under the vectorembedding node. The important concept is the profile: the application selects a profile, and the profile resolves to a concrete adapter and endpoint configuration.
<?php declare(strict_types=1); return [ 'vectorembedding' => [ 'default_profile' => 'openai-text-embedding-3-small', 'debug' => [ 'include_raw_response' => false, 'include_built_request' => false, ], 'cache' => [ 'enabled' => false, 'ttl' => 3600, ], 'profiles' => [ 'openai-text-embedding-3-small' => [ 'adapter' => \CitOmni\VectorEmbedding\Adapter\OpenAiEmbeddingAdapter::class, 'provider' => 'openai', 'model' => 'text-embedding-3-small', 'base_url' => 'https://api.openai.com/v1', 'api_key' => '', 'timeout' => 60, 'connect_timeout' => 10, ], 'gemini-embedding-001' => [ 'adapter' => \CitOmni\VectorEmbedding\Adapter\GeminiEmbeddingAdapter::class, 'provider' => 'google', 'model' => 'gemini-embedding-001', 'base_url' => 'https://generativelanguage.googleapis.com/v1beta', 'api_key' => '', 'timeout' => 60, 'connect_timeout' => 10, ], ], ], ];
Configuration semantics
default_profileis used when the request does not provide one.- Each profile points to an adapter class.
- Multiple profiles may reuse the same adapter.
- Each profile carries the concrete model, endpoint base, credentials, and timeout.
- General transport defaults belong to the existing cURL configuration, not to a duplicated VectorEmbedding transport subtree.
Notes on credentials
The package baseline may define empty api_key values, but production or application-level configuration must provide real credentials before the profile can be used successfully.
Provider adapters
Adapters are the translation boundary between VectorEmbedding's internal format and each provider's native API contract.
An adapter is responsible for:
- building the outbound provider URL,
- building the outbound provider payload,
- building provider-specific headers,
- decoding the provider response body,
- normalizing the result into VectorEmbedding's common response structure,
- and raising provider-appropriate exceptions on malformed or failed responses.
The adapter contract is intentionally small:
interface EmbeddingAdapterInterface { public function buildUrl(): string; public function buildRequest(array $request): array; public function buildHeaders(array $request): array; public function parseResponse(array $transportResult, array $request): array; }
This keeps provider logic sharply localized and prevents transport, logging, or profile selection concerns from bleeding into adapter implementations.
Transport model
VectorEmbedding delegates HTTP execution to the existing CitOmni cURL service:
$this->app->curl->execute(array $request): array
That service remains the transport authority. It validates transport request shape, performs the HTTP call, applies timeout and SSL behavior, and returns the raw transport result. VectorEmbedding does not replace or duplicate that layer. Instead, it builds the cURL request array, delegates execution, and lets the adapter decode and interpret the response body.
This separation is consequential:
- VectorEmbedding does not centralize provider JSON parsing.
- Adapters decode provider JSON where appropriate.
- Transport exceptions remain transport exceptions in origin, even when package-level context is added around request execution.
- The package stays operationally explicit without needing a parallel transport abstraction.
Logging
VectorEmbedding logs through the existing CitOmni log service when that service is available.
Useful VectorEmbedding log context typically includes:
- profile,
- provider,
- model,
- item count,
- vector count,
- duration,
- usage information where available,
- built request payload when explicitly enabled,
- and failure details where relevant.
Suggested log categories are intentionally few and high-signal:
embed.okembed.fail
Logging policy
The package does not attempt to build a second observability stack. It simply logs package-level execution context in a compact, explicit way. Because the underlying cURL service may also log, production installations should use a deliberate logging policy rather than accidentally generating duplicate transport noise.
Caching
The current V1 implementation does not implement response caching.
A future-friendly cache config node may exist in the package baseline, but caching is not yet active in the shared service flow. At present:
meta.cachedis alwaysfalsemeta.cache_keyis alwaysnull
This is intentional. The package keeps the service pipeline small and real first, rather than introducing persistence before there is a concrete need.
CLI usage
VectorEmbedding exposes a command-line entry point:
php bin/citomni vectorembedding:embed "Lejelovens regler om depositum"
A profile may be selected explicitly:
php bin/citomni vectorembedding:embed "Lejelovens regler om depositum" --profile="openai-text-embedding-3-small"
Request options may be supplied from the CLI:
php bin/citomni vectorembedding:embed "Test" --dimensions=256 php bin/citomni vectorembedding:embed "Depositum ved leje" --task-type="RETRIEVAL_QUERY"
The full normalized response may be printed as JSON:
php bin/citomni vectorembedding:embed "Lejelovens regler om depositum" --json
In plain mode, the command prints the first vector and then a compact info line containing the resolved profile, provider, model, cache status, vector count, dimensions, duration, and token usage where available.
Operational notes
Profiles are the stable application contract
Application code should target profile ids rather than provider-specific endpoints or model payload formats directly. This keeps provider translation localized to adapters and configuration.
The response contract is vector-first from day one
The package returns vectors[], even when a request often yields only one vector. This avoids a future breaking change from vector to vectors.
Transport remains delegated
VectorEmbedding does not replace the CitOmni cURL service. Transport validation, HTTP execution, timeout handling, and low-level request mechanics remain delegated to the existing infrastructure layer.
Validation is intentionally split
- The service validates what can be known generically: request shape, item structure, option keys, profile resolution.
- The adapter validates what depends on provider knowledge: unsupported modalities, provider-specific option semantics, and response shape.
Logging should be explicit, not noisy
VectorEmbedding logs package-level success and failure context. Since the underlying cURL service may also log, production installations should choose a deliberate logging policy rather than accidentally doubling transport detail.
Package structure
The package is organized around a small set of clear responsibilities:
src/
Adapter/
Boot/
Command/
Exception/
Interface/
Service/
A representative structure includes:
Boot/Registry.phpfor service/config/CLI registration,Service/VectorEmbedder.phpas the primary orchestration service,Interface/EmbeddingAdapterInterface.phpfor adapter contracts,Adapter/...for concrete provider adapters,Command/EmbedCommand.phpfor CLI usage,Exception/...for package-specific exceptions.
Performance notes
- Services are resolved through explicit service maps rather than scanning.
- Provider translation is localized to small adapters with a narrow contract.
- The package reuses existing infrastructure services rather than layering a second transport stack on top.
- Production should use optimized Composer autoloading.
- OPcache should be enabled in production.
Composer example:
{
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true,
"apcu-autoloader": true
}
}
Then run:
composer dump-autoload -o
Error model
VectorEmbedding defines its own domain exception family for request, configuration, adapter, provider-request, and provider-response failures.
The main exception types are:
VectorEmbeddingExceptionVectorEmbeddingConfigExceptionAdapterNotFoundExceptionInvalidRequestExceptionProviderRequestExceptionProviderResponseException
This keeps the fault surface small, explicit, and package-local.
Error handling philosophy
Fail fast.
VectorEmbedding does not hide malformed requests, profile misconfiguration, unsupported item types, malformed provider responses, or transport preparation failures behind vague fallback behavior. Invalid structure should fail as invalid structure; config problems should fail as config problems; provider response problems should fail as provider response problems.
That bias is deliberate. In integration code, silent fallback logic often looks convenient right up until it becomes the reason nobody can tell what actually happened.
Example
<?php declare(strict_types=1); $response = $this->app->vectorEmbedder->embed([ 'profile' => 'openai-text-embedding-3-small', 'items' => [ [ 'type' => 'text', 'text' => 'Lejelovens regler om depositum', ], ], 'options' => [ 'dimensions' => 256, ], ]); $vector = $response['vectors'][0]['vector'] ?? [];
Position within CitOmni
VectorEmbedding follows the broader CitOmni philosophy: explicit contracts, deterministic behavior, small public surfaces, and low overhead. It does one thing narrowly but well: it gives CitOmni applications a disciplined and reusable way to generate semantic embeddings without forcing application code to speak every provider's dialect.
Contributing
- PHP 8.2+
- PSR-4
- Tabs for indentation
- K&R brace style
- Keep ownership boundaries sharp
- Keep transport in services/adapters where it belongs
- Do not introduce magic or hidden fallback behavior without an explicit and documented reason
Coding & Documentation Conventions
All CitOmni projects follow the shared conventions documented here: CitOmni Coding & Documentation Conventions
License
CitOmni VectorEmbedding is open-source under the MIT License. See LICENSE.
Trademark notice: "CitOmni" and the CitOmni logo are trademarks of Lars Grove Mortensen. Usage of the name or logo must follow the policy in NOTICE. Do not imply endorsement or affiliation without prior written permission.
Trademarks
"CitOmni" and the CitOmni logo are trademarks of Lars Grove Mortensen. You may make factual references to "CitOmni", but do not modify the marks, create confusingly similar logos, or imply sponsorship, endorsement, or affiliation without prior written permission. Do not register or use "citomni" (or confusingly similar terms) in company names, domains, social handles, or top-level vendor/package names. For details, see NOTICE.
Author
Developed by Lars Grove Mortensen © 2012-present.
CitOmni - low overhead, high performance, ready for anything.