Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,67 @@ deepagent-code
deepagent
```

## Adding a Provider

Before you can run a task, DeepAgent Code needs at least one model provider. It
supports 75+ providers through the [AI SDK](https://ai-sdk.dev/) and
[models.dev](https://models.dev), plus any OpenAI- or Anthropic-compatible
endpoint. Pick whichever path fits how you work.

### Desktop app (recommended)

Open **Settings → Providers**:

- **Official providers** (OpenAI, Anthropic, DeepSeek, Google, xAI, ZhipuAI/GLM):
click **Connect**, paste your API key.
- **Any other provider or gateway**: click **Connect** on *Custom provider*, paste
the **Base URL** and **API key**. DeepAgent Code auto-detects the protocol
(OpenAI-compatible or Anthropic) and discovers the available models from the
endpoint's `/models` list — you don't have to fill anything else.

Model specs (context window, reasoning) are auto-filled by matching each model
against the models.dev catalog. You can reopen a custom provider to override a
model's context/reasoning/temperature; those overrides are best-effort and not
guaranteed to keep the model working.

### Terminal

```bash
# Log in to a provider (official providers, or a plugin auth flow)
deepagent auth login

# See what's connected
deepagent auth list
```

### Config file

Providers also live in `~/.deepagent/code/config.jsonc`. A custom
OpenAI-compatible endpoint looks like this — set `discovery: true` to have models
refreshed from the endpoint at runtime, or list them explicitly under `models`:

```jsonc
{
"$schema": "https://deepagent-code.ai/config.json",
"provider": {
"myprovider": {
"name": "My Provider",
"npm": "@ai-sdk/openai-compatible",
"discovery": true,
"options": {
"baseURL": "https://api.myprovider.com/v1",
"apiKey": "sk-..."
}
}
}
}
```

Official-provider keys added via the app/CLI are stored separately in
`~/.deepagent/code/auth.json`, not in the config file. See the
[providers guide](https://deepagent-code.ai/docs/providers/) for the full
reference (base URL overrides, headers, per-model config, gateways).

## Quick Example

Start the agent and give it a task:
Expand Down Expand Up @@ -216,6 +277,7 @@ bun run --cwd packages/deepagent-code dev import-history --from codex --dry-run

## Documentation

- [Providers & Models](https://deepagent-code.ai/docs/providers/)
- [Architecture & Design](design/README.md)
- [Security Policy](SECURITY.md)
- [Privacy Policy](PRIVACY.md)
Expand Down
57 changes: 57 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,62 @@ deepagent-code
deepagent
```

## 添加供应商

在运行任务之前,DeepAgent Code 至少需要一个模型供应商。它通过
[AI SDK](https://ai-sdk.dev/) 和 [models.dev](https://models.dev) 支持 75+ 家供应商,
以及任意 OpenAI 或 Anthropic 兼容的接口。按你习惯的方式选一种即可。

### 桌面应用(推荐)

打开 **设置 → 供应商(Settings → Providers)**:

- **官方供应商**(OpenAI、Anthropic、DeepSeek、Google、xAI、智谱/GLM):点击
**连接**,粘贴你的 API Key。
- **其他供应商或网关**:在「自定义供应商」上点击 **连接**,填入 **Base URL** 和
**API Key**。DeepAgent Code 会自动探测协议(OpenAI 兼容或 Anthropic),并从接口的
`/models` 列表自动发现可用模型——其余字段无需填写。

模型规格(上下文窗口、推理能力)会通过与 models.dev 目录按模型 id 匹配来自动补全。
你可以再次打开自定义供应商,覆盖某个模型的上下文/推理/温度;这些覆盖为尽力而为的默认值,
修改后不保证模型仍能正常使用。

### 终端

```bash
# 登录供应商(官方供应商,或插件鉴权流程)
deepagent auth login

# 查看已连接的供应商
deepagent auth list
```

### 配置文件

供应商也保存在 `~/.deepagent/code/config.jsonc` 中。一个自定义 OpenAI 兼容接口如下——
设 `discovery: true` 让模型在运行时从接口刷新,或在 `models` 下显式列出:

```jsonc
{
"$schema": "https://deepagent-code.ai/config.json",
"provider": {
"myprovider": {
"name": "My Provider",
"npm": "@ai-sdk/openai-compatible",
"discovery": true,
"options": {
"baseURL": "https://api.myprovider.com/v1",
"apiKey": "sk-..."
}
}
}
}
```

通过应用/CLI 添加的官方供应商密钥单独存放在 `~/.deepagent/code/auth.json`,不在配置文件里。
完整参考(Base URL 覆盖、请求头、逐模型配置、网关)见
[供应商文档](https://deepagent-code.ai/docs/providers/)。

## 快速示例

启动智能体并交给它一个任务:
Expand Down Expand Up @@ -216,6 +272,7 @@ bun run --cwd packages/deepagent-code dev import-history --from codex --dry-run

## 文档

- [供应商与模型](https://deepagent-code.ai/docs/providers/)
- [架构与设计](design/README.md)
- [安全策略](SECURITY.md)
- [隐私策略](PRIVACY.md)
Expand Down
112 changes: 104 additions & 8 deletions packages/app/src/components/dialog-custom-provider-form.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { isOfficialProvider } from "@deepagent-code/core/provider-official"
import type { Provider as ResolvedProvider, ProviderConfig } from "@deepagent-code/sdk/v2"

const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
const ANTHROPIC = "@ai-sdk/anthropic"

export type ProviderProtocol = "openai-compatible" | "anthropic"

// Per-model spec override written under `provider.<id>.models.<id>`. Only the fields the user actually
// set are emitted, so a blank field never clobbers the backend catalog-fill with a zero/false.
export type CustomModelConfig = {
name: string
reasoning?: boolean
temperature?: boolean
limit?: { context: number; output?: number }
}

// The config payload written under `provider.<id>`. `discovery` and `models` are mutually exclusive
// in practice (discovery mode emits an empty models map), but both are typed optional so the emitted
// object has one consistent shape instead of a union callers must narrow.
Expand All @@ -19,7 +29,7 @@ export type CustomProviderConfig = {
headers?: Record<string, string>
}
discovery?: boolean
models: Record<string, { name: string }>
models: Record<string, CustomModelConfig>
}

const npmForProtocol = (kind: ProviderProtocol | undefined) => (kind === "anthropic" ? ANTHROPIC : OPENAI_COMPATIBLE)
Expand All @@ -34,6 +44,7 @@ type Translator = (key: string, vars?: Record<string, string | number | boolean>
export type ModelErr = {
id?: string
name?: string
context?: string
}

export type HeaderErr = {
Expand All @@ -45,6 +56,11 @@ export type ModelRow = {
row: string
id: string
name: string
// Editable spec overrides. `context` is a text field (parsed to a positive int on save; blank means
// "let the backend/catalog fill it"). reasoning/temperature are booleans.
context: string
reasoning: boolean
temperature: boolean
err: ModelErr
}

Expand Down Expand Up @@ -82,6 +98,14 @@ type ValidateArgs = {
// every load instead of freezing them into config. Manual models always take precedence and turn
// this off for that provider.
discovery?: boolean
// Edit mode: the provider being edited was persisted with `discovery: true`. We keep discovery on
// (so the backend still refreshes the model list) AND emit the user's per-model spec overrides,
// which the build loop merges over the discovered models (manual wins per-id). Without this, saving
// spec edits would freeze the model snapshot and disable runtime refresh.
editDiscovery?: boolean
// Providers whose ids are already taken but belong to the provider being edited (so an edit doesn't
// trip the "already exists" check on its own id).
editingProviderID?: string
}

// Turn a base URL into a stable, unique provider id + a human display name so the user only has to
Expand Down Expand Up @@ -178,9 +202,11 @@ export function validateCustomProvider(input: ValidateArgs) {
const nameError = !name ? input.t("provider.custom.error.name.required") : undefined

const disabled = input.disabledProviders.includes(providerID)
// Editing a provider keeps its own id — don't flag that as a collision.
const isSelf = input.editingProviderID === providerID
const existsError = idError
? undefined
: input.existingProviderIDs.has(providerID) && !disabled
: input.existingProviderIDs.has(providerID) && !disabled && !isSelf
? input.t("provider.custom.error.providerID.exists")
: undefined

Expand All @@ -201,10 +227,25 @@ export function validateCustomProvider(input: ValidateArgs) {
return undefined
})()
const nameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined
return { id: idError, name: nameError }
const ctx = m.context.trim()
// Blank context is allowed (backend/catalog fills it); a non-empty value must be a positive int.
const contextError = ctx && !/^\d+$/.test(ctx) ? input.t("provider.custom.error.context") : undefined
return { id: idError, name: nameError, context: contextError }
})
const modelsValid = discoveryMode || models.every((m) => !m.id && !m.name)
const modelConfig = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
const modelsValid =
(discoveryMode || models.every((m) => !m.id && !m.name)) && models.every((m) => !m.context)
const modelConfig = Object.fromEntries(
input.form.models.map((m) => {
const ctx = m.context.trim()
const spec: CustomModelConfig = {
name: m.name.trim(),
...(m.reasoning ? { reasoning: true } : {}),
...(m.temperature ? { temperature: true } : {}),
...(ctx ? { limit: { context: Number(ctx) } } : {}),
}
return [m.id.trim(), spec]
}),
)

const seenHeaders = new Set<string>()
const headers = input.form.headers.map((h) => {
Expand Down Expand Up @@ -249,9 +290,15 @@ export function validateCustomProvider(input: ValidateArgs) {
...(key ? { apiKey: key } : {}),
...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}),
},
// Discovery mode: persist the opt-in flag and an empty model list (backend refreshes at runtime).
// Edit-of-discovery: keep discovery on (runtime refresh) AND persist the spec overrides — the
// build loop merges these over the discovered models (manual wins per-id).
// New discovery mode: persist the opt-in flag and an empty model list (backend refreshes).
// Manual mode: freeze the listed models and leave discovery off.
...(discoveryMode ? { discovery: true, models: {} } : { models: modelConfig }),
...(input.editDiscovery
? { discovery: true, models: modelConfig }
: discoveryMode
? { discovery: true, models: {} }
: { models: modelConfig }),
}

return {
Expand All @@ -262,9 +309,58 @@ export function validateCustomProvider(input: ValidateArgs) {
}
}

// Build the dialog form state for editing an existing custom provider. Fields (URL/key/headers/name)
// come from the raw config entry; model rows are seeded from the RESOLVED provider so the user sees the
// actual context/reasoning/temperature values (a discovery provider has no models in config — its
// specs only exist post-resolve). Each row is pre-filled so edits override just those fields.
export function formStateFromProvider(input: {
config: ProviderConfig
resolved: ResolvedProvider | undefined
}): FormState {
const { config, resolved } = input
const headers = config.options?.headers
const headerRows =
headers && typeof headers === "object" && Object.keys(headers).length
? Object.entries(headers as Record<string, string>).map(([key, value]) =>
headerRow2(String(key), String(value)),
)
: [headerRow()]

const resolvedModels = resolved?.models ?? {}
const modelRows = Object.entries(resolvedModels).map(([id, m]) =>
modelRow({
id,
name: m.name || id,
context: m.limit?.context ? String(m.limit.context) : "",
reasoning: !!m.capabilities?.reasoning,
temperature: !!m.capabilities?.temperature,
}),
)

return {
providerID: resolved?.id ?? config.id ?? "",
name: config.name ?? resolved?.name ?? "",
baseURL: (config.options?.baseURL as string | undefined) ?? "",
apiKey: (config.options?.apiKey as string | undefined) ?? "",
models: modelRows.length ? modelRows : [modelRow()],
headers: headerRows,
err: {},
}
}

let row = 0

const nextRow = () => `row-${row++}`

export const modelRow = (): ModelRow => ({ row: nextRow(), id: "", name: "", err: {} })
export const modelRow = (init?: Partial<ModelRow>): ModelRow => ({
row: nextRow(),
id: "",
name: "",
context: "",
reasoning: false,
temperature: false,
err: {},
...init,
})
export const headerRow = (): HeaderRow => ({ row: nextRow(), key: "", value: "", err: {} })
const headerRow2 = (key: string, value: string): HeaderRow => ({ row: nextRow(), key, value, err: {} })
Loading
Loading