Embeddings
Embedding request bodies on a batch line. There is no live POST /v1/embeddings.
Not a live HTTP endpoint
Do not POST to /v1/embeddings. Put that path on each JSONL line as url, then submit the file
with POST /v1/batches and "endpoint": "/v1/embeddings".
OpenAI-compatible embeddings, as the body of a batch line. Embedding work is
almost always a backfill or an indexing job, so it was already the clearest fit for running as a job:
results can land on a schedule, and pricing sits below OpenRouter batch inference.
Requires the batches:write scope on the key that creates the batch.
{"custom_id":"doc_5501","method":"POST","url":"/v1/embeddings","body":{"model":"bge-large-en-v1.5","input":["Refunds are issued within 5 business days.","Shipping is free over $50."]},"metadata":{"entity":"customer:cus_2291"}}Submit the file:
POST /v1/batches HTTP/1.1
Host: REGION.api.lararouter.com
Authorization: Bearer $LARAROUTER_API_KEY
Content-Type: application/json
{
"input_file_id": "file_9Hs4TnQ2",
"endpoint": "/v1/embeddings",
"completion_window": "24h"
}Every line in a file must target the same endpoint, so chat and embedding work go in separate batches.
Parameters
Prop
Type
Response
Each result line pairs your custom_id with the embeddings response for that input array.
{
"custom_id": "doc_5501",
"response": {
"status_code": 200,
"request_id": "req_01JD8XK4M2",
"body": {
"object": "list",
"model": "bge-large-en-v1.5",
"data": [
{ "object": "embedding", "index": 0, "embedding": [0.0023, -0.0091, 0.0142] },
{ "object": "embedding", "index": 1, "embedding": [0.0117, 0.0038, -0.0064] }
],
"usage": { "prompt_tokens": 24, "total_tokens": 24, "cost_usd": 0.0000012 }
}
},
"error": null
}Match on index, not order
data is returned in request order today, but relying on that is fragile. Every item carries its
index. Use it to line vectors up with the inputs on that line, and custom_id to line the line
up with your own records.
Packing lines
input takes an array, so a line can carry many documents. Fewer, fatter lines mean a smaller file
and fewer audit log entries: 500 documents across two lines rather than 500 lines.
Pack to the token limit rather than the item limit: 2,048 short strings fit easily, 2,048 long documents will not. Chunks of 128–256 are a reasonable default.
namespace App\Actions\Lararouter;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Throwable;
class EmbedDocuments
{
public function handle(Collection $documents, int $chunkSize = 256): string
{
try {
$path = Storage::path('batches/embeddings-'.now()->timestamp.'.jsonl');
$handle = fopen($path, 'w');
$documents->chunk($chunkSize)->each(function (Collection $chunk) use ($handle) {
fwrite($handle, json_encode([
'custom_id' => 'chunk_'.md5($chunk->pluck('id')->implode('-')),
'method' => 'POST',
'url' => '/v1/embeddings',
'body' => [
'model' => 'bge-large-en-v1.5',
'input' => $chunk->pluck('body')->values()->all(),
],
]).PHP_EOL);
});
fclose($handle);
$file = Http::withToken(config('services.lararouter.key'))
->attach('file', file_get_contents($path), basename($path))
->post('https://REGION.api.lararouter.com/v1/files', ['purpose' => 'batch'])
->throw()
->json();
return Http::withToken(config('services.lararouter.key'))
->withHeaders(['Idempotency-Key' => 'embed-'.$file['id']])
->post('https://REGION.api.lararouter.com/v1/batches', [
'input_file_id' => $file['id'],
'endpoint' => '/v1/embeddings',
'completion_window' => '24h',
])
->throw()
->json('id');
} catch (Throwable $e) {
report($e);
throw $e;
}
}
}Deriving custom_id from the chunk contents means the mapping back to documents survives a retry, and
the idempotency key on the create stops a re-run paying to embed the same documents twice. See
Idempotency.
Cost
Embeddings bill on input tokens only. There is no output charge. Grouping usage by model separates embedding spend from chat spend:
curl -G https://REGION.api.lararouter.com/v1/usage/entities \
-H "Authorization: Bearer $LARAROUTER_API_KEY" \
-d "start=2026-08-01" \
-d "group_by=model"