https://github.com/digitaldreams/local-batch-api
Drop-in replacement for the Anthropic and OpenAI batch APIs — self-hosted, running against local Ollama or LM Studio. Point your existing SDK code at this server instead of the real APIs. No API keys, no cloud costs, no rate limits.
https://github.com/digitaldreams/local-batch-api
anthropic-batch-api batch-api laravel lmstudio ollama openai-batch
Last synced: about 2 months ago
JSON representation
Drop-in replacement for the Anthropic and OpenAI batch APIs — self-hosted, running against local Ollama or LM Studio. Point your existing SDK code at this server instead of the real APIs. No API keys, no cloud costs, no rate limits.
- Host: GitHub
- URL: https://github.com/digitaldreams/local-batch-api
- Owner: digitaldreams
- Created: 2026-05-25T05:33:32.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-26T02:15:35.000Z (about 2 months ago)
- Last Synced: 2026-05-26T02:32:22.645Z (about 2 months ago)
- Topics: anthropic-batch-api, batch-api, laravel, lmstudio, ollama, openai-batch
- Language: PHP
- Homepage:
- Size: 43.9 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# local-batch-api
Drop-in replacement for the **Anthropic** and **OpenAI** batch APIs — self-hosted, running against local [Ollama](https://ollama.ai) or [LM Studio](https://lmstudio.ai). No API keys, no cloud costs, no rate limits.
---
## Requirements
- PHP 8.4+
- Laravel 13+
- Running [Ollama](https://ollama.ai/download) or [LM Studio](https://lmstudio.ai) instance
- A queue worker (`php artisan queue:work`)
---
## Installation
### Step 1 — Install the package
```bash
composer require digitaldreams/local-batch-api
```
### Step 2 — Run migrations
```bash
php artisan migrate
```
This creates two tables: `batches` and `batch_files`.
### Step 3 — Configure your inference backend
Add to your `.env`:
```env
# 'ollama' (default) or 'lmstudio'
INFERENCE_PROVIDER=ollama
# Base URL of your local inference server
INFERENCE_URL=http://localhost:11434 # Ollama default
# INFERENCE_URL=http://localhost:1234 # LM Studio default
# Default model (can be overridden per request)
INFERENCE_MODEL=llama3.2
# Seconds before a single request times out
INFERENCE_TIMEOUT=120
# Parallel requests per batch chunk — keep at 1 for CPU, raise to 3-5 for GPU
INFERENCE_CONCURRENCY=1
```
### Step 4 — Start a queue worker
Batch jobs run asynchronously. The worker must be running:
```bash
php artisan queue:work
```
---
## Two Ways to Use This Package
This package supports two independent usage patterns:
| | Event-based | REST API |
|---|---|---|
| **Who calls it** | Your own Laravel code | Any HTTP client (SDK, curl, external app) |
| **Auth** | Laravel's existing auth | Sanctum token (or your middleware) |
| **Routes needed** | No | Yes |
| **Best for** | Internal pipelines, jobs, commands | Replacing Anthropic/OpenAI SDK endpoints |
---
## Approach 1 — Event-based (Internal Usage)
Use this when your own Laravel application needs to submit and process batches. No HTTP routes required.
### Submitting an Anthropic-format batch
Fire a `SubmitAnthropicBatchEvent` event. The package listener picks it up and dispatches the processing job automatically.
```php
use BatchApi\Events\SubmitAnthropicBatchEvent;
use BatchApi\Data\Input\AnthropicBatchItemDto;
$items = [
new AnthropicBatchItemDto(
customId: 'req-1',
maxTokens: 512,
messages: [
['role' => 'user', 'content' => 'Summarise this article in one paragraph.'],
],
),
new AnthropicBatchItemDto(
customId: 'req-2',
maxTokens: 256,
messages: [
['role' => 'user', 'content' => 'What is the capital of France?'],
],
system: 'You are a geography expert.',
),
];
event(new SubmitAnthropicBatchEvent($items));
```
### Submitting an OpenAI-format batch
The OpenAI flow requires a file ID. Upload first using the `BatchService`, then fire the event.
```php
use BatchApi\BatchService;
use BatchApi\Events\SubmitOpenAiBatchEvent;
use BatchApi\Data\Input\OpenAiBatchItemDto;
$service = app(BatchService::class);
// Build items from raw JSONL or manually
$items = [
new OpenAiBatchItemDto(
customId: 'req-1',
messages: [['role' => 'user', 'content' => 'Hello']],
maxTokens: 512,
),
];
// Create a file record (mirrors OpenAI's file upload step)
$file = $service->uploadFile(
collect($items)->map(fn ($item) => json_encode([
'custom_id' => $item->customId,
'method' => 'POST',
'url' => '/v1/chat/completions',
'body' => ['messages' => $item->messages, 'max_tokens' => $item->maxTokens],
]))->implode("\n")
);
event(new SubmitOpenAiBatchEvent($file->id, $items));
```
### Listening for results
Listen to `BatchCompletedEvent` to act on results when processing finishes:
```php
// app/Listeners/HandleBatchCompletedListener.php
use BatchApi\Events\BatchCompletedEvent;
use BatchApi\Data\BatchResultDto;
class HandleBatchCompletedListener
{
public function handle(BatchCompletedEvent $event): void
{
$batch = $event->batch;
foreach ($event->results as $result) {
/** @var BatchResultDto $result */
if ($result->succeeded) {
// $result->customId — matches your request's custom_id
// $result->content — the model's response text
// $result->model — model used
// $result->inputTokens / $result->outputTokens
} else {
// $result->error — failure message
}
}
}
}
```
Register it in `AppServiceProvider::boot()`:
```php
// app/Providers/AppServiceProvider.php
use BatchApi\Events\BatchCompletedEvent;
use App\Listeners\HandleBatchCompletedListener;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(BatchCompletedEvent::class, HandleBatchCompletedListener::class);
}
```
### All available events
| Event | Properties | Fired when |
|-------|-----------|------------|
| `BatchCreatedEvent` | `$batch`, `$items`, `$provider` | Batch record saved, job dispatched |
| `BatchProcessingEvent` | `$batch` | Queue worker picks up the job |
| `BatchItemStartedEvent` | `$batch`, `$dto` | Single request about to fire |
| `BatchItemCompletedEvent` | `$batch`, `$result` | Single request finished |
| `BatchCompletedEvent` | `$batch`, `$results` | All requests done |
| `BatchFailedEvent` | `$batch`, `$exception` | Job threw an unrecoverable error |
| `BatchCancelledEvent` | `$batch` | Batch cancelled |
---
## Approach 2 — REST API (External HTTP Clients)
Use this when you want to **point an existing Anthropic or OpenAI SDK** at your local server instead of the cloud. The API surface is identical to the real APIs.
### Step 1 — Register routes with authentication
Do **not** set `BATCH_API_EXPOSE_ROUTES=true`. Instead, register routes manually inside a protected middleware group so you control authentication.
Install Sanctum if you haven't already:
```bash
composer require laravel/sanctum
php artisan install:api
```
In your `routes/api.php` (or a service provider), wrap `BatchApi::routes()` with Sanctum middleware:
```php
use BatchApi\Facades\BatchApi;
Route::middleware('auth:sanctum')->group(function () {
BatchApi::routes();
});
```
This registers all 11 endpoints, each requiring a valid Sanctum token.
> **Note:** `BatchApi::routes()` also applies the `api` middleware internally. Wrapping it with `auth:sanctum` stacks both, so your routes have `api` + `auth:sanctum`.
### Step 2 — Issue a token
```php
// In a controller or seeder
$token = $user->createToken('batch-api-client')->plainTextToken;
// Pass this token to the HTTP client
```
### Step 3 — Call the API
All requests need the token in the `Authorization` header:
```
Authorization: Bearer
```
---
### Anthropic Batch API — Step by Step
#### 1. Submit a batch
```http
POST /api/anthropic/v1/messages/batches
Content-Type: application/json
Authorization: Bearer
```
```json
{
"requests": [
{
"custom_id": "req-1",
"params": {
"model": "llama3.2",
"max_tokens": 512,
"messages": [
{ "role": "user", "content": "Say hello in one sentence." }
]
}
},
{
"custom_id": "req-2",
"params": {
"model": "llama3.2",
"max_tokens": 512,
"system": "You are a pirate. Always respond like a pirate.",
"messages": [
{ "role": "user", "content": "What is the capital of France?" }
]
}
}
]
}
```
Response `202 Accepted`:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "message_batch",
"processing_status": "in_progress",
"request_counts": { "processing": 2, "succeeded": 0, "errored": 0, "canceled": 0, "expired": 0 },
"created_at": "2026-05-25T10:00:00+00:00",
"expires_at": "2026-05-26T10:00:00+00:00",
"ended_at": null,
"cancel_initiated_at": null,
"results_url": null
}
```
#### 2. Poll until done
```http
GET /api/anthropic/v1/messages/batches/{id}
Authorization: Bearer
```
Keep polling until `processing_status` is `"ended"`.
#### 3. Fetch results (NDJSON)
```http
GET /api/anthropic/v1/messages/batches/{id}/results
Accept: application/x-ndjson
Authorization: Bearer
```
Returns `204 No Content` if still processing. When ready, streams one JSON object per line:
```jsonl
{"custom_id":"req-1","result":{"type":"succeeded","message":{"id":"msg_abc","type":"message","role":"assistant","model":"llama3.2","content":[{"type":"text","text":"Hello! Great to meet you."}],"stop_reason":"end_turn","usage":{"input_tokens":12,"output_tokens":10}}}}
{"custom_id":"req-2","result":{"type":"errored","error":{"type":"server_error","message":"Ollama timeout"}}}
```
#### Other Anthropic endpoints
```http
GET /api/anthropic/v1/messages/batches # list (supports ?limit=&before_id=&after_id=)
POST /api/anthropic/v1/messages/batches/{id}/cancel # cancel
```
---
### OpenAI Batch API — Step by Step
#### 1. Upload a JSONL file
Create a `.jsonl` file (one request per line):
```jsonl
{"custom_id":"req-1","method":"POST","url":"/v1/chat/completions","body":{"model":"llama3.2","messages":[{"role":"user","content":"Hello"}],"max_tokens":512}}
{"custom_id":"req-2","method":"POST","url":"/v1/chat/completions","body":{"model":"llama3.2","messages":[{"role":"user","content":"What is 2+2?"}],"max_tokens":256}}
```
Upload it:
```http
POST /api/openai/v1/files
Content-Type: multipart/form-data
Authorization: Bearer
file=@requests.jsonl
purpose=batch
```
Response `201 Created`:
```json
{
"id": "file-abc123",
"object": "file",
"purpose": "batch",
"created_at": 1716631200
}
```
#### 2. Submit the batch
```http
POST /api/openai/v1/batches
Content-Type: application/json
Authorization: Bearer
```
```json
{
"input_file_id": "file-abc123",
"endpoint": "/v1/chat/completions",
"completion_window": "24h"
}
```
Response `201 Created`:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"object": "batch",
"status": "validating",
"input_file_id": "file-abc123",
"output_file_id": null,
"request_counts": { "total": 2, "completed": 0, "failed": 0 }
}
```
#### 3. Poll until completed
```http
GET /api/openai/v1/batches/{id}
Authorization: Bearer
```
Poll until `status` is `"completed"`. Note the `output_file_id` in the response.
#### 4. Download results
```http
GET /api/openai/v1/files/{output_file_id}/content
Authorization: Bearer
```
Returns JSONL, one result per line:
```jsonl
{"id":"batch_req_abc","custom_id":"req-1","response":{"status_code":200,"body":{"id":"chatcmpl-123","object":"chat.completion","model":"llama3.2","choices":[{"index":0,"message":{"role":"assistant","content":"Hello! How can I help?"},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":8,"total_tokens":18}}},"error":null}
```
#### Other OpenAI endpoints
```http
GET /api/openai/v1/batches # list (supports ?limit=&after=)
POST /api/openai/v1/batches/{id}/cancel # cancel
```
---
### Pointing an existing SDK at this server
**Python (Anthropic SDK):**
```python
import anthropic
client = anthropic.Anthropic(
api_key="any-value", # required by SDK but not validated here
base_url="http://localhost:8000/api/anthropic",
default_headers={"Authorization": "Bearer "},
)
```
**Python (OpenAI SDK):**
```python
from openai import OpenAI
client = OpenAI(
api_key="any-value",
base_url="http://localhost:8000/api/openai",
default_headers={"Authorization": "Bearer "},
)
```
---
## Batch Status Lifecycle
```
pending → processing → completed
→ failed
→ cancelling → cancelled
```
| Internal | Anthropic `processing_status` | OpenAI `status` |
|----------|-------------------------------|-----------------|
| `pending` | `in_progress` | `validating` |
| `processing` | `in_progress` | `in_progress` |
| `completed` | `ended` | `completed` |
| `failed` | `ended` | `failed` |
| `cancelling` | `canceling` | `cancelling` |
| `cancelled` | `ended` | `cancelled` |
Batches expire after 24 hours.
---
## Switching to LM Studio
1. Open LM Studio → start the local server (default port `1234`)
2. Load a model
3. Update `.env`:
```env
INFERENCE_PROVIDER=lmstudio
INFERENCE_URL=http://localhost:1234
INFERENCE_MODEL=your-model-name
```
No other changes needed.
---
## Concurrency Tuning
`INFERENCE_CONCURRENCY` controls parallel requests per batch chunk.
| Hardware | Value |
|----------|-------|
| CPU-only | `1` |
| GPU with spare VRAM | `3`–`5` |
---
## Postman Collection
Import `Local-Batch-API.postman_collection.json`. Set the `baseUrl` variable to your server URL. The collection auto-saves batch IDs and file IDs between requests so you can run folders top-to-bottom without manually copying values.
---
## Troubleshooting
**Batches stay `pending` forever** — Queue worker not running. Run `php artisan queue:work`.
**`Ollama timeout` in results** — Model is slow or `INFERENCE_TIMEOUT` too low. Raise to `300`.
**Routes return 404** — Routes not registered. Either set `BATCH_API_EXPOSE_ROUTES=true` (no auth) or call `BatchApi::routes()` manually in a middleware group.
**401 Unauthorized on API routes** — Sanctum token missing or invalid. Pass `Authorization: Bearer ` header.
**`cannot chdir` git error in submodule** — Run `git submodule update --init` in the parent repo.
---
## License
MIT