# Custom Models (BYOK)

Connect your own API keys, use open source models, or run local models

The Droid CLI supports custom model configurations through BYOK (Bring Your Own Key). Use your own OpenAI or Anthropic keys, connect to any open source model providers, or run models locally on your hardware.

For Factory-managed models and multipliers, see [Available Models](/models).
For org-level restrictions on custom models, base URLs, and provider access, see [Enterprise Controls](/enterprise/hierarchical-settings-and-org-control).

<Note>
  Your API keys remain local and are not uploaded to Factory servers. Custom models are available in the Droid CLI and the desktop app, which read your local `settings.json`; they don't appear in Factory's hosted web or mobile platforms.
</Note>

[Install the CLI with the 5-minute quickstart →](/droid-cli/quickstart)

---

## Configuration reference

Add custom models to `~/.factory/settings.json` under the `customModels` array:

```json
{
  "customModels": [
    {
      "model": "your-model-id",
      "displayName": "My Custom Model",
      "baseUrl": "https://api.provider.com/v1",
      "apiKey": "${PROVIDER_API_KEY}",
      "provider": "generic-chat-completion-api",
      "maxOutputTokens": 16384
    }
  ]
}
```

<Tip>
  In `settings.json` (and `settings.local.json`), `apiKey` supports environment variable references using `${VAR_NAME}` syntax. For example, `"apiKey": "${PROVIDER_API_KEY}"` reads from the environment variable named `PROVIDER_API_KEY` (for example: `export PROVIDER_API_KEY=your_key_here`).
</Tip>

<Note>
  `apiKey` is optional for HTTP custom models. Omit it entirely for a **keyless** endpoint (for example, a gateway that authenticates via `extraHeaders` or network-level trust), or use [`apiKeyHelper`](#dynamic-credentials-with-apikeyhelper) to mint a short-lived token at request time. Some OpenAI-compatible servers require a non-empty `apiKey` even if they ignore its value; in that case, set any placeholder. `baseUrl` is still required for non-Bedrock models.
</Note>

<Note>
  **Legacy support**: Custom models in `~/.factory/config.json` using snake_case field names (`custom_models`, `base_url`, etc.) are still supported for backwards compatibility. Both files are loaded and merged, with `settings.json` taking priority. Environment variable expansion for `apiKey` does not apply to legacy `config.json`.
</Note>

### Supported fields

<PropertyList>
  <Property name='model' type='string' required>
    Model identifier sent via API (e.g., `claude-sonnet-4-5-20250929`,
    `gpt-5.3-codex`, `qwen3:4b`).
  </Property>
  <Property name='displayName' type='string'>
    Human-friendly name shown in model selector.
  </Property>
  <Property name='baseUrl' type='string' required>
    API endpoint base URL.
  </Property>
  <Property name='apiKey' type='string'>
    Your API key for the provider. Optional: omit it for a keyless endpoint,
    or use `apiKeyHelper` instead. If set, it can't be empty. Supports
    environment variable references; see the Tip above.
  </Property>
  <Property name='apiKeyHelper' type='string'>
    Shell command whose stdout is used as the API key, resolved at request
    time and refreshed on a TTL and after a `401`. Takes precedence over
    `apiKey`. Only honored from org-managed settings; see
    [Dynamic credentials](#dynamic-credentials-with-apikeyhelper).
  </Property>
  <Property name='apiKeyHelperTtlMs' type='number'>
    Refresh interval for `apiKeyHelper` output, in milliseconds. Defaults to
    5 minutes; the `FACTORY_API_KEY_HELPER_TTL_MS` environment variable
    overrides it.
  </Property>
  <Property name='provider' type='string' required>
    Provider type that determines API compatibility. See the Understanding
    Providers section below.
  </Property>
  <Property name='maxOutputTokens' type='number'>
    Maximum output tokens for model responses.
  </Property>
  <Property name='noImageSupport' type='boolean'>
    Set to `true` to disable image inputs for this model.
  </Property>
  <Property name='extraArgs' type='object'>
    Additional provider-specific arguments to include in API requests.
  </Property>
  <Property name='extraHeaders' type='object'>
    Additional HTTP headers to send with requests.
  </Property>
  <Property name='bedrock' type='object'>
    AWS Bedrock routing options (`awsRegion`, `awsProfile`, `bedrockBaseUrl`,
    `awsAuthRefresh`, `awsCredentialExport`, `requestMetadata`). See
    [AWS Bedrock](#aws-bedrock).
  </Property>
</PropertyList>

### Using extraArgs

Pass provider-specific parameters like temperature or top_p by adding an `extraArgs` object:

```json
{
  "extraArgs": {
    "temperature": 0.7,
    "top_p": 0.9
  }
}
```

### Using extraHeaders

Add custom HTTP headers to API requests by adding an `extraHeaders` object:

```json
{
  "extraHeaders": {
    "X-Custom-Header": "value",
    "Authorization": "Bearer YOUR_TOKEN"
  }
}
```

### Dynamic credentials with apiKeyHelper

Some gateways issue short-lived tokens (for example, an internal LLM gateway fronted by an identity provider) rather than a long-lived static key. Instead of a static `apiKey`, point the model at an `apiKeyHelper` command whose standard output is the token. Droid runs it at request time, caches the result, and refreshes it automatically.

```json
{
  "customModels": [
    {
      "model": "your-model",
      "displayName": "Gateway Model",
      "baseUrl": "https://llm-gateway.internal.example.com/v1",
      "provider": "generic-chat-completion-api",
      "apiKeyHelper": "/usr/local/bin/mint-gateway-token.sh",
      "apiKeyHelperTtlMs": 300000
    }
  ]
}
```

Behavior:

- The command's trimmed stdout is used as the credential (sent as the provider-appropriate auth header). `apiKeyHelper` takes precedence over any static `apiKey`.
- The token is cached and re-minted when it expires. TTL resolution order: the `FACTORY_API_KEY_HELPER_TTL_MS` environment variable, then the per-model `apiKeyHelperTtlMs`, then a 5-minute default.
- The token is also refreshed once after a `401` response. If the command fails, it enters a short cooldown and fails fast rather than retrying on every request.
- The token, command output, and command text are never logged.

<Warning>
  `apiKeyHelper` runs a shell command, so it is only honored from **org-managed (trusted) settings**. It is stripped from user, project, and folder settings so an untrusted repository cannot execute commands on your machine.
</Warning>

<Note>
  When adding custom models through **org-managed settings**, net-new models cannot use a static `apiKey` — use `apiKeyHelper`, a keyless endpoint, or a Bedrock model instead. Existing static-key models continue to work and can still rotate their key.
</Note>

This mirrors Claude Code's `apiKeyHelper` (the `FACTORY_API_KEY_HELPER_TTL_MS` variable is the analog of `CLAUDE_CODE_API_KEY_HELPER_TTL_MS`).

---

## AWS Bedrock

To route an Anthropic or OpenAI custom model through AWS Bedrock, add a `bedrock` object to the model config. Credentials are resolved through the standard AWS SDK provider chain (environment variables, shared config/credentials files, or an SSO/IAM profile).

```json
{
  "customModels": [
    {
      "model": "anthropic.claude-sonnet-4-5-20250929-v1:0",
      "displayName": "Sonnet 4.5 [Bedrock]",
      "provider": "anthropic",
      "apiKey": "not-used-for-bedrock",
      "bedrock": {
        "awsRegion": "${AWS_REGION}",
        "awsProfile": "${AWS_PROFILE}"
      }
    }
  ]
}
```

### Bedrock fields

| Field | Type | Description |
|-------|------|-------------|
| `awsRegion` | `string` | AWS region for Bedrock requests. Optional; when omitted, Droid falls back to the AWS default region chain. Supports `${VAR_NAME}` interpolation. |
| `awsProfile` | `string` | AWS profile name to resolve credentials and (as a last resort) a region. Supports `${VAR_NAME}` interpolation. |
| `bedrockBaseUrl` | `string` | Explicit Bedrock endpoint override. Accepts a full URL or a `${VAR_NAME}` reference that expands to one. |
| `awsAuthRefresh` | `string` | Shell command run to refresh AWS credentials before requests. |
| `awsCredentialExport` | `string` | Shell command whose output exports AWS credentials into the request environment. |
| `requestMetadata` | `object` | Key-value tags attached to every Bedrock inference call for this model, so Droid usage is attributable in Bedrock model-invocation logs and cost reports. Keys and values support `${VAR_NAME}` interpolation. See [Request metadata](#request-metadata). |

### Request metadata

`requestMetadata` is a map of string keys to string values that Droid sends on every Bedrock inference call for that model. AWS surfaces the tags in model-invocation logs and cost allocation, which is how you attribute Bedrock spend in your own AWS account back to Droid, a team, or a cost center:

```json
{
  "customModels": [
    {
      "model": "anthropic.claude-sonnet-4-5-20250929-v1:0",
      "displayName": "Sonnet 4.5 [Bedrock]",
      "provider": "anthropic",
      "apiKey": "not-used-for-bedrock",
      "bedrock": {
        "awsRegion": "us-west-2",
        "requestMetadata": {
          "tool": "droid",
          "team": "platform-eng",
          "cost_center": "${COST_CENTER}"
        }
      }
    }
  ]
}
```

Bedrock carries this parameter differently on each of its wire formats, so what Droid sends depends on the model's `provider`:

| `provider` | Bedrock API | How the tags are sent |
| --- | --- | --- |
| `anthropic` | InvokeModel / InvokeModelWithResponseStream | As the `X-Amzn-Bedrock-Request-Metadata` header, included in the SigV4 signature. |
| `bedrock-converse` | Converse / ConverseStream | As the `requestMetadata` field in the request body. |
| `openai` | Bedrock's OpenAI-compatible endpoint | Not sent. AWS does not support request metadata there, so the setting has no effect. |

AWS constrains the tags, and exceeding a limit fails every call on that model:

- At most **16 entries**.
- Keys are 1-256 characters; values are at most 256 characters.
- Keys and values may contain only alphanumerics, whitespace, and `: _ @ $ # = / + , . -`.

Droid checks these limits per model, at request time, on the two APIs that actually send the tags. A violation fails only that model with a message naming the offending key, rather than invalidating your whole settings file. If the block itself cannot be represented at all (not an object of string values, or two keys that collapse onto the same name after `${VAR_NAME}` expansion), Droid logs a warning naming the key, drops `requestMetadata` for that model, and keeps the rest of your settings.

<Warning>
  Tags are attribution data, not access control. They are sent to AWS on every call, and org-distributed values are readable by every member of the organization. Do not put secrets or credentials in `requestMetadata`.
</Warning>

### Environment variable interpolation

Like `apiKey`, the `awsRegion`, `awsProfile`, `bedrockBaseUrl`, and `requestMetadata` fields support `${VAR_NAME}` references in `settings.json`/`settings.local.json`, expanded per workspace at parse time. This lets a single centrally-distributed `customModels` config resolve to the correct value in each environment:

```json
{
  "bedrock": {
    "awsRegion": "${AWS_REGION}"
  }
}
```

If a referenced variable is not set, Droid fails fast with a clear error naming the missing variable instead of sending the literal `${VAR_NAME}` to AWS. In `requestMetadata`, keys are expanded alongside values, and an unresolved reference is only reported on the APIs that send the tags, so it never fails a model whose dialect ignores them.

<Note>
  `awsAuthRefresh` and `awsCredentialExport` are **not** expanded by the settings parser. They run through a shell, so `${VAR}` in those commands is substituted by the shell at run time against the child process environment.
</Note>

### Region resolution

When `awsRegion` is omitted, Droid resolves the region using the AWS default chain, in order:

1. `AWS_REGION`
2. `AWS_DEFAULT_REGION`
3. The `region` set on the resolved AWS profile in `~/.aws/config`

If none of these supply a region, Droid fails fast with a clear error rather than silently defaulting, so requests never target a region the workspace did not choose (important for VPC-endpoint region affinity).

<LabeledDivider label='Choosing a provider' />

## Understanding providers

Factory supports three provider types that determine API compatibility:

| Provider | API Format | Use For | Documentation |
|----------|------------|---------|---------------|
| `anthropic` | Anthropic Messages API (v1/messages) | Anthropic models on their official API or compatible proxies | [Anthropic Messages API](https://docs.claude.com/en/api/messages) |
| `openai` | OpenAI Responses API | OpenAI models on their official API or compatible proxies. Required for the newest models like GPT-5 and GPT-5-Codex. | [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) |
| `generic-chat-completion-api` | OpenAI Chat Completions API | OpenRouter, Fireworks, Together AI, Ollama, vLLM, and most open-source providers | [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat) |

<Warning>
  Factory is actively verifying Droid's performance on popular models, but we cannot guarantee that all custom models will work out of the box. Only Anthropic and OpenAI models accessed via their official APIs are fully tested and benchmarked.
</Warning>

<Note>
  **Model Size Consideration**: Models below 30 billion parameters have shown significantly lower performance on agentic coding tasks. While these smaller models can be useful for experimentation and learning, they are generally not recommended for production coding work or complex software engineering tasks.
</Note>

---

## Provider reference

Every provider below works through the configuration above. Use `provider: "generic-chat-completion-api"` unless you are calling OpenAI's or Anthropic's official API.

| Provider | `baseUrl` | Example `model` |
| --- | --- | --- |
| OpenAI (official) | `https://api.openai.com/v1` | `gpt-5.3-codex` (set `provider: "openai"`) |
| Anthropic (official) | `https://api.anthropic.com` | `claude-sonnet-4-5-20250929` (set `provider: "anthropic"`) |
| OpenRouter | `https://openrouter.ai/api/v1` | `openai/gpt-oss-20b` |
| Fireworks AI | `https://api.fireworks.ai/inference/v1` | `accounts/fireworks/models/deepseek-v3p1-terminus` |
| DeepInfra | `https://api.deepinfra.com/v1/openai` | `Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo` |
| Groq | `https://api.groq.com/openai/v1` | `moonshotai/kimi-k2-instruct-0905` |
| Baseten | `https://inference.baseten.co/v1` | your deployed model ID |
| Hugging Face | `https://router.huggingface.co/v1` | `openai/gpt-oss-120b:fireworks-ai` |
| Google Gemini | `https://generativelanguage.googleapis.com/v1beta/` | `gemini-2.5-pro` |

### Local models

Run models on your own hardware with an OpenAI-compatible server, then point `baseUrl` at it:

- **Ollama**: set `baseUrl` to `http://localhost:11434/v1` (no `apiKey` needed, though some builds want any placeholder value), and raise the context window to at least 32k (for example `OLLAMA_CONTEXT_LENGTH=32000 ollama serve`). Models with 30B+ parameters are recommended for agentic coding, and Ollama Cloud models work through the same endpoint.
- **LM Studio**: set `baseUrl` to `http://localhost:1234/v1`, load an OpenAI-compatible model, and start its local server.

<LabeledDivider label='After setup' />

## Prompt caching

The Droid CLI automatically uses prompt caching when available to reduce API costs:

- **Official providers (`anthropic`, `openai`)**: Factory attempts to use prompt caching via the official APIs. Caching behavior follows each provider's implementation and requirements.
- **Generic providers (`generic-chat-completion-api`)**: Prompt caching support varies by provider and cannot be guaranteed. Some providers may support caching, while others may not.

### Verifying prompt caching

To check if prompt caching is working correctly with your custom model:

1. Run a conversation with your custom model
2. Use the `/cost` command in the Droid CLI to view cost breakdowns
3. Look for cache hit rates and savings in the output

If you're not seeing expected caching savings, consult your provider's documentation about their prompt caching support and requirements.

---

## Using custom models

Once configured, access your custom models in the CLI:

1. Use the `/model` command
2. Your custom models appear in a separate "Custom models" section below Factory-provided models
3. Select any model to start using it

Custom models display with the name you set in `displayName`, making it easy to identify different providers and configurations.

---

## Troubleshooting

<Troubleshooting>
  <TroubleshootingItem title="Model not appearing in selector">
    - Check JSON syntax in `~/.factory/settings.json` (or `config.json` if using legacy format)
    - Settings changes are detected automatically via file watching
    - Verify all required fields are present
  </TroubleshootingItem>

  <TroubleshootingItem title='"Invalid provider" error'>
    - Provider must be exactly `anthropic`, `openai`, or `generic-chat-completion-api`
    - Check for typos and ensure proper capitalization
  </TroubleshootingItem>

  <TroubleshootingItem title="Authentication errors">
    - Verify your API key is valid and has available credits
    - Check that the API key has proper permissions
    - Confirm the base URL matches your provider's documentation
    - If using `apiKeyHelper`: run the command yourself and confirm it exits `0` and prints a valid token to stdout. A failed helper enters a short cooldown, and `apiKeyHelper` is only honored from org-managed settings.
  </TroubleshootingItem>

  <TroubleshootingItem title="Local model does not connect">
    - Ensure your local server is running (e.g., `ollama serve`)
    - Verify the base URL is correct and includes `/v1/` suffix if required
    - Check that the model is pulled/available locally
  </TroubleshootingItem>

  <TroubleshootingItem title="Rate limiting or quota errors">
    - Check your provider's rate limits and usage quotas
    - Monitor your usage through your provider's dashboard
  </TroubleshootingItem>
</Troubleshooting>

<RelatedLinks>
  <RelatedLink href='/models' title='Available Models'>
    Browse Factory-managed models and multipliers.
  </RelatedLink>
  <RelatedLink href='/enterprise/hierarchical-settings-and-org-control' title='Enterprise Controls & Managed Settings'>
    Restrict custom models, base URLs, and provider access at the org level.
  </RelatedLink>
</RelatedLinks>
