How to Set Up Custom OpenAI-Compatible Endpoints with FreeLLMAPI
Send a POST request to /keys/custom with your baseUrl and model list, then use the registered model names in standard OpenAI API calls—FreeLLMAPI handles the rest automatically.
FreeLLMAPI is an open-source LLM gateway that unifies multiple providers behind a single OpenAI-compatible API. While it ships with built-in support for Groq, Mistral, and OpenRouter, its custom provider system lets you integrate any OpenAI-compatible endpoint—whether that's Ollama running locally, a self-hosted vLLM instance, or a third-party API service. This guide walks through the complete setup process based on the actual source implementation in tashfeenahmed/freellmapi.
Understanding the Custom Provider Architecture
FreeLLMAPI treats custom endpoints as dynamic providers that are created on-the-fly rather than initialized at startup. This differs from built-in providers like Groq or Mistral, which are instantiated once in server/src/providers/index.ts.
The architecture follows this flow:
- Registration: Your
POST /keys/customrequest stores the endpoint configuration in the database - Model binding: Each model you specify gets inserted into the
modelstable with a foreign key to your endpoint - Runtime resolution: When a chat request arrives,
resolveProvider()constructs anOpenAICompatProviderinstance using your storedbaseUrl - Request forwarding: The provider forwards calls to
baseUrl + '/v1/chat/completions'with your stored API key
This design means you don't need to restart the server or modify configuration files—endpoints are immediately available after registration.
Registering a Custom Endpoint
Required Parameters
The POST /keys/custom endpoint in server/src/routes/keys.ts expects:
| Parameter | Type | Required | Description |
|---|---|---|---|
baseUrl |
string | Yes | The OpenAI-compatible base URL (e.g., http://localhost:11434/v1) |
models |
array | Yes | List of model objects or strings to register |
apiKey |
string | No | Authentication key, or omit/"no-key" for keyless services |
label |
string | No | Human-readable name for this endpoint |
Example: Registering Ollama
curl -X POST https://your-llmapi.example.com/keys/custom \
-H "Content-Type: application/json" \
-d '{
"baseUrl": "http://localhost:11434/v1",
"apiKey": "no-key",
"label": "Local Ollama",
"models": [
{"model": "llama3:8b", "displayName": "Llama 3 8B"},
{"model": "phi2", "displayName": "Phi-2"}
]
}'
Response structure:
{
"success": true,
"keyId": 23,
"platform": "custom",
"baseUrl": "http://localhost:11434/v1",
"model": "llama3:8b",
"displayName": "Llama 3 8B",
"models": [
{"modelDbId": 101, "model": "llama3:8b", "displayName": "Llama 3 8B"},
{"modelDbId": 102, "model": "phi2", "displayName": "Phi-2"}
],
"maskedKey": "no-key"
}
The keysRouter.post('/custom', ...) handler in server/src/routes/keys.ts performs three operations: it creates or updates a row in api_keys with platform = 'custom', normalizes the model list to handle both string and object formats, and inserts each model into the models table with the appropriate foreign key reference.
How Provider Resolution Works at Runtime
When a client sends a chat completion request using a model from your custom endpoint, FreeLLMAPI needs to determine where to route it. This happens through the resolveProvider() function in server/src/providers/index.ts:
export function resolveProvider(platform: Platform, baseUrl?: string | null): BaseProvider | undefined {
if (platform === 'custom') {
const trimmed = baseUrl?.trim();
if (!trimmed) return undefined;
return new OpenAICompatProvider({
platform: 'custom',
name: 'Custom (OpenAI-compatible)',
baseUrl: trimmed,
timeoutMs: CUSTOM_PROVIDER_TIMEOUT_MS, // 30 seconds
});
}
return providers.get(platform);
}
Key details:
- Built-in providers are cached in a
Map<Platform, BaseProvider> - Custom providers are instantiated fresh for each request using the stored
baseUrl - Custom providers receive an extended 30-second timeout (
CUSTOM_PROVIDER_TIMEOUT_MS) compared to built-in providers, accommodating slower self-hosted inference
The getProviderForModel() function handles the database lookup: it queries the models table by model identifier, retrieves the associated api_keys row including the base_url field, and calls resolveProvider() with the appropriate platform and URL.
The OpenAICompatProvider Implementation
The OpenAICompatProvider class in server/src/providers/openai-compat.ts provides the actual transport layer for custom endpoints. It extends BaseProvider and implements four key methods:
Authentication Handling
The authHeader() method supports both authenticated and keyless services:
private authHeader(apiKey: string): Record<string, string> {
if (apiKey === 'no-key') {
return {};
}
return { 'Authorization': `Bearer ${apiKey}` };
}
When you register with "no-key" or omit the apiKey field, the provider sends requests without an Authorization header—essential for local services like Ollama that don't require authentication.
Request Routing
The provider normalizes the base URL by stripping trailing /v1/ paths, then constructs full URLs:
- Chat completions:
${baseUrl}/v1/chat/completions - Model listing:
${baseUrl}/v1/models(used for key validation)
Timeout Configuration
The constructor accepts a timeoutMs parameter (defaulting to 30 seconds) and uses AbortController to enforce it:
private async fetchWithTimeout(url: string, options: any): Promise<any> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
// ... fetch with signal
}
Using Your Custom Endpoint
Once registered, use your custom models exactly like any OpenAI-compatible model:
curl -X POST https://your-llmapi.example.com/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama3:8b",
"messages": [{"role": "user", "content": "Hello from FreeLLMAPI"}],
"temperature": 0.7
}'
The server executes this resolution chain:
- Looks up
"llama3:8b"in themodelstable, findingplatform = "custom"andkey_id = 23 - Retrieves the
api_keysrow for ID 23, gettingbase_url = "http://localhost:11434/v1"andkey = "no-key" - Calls
resolveProvider(Platform.CUSTOM, "http://localhost:11434/v1") - The
OpenAICompatProviderforwards the request tohttp://localhost:11434/v1/chat/completions
Managing Custom Endpoints
List All Custom Endpoints
curl https://your-llmapi.example.com/keys/custom
Returns all custom configurations with masked keys and their associated models.
Delete a Custom Endpoint
curl -X DELETE https://your-llmapi.example.com/keys/custom/23
This cascades to delete associated models from the models table due to the foreign key constraint defined in server/src/db/migrations.ts.
Database Schema for Custom Providers
The migration in server/src/db/migrations.ts defines the schema supporting custom endpoints:
table.enum('platform', ['groq', 'mistral', 'openrouter', 'custom']).notNullable();
table.text('base_url').nullable(); // For custom OpenAI-compatible endpoints
The base_url column is nullable for built-in providers but required for custom providers. The Platform enum in shared/types.ts includes CUSTOM = 'custom', ensuring type safety across the codebase.
Summary
- Single POST to
/keys/customregisters any OpenAI-compatible endpoint with FreeLLMAPI - Dynamic provider instantiation means no server restarts or configuration file edits required
- Automatic routing via
resolveProvider()inserver/src/providers/index.tshandles request forwarding - Flexible authentication supports both API-key and keyless services through the
OpenAICompatProvider.authHeader()method - Extended timeouts (30 seconds) accommodate slower self-hosted inference compared to cloud providers
Frequently Asked Questions
Can I register multiple custom endpoints?
Yes. Each unique baseUrl creates a distinct entry in the api_keys table with its own key_id. You can register the same model name on different endpoints—FreeLLMAPI routes based on the specific model-to-key mapping stored in the database.
What if my custom endpoint uses a different path structure?
The OpenAICompatProvider expects standard OpenAI API paths (/v1/chat/completions, /v1/models). If your service uses different paths, you may need to proxy it or modify the baseUrl to include any prefix paths before the standard /v1/ segment.
How does FreeLLMAPI handle authentication for local services?
When you register with "apiKey": "no-key" or omit the field entirely, the provider's authHeader() method returns an empty object, and requests are sent without an Authorization header. This matches the behavior of local inference servers like Ollama or text-generation-inference in unauthenticated mode.
Can I update an existing custom endpoint's models?
Yes. Sending another POST /keys/custom request with the same baseUrl updates the existing api_keys row and adds or updates models in the models table. The handler in server/src/routes/keys.ts checks for existing entries and performs updates rather than duplicate inserts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →