How User-Added Models Are Handled in the FreeLLMAPI Catalog

User-added models in FreeLLMAPI are registered under the special platform name custom, excluded from catalog lifecycle operations like retirement and pruning, yet remain accessible through the unified API when associated with a valid user credential.

FreeLLMAPI maintains a dual-type model system: catalog models sourced from a signed provider feed, and user-added models that operators register for custom endpoints. According to the tashfeenahmed/freellmapi source code, these custom models follow distinct persistence and routing rules that separate them from managed catalog state.

The custom Platform Flag

All user-added models are stored in the SQLite models table with platform set to the reserved string 'custom'. This single field acts as the primary discriminator throughout the codebase.

The isCatalogRow helper in [server/src/services/model-state.ts](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/model-state.ts) implements the check:

// Pseudocode based on source logic
function isCatalogRow(row: ModelRow): boolean {
  // Custom platform entries are NOT catalog state
  return !(row.platform === 'custom' && row.key_id == null);
}

Rows matching the custom platform with no automatic key assignment are treated as external to the catalog system.

Exclusion from Catalog Lifecycle Operations

Retirement and Pruning Immunity

The model-retirement service explicitly skips user-added models. Test coverage in [server/src/__tests__/services/model-retirement.test.ts](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/__tests__/services/model-retirement.test.ts) includes the assertion:

"never touches user-added models (they are not catalog state)"

This guarantee means:

  • Custom models survive feed updates that disable or remove provider models
  • Quota-driven pruning ignores platform === 'custom' rows
  • Version drift handling does not apply to user endpoints

No Catalog State Merging

Catalog models route through feed-based synchronization. User-added models bypass this entirely—their metadata (display name, context window, pricing) remains exactly as registered.

Routing and Credential Binding

Each custom model binds to a specific credential via customEndpointKeyIds, resolved in [server/src/services/router.ts](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts):

// Example: Router resolving custom endpoint scope
import { resolveCustomEndpoint } from './custom-endpoint.js';

async function routeRequest(modelId: string, credentialKey: string) {
  const model = await getModel(modelId);
  
  if (model.platform === 'custom') {
    // Use user-provided base URL, not provider feed
    const endpoint = await resolveCustomEndpoint(model.key_id);
    return forwardToEndpoint(endpoint.base_url, credentialKey);
  }
  
  // Standard catalog routing...
}

The same resolution pattern appears in:

Model Listing and Availability

The public /v1/models endpoint ([server/src/services/model-listing.ts](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/model-listing.ts)) uses availableExpr to determine visibility:

// Simplified listing logic
const availableExpr = sql`
  CASE 
    WHEN platform = 'custom' THEN (key_id IS NOT NULL)
    ELSE (catalog_enabled = 1 AND provider_key_valid = 1)
  END as available
`;

Custom models appear only when the associated credential exists. They are never deduplicated against catalog entries with similar IDs.

Model Group Isolation

The grouping logic in [server/src/services/model-groups.ts](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/model-groups.ts) assigns unscoped custom rows to the empty group '':

// Custom rows without concrete endpoint scope are isolated
function getModelGroup(row: ModelRow): string {
  if (row.platform === 'custom' && !row.endpoint_scope) {
    return ''; // Isolated group, prevents provider unification
  }
  return `${row.provider}/${row.endpoint_scope}`;
}

This prevents accidental load-balancing of requests between a user endpoint and a vendor API.

Practical Code Examples

Registering a User-Added Model

import { getDb } from '../db/index.js';

async function addCustomModel(userConfig: CustomModelConfig) {
  const db = await getDb();
  
  const result = await db.insertInto('models')
    .values({
      model_id: userConfig.modelId,
      platform: 'custom',              // Required discriminator
      display_name: userConfig.name,
      context_window: userConfig.contextWindow,
      endpoint_base_url: userConfig.baseUrl,
      key_id: await createEndpointKey(userConfig.apiKey),
    })
    .returningAll()
    .executeTakeFirst();
    
  return result;
}

Filtering Catalog vs. Custom in Application Code

import { isCatalogRow } from './services/model-state.js';

function partitionModels(allModels: ModelRow[]) {
  const catalog = allModels.filter(m => isCatalogRow(m));
  const custom = allModels.filter(m => m.platform === 'custom');
  
  console.log(`Catalog: ${catalog.length}, Custom: ${custom.length}`);
  return { catalog, custom };
}

Building a Model List Response

import { buildModelListing } from './services/model-listing.js';

app.get('/v1/models', async (req, res) => {
  const { models, object } = await buildModelListing({
    includeCustom: true,        // Surface user-added entries
    credentialCheck: req.apiKey // Filter by available keys
  });
  
  res.json({ object, data: models });
});

Summary

Frequently Asked Questions

How do I add a custom OpenAI-compatible endpoint to FreeLLMAPI?

Register the model with platform: 'custom' and supply the base URL plus API key. The system stores these in the models table with a key_id reference to your credential. The router automatically forwards requests to your endpoint when that model is selected.

Will my custom models disappear if the provider feed updates?

No. The retirement logic explicitly excludes platform === 'custom' rows. Your models persist independently of catalog synchronizations, feed changes, or provider rate limit adjustments.

Can I use the same model ID for a custom entry and a catalog model?

While technically possible in storage, the listing endpoint treats them as distinct entries. The availableExpr logic ensures no collision occurs—catalog availability depends on feed state, while custom availability depends solely on your credential validity.

Why does my custom model show as unavailable in /v1/models?

The available flag requires a valid key_id association. Verify your credential was properly linked during registration and that the key has not expired or been revoked. Check server/src/services/model-listing.ts for the exact availability calculation.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →