# How User-Added Models Are Handled in the FreeLLMAPI Catalog

> Learn how FreeLLMAPI handles user-added models. Discover how custom models are registered, excluded from lifecycle operations, and still accessible via the API with your credentials.

- Repository: [Tashfeen/freellmapi](https://github.com/tashfeenahmed/freellmapi)
- Tags: how-to-guide
- Published: 2026-08-30

---

**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](https://github.com/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)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/model-state.ts) implements the check:

```typescript
// 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)](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)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/router.ts):

```typescript
// 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:

- **Rate limiting** ([[`server/src/services/ratelimit.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/ratelimit.ts)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/ratelimit.ts)) — quotas apply per custom endpoint key
- **Embeddings** ([[`server/src/services/embeddings.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/embeddings.ts)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/embeddings.ts)) — base URL selection for vector operations

## 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)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/model-listing.ts)) uses **`availableExpr`** to determine visibility:

```typescript
// 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)](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/model-groups.ts) assigns unscoped custom rows to the empty group `''`:

```typescript
// 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

```typescript
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

```typescript
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

```typescript
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

- **Platform flag**: `platform === 'custom'` identifies user-added models in [`server/src/services/model-state.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/model-state.ts)
- **Lifecycle exclusion**: Retirement, pruning, and feed updates ignore custom rows per [`model-retirement.test.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/model-retirement.test.ts)
- **Credential binding**: Custom models route through `customEndpointKeyIds` resolution in [`router.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/router.ts), [`ratelimit.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/ratelimit.ts), and [`embeddings.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/embeddings.ts)
- **Conditional visibility**: [`model-listing.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/model-listing.ts) shows custom entries only when their associated key exists
- **Group isolation**: [`model-groups.ts`](https://github.com/tashfeenahmed/freellmapi/blob/main/model-groups.ts) prevents custom rows from merging with provider-unified groups

## 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`](https://github.com/tashfeenahmed/freellmapi/blob/main/server/src/services/model-listing.ts) for the exact availability calculation.