# 自定义模型
通过 `~/.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)
## 最小示例
对于本地 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
}
]
}
}
}
```
## 完整示例
当你需要显式覆盖默认值时,可以写成更完整的配置:
```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` 时,这个文件都会重新加载。可以在会话过程中直接编辑,无需重启。
## 支持的 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)。
## Provider 配置
| 字段 | 说明 |
|------|------|
| `baseUrl` | API endpoint URL |
| `api` | API 类型(见上) |
| `apiKey` | API key(见下方值解析) |
| `headers` | 自定义请求头(见下方值解析) |
| `authHeader` | 设为 `true` 时,自动添加 `Authorization: Bearer ` |
| `models` | model 配置数组 |
| `modelOverrides` | 针对该 provider 的内置 models 做按 model 覆盖 |
### 值解析
`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-..."
```
#### 命令允许列表
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(`/.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": [...]
}
}
}
```
## 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 匹配,以及详情 / 状态文本展示
## 覆盖内置 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` 是新的,它会作为新增条目并列出现
## 按 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 为准
## 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"]
}
}
}
]
}
}
}
```