# 自定义模型

通过 `~/.sf/agent/models.json` 添加自定义 providers 和 models（Ollama、vLLM、LM Studio、代理等）。

## 目录

- [最小示例](#minimal-example)
- [完整示例](#full-example)
- [支持的 API](#supported-apis)
- [Provider 配置](#provider-configuration)
- [Model 配置](#model-configuration)
- [覆盖内置 Providers](#overriding-built-in-providers)
- [按 model 覆盖](#per-model-overrides)
- [OpenAI 兼容性](#openai-compatibility)

<a id="minimal-example"></a>
## 最小示例

对于本地 models（Ollama、LM Studio、vLLM），每个 model 只要求提供 `id`：

```json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        { "id": "llama3.1:8b" },
        { "id": "qwen2.5-coder:7b" }
      ]
    }
  }
}
```

`apiKey` 在 schema 中是必填，但 Ollama 会忽略它，因此任意值都可以。

有些 OpenAI-compatible server 不支持推理模型使用的 `developer` role。对于这类 provider，需要把 `compat.supportsDeveloperRole` 设为 `false`，这样 SF 会改用 `system` message 发送 system prompt。如果该 server 同时也不支持 `reasoning_effort`，还应把 `compat.supportsReasoningEffort` 也设为 `false`。

你可以在 provider 级别设置 `compat`，让它应用到该 provider 下的所有 models；也可以在 model 级别单独覆盖某个 model。这个设置常见于 Ollama、vLLM、SGLang 以及类似的 OpenAI-compatible server。

```json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "compat": {
        "supportsDeveloperRole": false,
        "supportsReasoningEffort": false
      },
      "models": [
        {
          "id": "gpt-oss:20b",
          "reasoning": true
        }
      ]
    }
  }
}
```

<a id="full-example"></a>
## 完整示例

当你需要显式覆盖默认值时，可以写成更完整的配置：

```json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        {
          "id": "llama3.1:8b",
          "name": "Llama 3.1 8B (Local)",
          "reasoning": false,
          "input": ["text"],
          "contextWindow": 128000,
          "maxTokens": 32000,
          "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
        }
      ]
    }
  }
}
```

每次打开 `/model` 时，这个文件都会重新加载。可以在会话过程中直接编辑，无需重启。

<a id="supported-apis"></a>
## 支持的 API

| API | 说明 |
|-----|------|
| `openai-completions` | OpenAI Chat Completions（兼容性最好） |
| `openai-responses` | OpenAI Responses API |
| `anthropic-messages` | Anthropic Messages API |
| `google-generative-ai` | Google Generative AI |

`api` 可以设置在 provider 级别（作为该 provider 下所有 models 的默认值），也可以设置在 model 级别（覆盖单个 model）。

<a id="provider-configuration"></a>
## Provider 配置

| 字段 | 说明 |
|------|------|
| `baseUrl` | API endpoint URL |
| `api` | API 类型（见上） |
| `apiKey` | API key（见下方值解析） |
| `headers` | 自定义请求头（见下方值解析） |
| `authHeader` | 设为 `true` 时，自动添加 `Authorization: Bearer <apiKey>` |
| `models` | model 配置数组 |
| `modelOverrides` | 针对该 provider 的内置 models 做按 model 覆盖 |

<a id="value-resolution"></a>
### 值解析

`apiKey` 和 `headers` 支持三种写法：

- **Shell 命令：** `"!command"`，执行后读取 stdout
  ```json
  "apiKey": "!security find-generic-password -ws 'anthropic'"
  "apiKey": "!op read 'op://vault/item/credential'"
  ```
- **环境变量：** 取对应环境变量的值
  ```json
  "apiKey": "MY_API_KEY"
  ```
- **字面量：** 直接使用
  ```json
  "apiKey": "sk-..."
  ```

<a id="command-allowlist"></a>
#### 命令允许列表

Shell 命令（`!command`）只能执行一组已知的凭据工具。只有以下前缀开头的命令才会被允许：

`pass`、`op`、`aws`、`gcloud`、`vault`、`security`、`gpg`、`bw`、`gopass`、`lpass`

不在列表中的命令会被阻止，最终该值会解析为 `undefined`。同时会向 stderr 输出一条警告。

为了防止注入，命令参数中的 shell 操作符（`;`、`|`、`&`、`` ` ``、`$`、`>`、`<`）同样会被阻止。

**自定义允许列表：**

如果你使用的凭据工具不在默认列表中，可以在全局设置（`~/.sf/agent/settings.json`）里覆盖：

```json
{
  "allowedCommandPrefixes": ["pass", "op", "sops", "doppler", "mycli"]
}
```

这会完全替换默认列表，因此如果你还想保留默认命令，需要一起写进去。

你也可以设置 `SF_ALLOWED_COMMAND_PREFIXES` 环境变量（逗号分隔）。环境变量优先级高于 settings.json：

```bash
export SF_ALLOWED_COMMAND_PREFIXES="pass,op,sops,doppler"
```

> **注意：** 这是一个仅全局生效的设置。项目级 settings.json（`<project>/.sf/settings.json`）不能覆盖命令 allowlist，以防克隆下来的仓库提升命令执行权限。

### 自定义 Headers

```json
{
  "providers": {
    "custom-proxy": {
      "baseUrl": "https://proxy.example.com/v1",
      "apiKey": "MY_API_KEY",
      "api": "anthropic-messages",
      "headers": {
        "x-portkey-api-key": "PORTKEY_API_KEY",
        "x-secret": "!op read 'op://vault/item/secret'"
      },
      "models": [...]
    }
  }
}
```

<a id="model-configuration"></a>
## Model 配置

| 字段 | 必填 | 默认值 | 说明 |
|------|------|--------|------|
| `id` | 是 | — | Model 标识符（会原样传给 API） |
| `name` | 否 | `id` | 可读的 model 标签，用于匹配（例如 `--model` 模糊匹配）并显示在详情 / 状态文字里 |
| `api` | 否 | provider 的 `api` | 为这个 model 覆盖 provider 的 API 类型 |
| `reasoning` | 否 | `false` | 是否支持扩展 thinking |
| `input` | 否 | `["text"]` | 输入类型：`["text"]` 或 `["text", "image"]` |
| `contextWindow` | 否 | `128000` | 上下文窗口大小（tokens） |
| `maxTokens` | 否 | `16384` | 最大输出 tokens |
| `cost` | 否 | 全为 0 | `{"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}`（每百万 tokens） |
| `compat` | 否 | provider 的 `compat` | OpenAI 兼容性覆盖项。如果 provider 和 model 两边都配置了，会合并 |

当前行为：

- `/model` 与 `--list-models` 都是按 model `id` 列出条目
- 配置里的 `name` 会用于 model 匹配，以及详情 / 状态文本展示

<a id="overriding-built-in-providers"></a>
## 覆盖内置 Providers

如果你想把某个内置 provider 经由代理路由出去，但又不想重新定义全部 models，可以这样写：

```json
{
  "providers": {
    "anthropic": {
      "baseUrl": "https://my-proxy.example.com/v1"
    }
  }
}
```

这样所有内置 Anthropic models 仍然可用。已有的 OAuth 或 API key 认证也会继续生效。

如果你想把自定义 models 合并进某个内置 provider，就同时提供 `models` 数组：

```json
{
  "providers": {
    "anthropic": {
      "baseUrl": "https://my-proxy.example.com/v1",
      "apiKey": "ANTHROPIC_API_KEY",
      "api": "anthropic-messages",
      "models": [...]
    }
  }
}
```

合并规则如下：

- 内置 models 会保留
- 自定义 models 会按 `id` 在该 provider 下执行 upsert
- 如果某个自定义 model 的 `id` 与内置 model 相同，自定义 model 会替换那个内置 model
- 如果某个自定义 model 的 `id` 是新的，它会作为新增条目并列出现

<a id="per-model-overrides"></a>
## 按 model 覆盖

如果你只想修改某些特定的内置 model，而不想替换整个 provider 的 model 列表，可以使用 `modelOverrides`。

```json
{
  "providers": {
    "openrouter": {
      "modelOverrides": {
        "anthropic/claude-sonnet-4": {
          "name": "Claude Sonnet 4 (Bedrock Route)",
          "compat": {
            "openRouterRouting": {
              "only": ["amazon-bedrock"]
            }
          }
        }
      }
    }
  }
}
```

`modelOverrides` 支持的字段包括：`name`、`reasoning`、`input`、`cost`（可部分覆盖）、`contextWindow`、`maxTokens`、`headers`、`compat`。

行为说明：

- `modelOverrides` 只会应用到内置 provider 的 models 上
- 未知的 model ID 会被忽略
- 可以把 provider 级别的 `baseUrl` / `headers` 与 `modelOverrides` 组合使用
- 如果某个 provider 同时定义了 `models`，那么自定义 models 会在应用完内置覆盖后再合并；如果它的 `id` 与已覆盖的内置 model 相同，最终会以自定义 model 为准

<a id="openai-compatibility"></a>
## OpenAI 兼容性

对于只部分兼容 OpenAI 的 providers，可通过 `compat` 字段修正行为。

- provider 级别的 `compat` 会作为该 provider 下所有 models 的默认值
- model 级别的 `compat` 会覆盖该 model 的 provider 级别设置

```json
{
  "providers": {
    "local-llm": {
      "baseUrl": "http://localhost:8080/v1",
      "api": "openai-completions",
      "compat": {
        "supportsUsageInStreaming": false,
        "maxTokensField": "max_tokens"
      },
      "models": [...]
    }
  }
}
```

| 字段 | 说明 |
|------|------|
| `supportsStore` | Provider 是否支持 `store` 字段 |
| `supportsDeveloperRole` | 是否使用 `developer` 而非 `system` role |
| `supportsReasoningEffort` | 是否支持 `reasoning_effort` 参数 |
| `reasoningEffortMap` | 把 SF 的 thinking levels 映射到 provider 专属 `reasoning_effort` 值 |
| `supportsUsageInStreaming` | 是否支持 `stream_options: { include_usage: true }`（默认 `true`） |
| `maxTokensField` | 使用 `max_completion_tokens` 还是 `max_tokens` |
| `requiresToolResultName` | tool result message 中是否必须包含 `name` |
| `requiresAssistantAfterToolResult` | tool result 之后、user message 之前是否需要插入 assistant message |
| `requiresThinkingAsText` | 是否把 thinking block 转成纯文本 |
| `thinkingFormat` | 使用 `reasoning_effort`、`zai`、`qwen` 或 `qwen-chat-template` 的 thinking 参数格式 |
| `supportsStrictMode` | 是否在 tool definitions 中包含 `strict` 字段 |
| `openRouterRouting` | 传给 OpenRouter 的路由配置，用于 model/provider 选择 |
| `vercelGatewayRouting` | Vercel AI Gateway 的路由配置，用于 provider 选择（`only`、`order`） |

`qwen` 使用顶层 `enable_thinking`。对于要求 `chat_template_kwargs.enable_thinking` 的本地 Qwen-compatible server，请使用 `qwen-chat-template`。

示例：

```json
{
  "providers": {
    "openrouter": {
      "baseUrl": "https://openrouter.ai/api/v1",
      "apiKey": "OPENROUTER_API_KEY",
      "api": "openai-completions",
      "models": [
        {
          "id": "openrouter/anthropic/claude-3.5-sonnet",
          "name": "OpenRouter Claude 3.5 Sonnet",
          "compat": {
            "openRouterRouting": {
              "order": ["anthropic"],
              "fallbacks": ["openai"]
            }
          }
        }
      ]
    }
  }
}
```

Vercel AI Gateway 示例：

```json
{
  "providers": {
    "vercel-ai-gateway": {
      "baseUrl": "https://ai-gateway.vercel.sh/v1",
      "apiKey": "AI_GATEWAY_API_KEY",
      "api": "openai-completions",
      "models": [
        {
          "id": "moonshotai/kimi-k2.5",
          "name": "Kimi K2.5 (Fireworks via Vercel)",
          "reasoning": true,
          "input": ["text", "image"],
          "cost": { "input": 0.6, "output": 3, "cacheRead": 0, "cacheWrite": 0 },
          "contextWindow": 262144,
          "maxTokens": 262144,
          "compat": {
            "vercelGatewayRouting": {
              "only": ["fireworks", "novita"],
              "order": ["fireworks", "novita"]
            }
          }
        }
      ]
    }
  }
}
```
