Skip to content

Commit 05dd475

Browse files
deepagent-aiclaude
andauthored
v4.0.2 (#69)
### Issue for this PR Closes # ### Type of change - [ ] Bug fix - [ ] New feature - [ ] Refactor / code improvement - [ ] Documentation ### What does this PR do? Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** ### How did you verify your code works? ### Screenshots / recordings _If this is a UI change, please include a screenshot or recording._ ### Checklist - [ ] I have tested my changes locally - [ ] I have not included unrelated changes in this PR _If you do not follow this template your PR will be automatically rejected._ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 746e73e commit 05dd475

41 files changed

Lines changed: 1088 additions & 113 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,67 @@ deepagent-code
117117
deepagent
118118
```
119119

120+
## Adding a Provider
121+
122+
Before you can run a task, DeepAgent Code needs at least one model provider. It
123+
supports 75+ providers through the [AI SDK](https://ai-sdk.dev/) and
124+
[models.dev](https://models.dev), plus any OpenAI- or Anthropic-compatible
125+
endpoint. Pick whichever path fits how you work.
126+
127+
### Desktop app (recommended)
128+
129+
Open **Settings → Providers**:
130+
131+
- **Official providers** (OpenAI, Anthropic, DeepSeek, Google, xAI, ZhipuAI/GLM):
132+
click **Connect**, paste your API key.
133+
- **Any other provider or gateway**: click **Connect** on *Custom provider*, paste
134+
the **Base URL** and **API key**. DeepAgent Code auto-detects the protocol
135+
(OpenAI-compatible or Anthropic) and discovers the available models from the
136+
endpoint's `/models` list — you don't have to fill anything else.
137+
138+
Model specs (context window, reasoning) are auto-filled by matching each model
139+
against the models.dev catalog. You can reopen a custom provider to override a
140+
model's context/reasoning/temperature; those overrides are best-effort and not
141+
guaranteed to keep the model working.
142+
143+
### Terminal
144+
145+
```bash
146+
# Log in to a provider (official providers, or a plugin auth flow)
147+
deepagent auth login
148+
149+
# See what's connected
150+
deepagent auth list
151+
```
152+
153+
### Config file
154+
155+
Providers also live in `~/.deepagent/code/config.jsonc`. A custom
156+
OpenAI-compatible endpoint looks like this — set `discovery: true` to have models
157+
refreshed from the endpoint at runtime, or list them explicitly under `models`:
158+
159+
```jsonc
160+
{
161+
"$schema": "https://deepagent-code.ai/config.json",
162+
"provider": {
163+
"myprovider": {
164+
"name": "My Provider",
165+
"npm": "@ai-sdk/openai-compatible",
166+
"discovery": true,
167+
"options": {
168+
"baseURL": "https://api.myprovider.com/v1",
169+
"apiKey": "sk-..."
170+
}
171+
}
172+
}
173+
}
174+
```
175+
176+
Official-provider keys added via the app/CLI are stored separately in
177+
`~/.deepagent/code/auth.json`, not in the config file. See the
178+
[providers guide](https://deepagent-code.ai/docs/providers/) for the full
179+
reference (base URL overrides, headers, per-model config, gateways).
180+
120181
## Quick Example
121182

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

217278
## Documentation
218279

280+
- [Providers & Models](https://deepagent-code.ai/docs/providers/)
219281
- [Architecture & Design](design/README.md)
220282
- [Security Policy](SECURITY.md)
221283
- [Privacy Policy](PRIVACY.md)

README.zh.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,62 @@ deepagent-code
117117
deepagent
118118
```
119119

120+
## 添加供应商
121+
122+
在运行任务之前,DeepAgent Code 至少需要一个模型供应商。它通过
123+
[AI SDK](https://ai-sdk.dev/)[models.dev](https://models.dev) 支持 75+ 家供应商,
124+
以及任意 OpenAI 或 Anthropic 兼容的接口。按你习惯的方式选一种即可。
125+
126+
### 桌面应用(推荐)
127+
128+
打开 **设置 → 供应商(Settings → Providers)**
129+
130+
- **官方供应商**(OpenAI、Anthropic、DeepSeek、Google、xAI、智谱/GLM):点击
131+
**连接**,粘贴你的 API Key。
132+
- **其他供应商或网关**:在「自定义供应商」上点击 **连接**,填入 **Base URL**
133+
**API Key**。DeepAgent Code 会自动探测协议(OpenAI 兼容或 Anthropic),并从接口的
134+
`/models` 列表自动发现可用模型——其余字段无需填写。
135+
136+
模型规格(上下文窗口、推理能力)会通过与 models.dev 目录按模型 id 匹配来自动补全。
137+
你可以再次打开自定义供应商,覆盖某个模型的上下文/推理/温度;这些覆盖为尽力而为的默认值,
138+
修改后不保证模型仍能正常使用。
139+
140+
### 终端
141+
142+
```bash
143+
# 登录供应商(官方供应商,或插件鉴权流程)
144+
deepagent auth login
145+
146+
# 查看已连接的供应商
147+
deepagent auth list
148+
```
149+
150+
### 配置文件
151+
152+
供应商也保存在 `~/.deepagent/code/config.jsonc` 中。一个自定义 OpenAI 兼容接口如下——
153+
`discovery: true` 让模型在运行时从接口刷新,或在 `models` 下显式列出:
154+
155+
```jsonc
156+
{
157+
"$schema": "https://deepagent-code.ai/config.json",
158+
"provider": {
159+
"myprovider": {
160+
"name": "My Provider",
161+
"npm": "@ai-sdk/openai-compatible",
162+
"discovery": true,
163+
"options": {
164+
"baseURL": "https://api.myprovider.com/v1",
165+
"apiKey": "sk-..."
166+
}
167+
}
168+
}
169+
}
170+
```
171+
172+
通过应用/CLI 添加的官方供应商密钥单独存放在 `~/.deepagent/code/auth.json`,不在配置文件里。
173+
完整参考(Base URL 覆盖、请求头、逐模型配置、网关)见
174+
[供应商文档](https://deepagent-code.ai/docs/providers/)
175+
120176
## 快速示例
121177

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

217273
## 文档
218274

275+
- [供应商与模型](https://deepagent-code.ai/docs/providers/)
219276
- [架构与设计](design/README.md)
220277
- [安全策略](SECURITY.md)
221278
- [隐私策略](PRIVACY.md)

packages/app/src/components/dialog-custom-provider-form.ts

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import { isOfficialProvider } from "@deepagent-code/core/provider-official"
2+
import type { Provider as ResolvedProvider, ProviderConfig } from "@deepagent-code/sdk/v2"
23

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

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

10+
// Per-model spec override written under `provider.<id>.models.<id>`. Only the fields the user actually
11+
// set are emitted, so a blank field never clobbers the backend catalog-fill with a zero/false.
12+
export type CustomModelConfig = {
13+
name: string
14+
reasoning?: boolean
15+
temperature?: boolean
16+
limit?: { context: number; output?: number }
17+
}
18+
919
// The config payload written under `provider.<id>`. `discovery` and `models` are mutually exclusive
1020
// in practice (discovery mode emits an empty models map), but both are typed optional so the emitted
1121
// object has one consistent shape instead of a union callers must narrow.
@@ -19,7 +29,7 @@ export type CustomProviderConfig = {
1929
headers?: Record<string, string>
2030
}
2131
discovery?: boolean
22-
models: Record<string, { name: string }>
32+
models: Record<string, CustomModelConfig>
2333
}
2434

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

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

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

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

180204
const disabled = input.disabledProviders.includes(providerID)
205+
// Editing a provider keeps its own id — don't flag that as a collision.
206+
const isSelf = input.editingProviderID === providerID
181207
const existsError = idError
182208
? undefined
183-
: input.existingProviderIDs.has(providerID) && !disabled
209+
: input.existingProviderIDs.has(providerID) && !disabled && !isSelf
184210
? input.t("provider.custom.error.providerID.exists")
185211
: undefined
186212

@@ -201,10 +227,25 @@ export function validateCustomProvider(input: ValidateArgs) {
201227
return undefined
202228
})()
203229
const nameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined
204-
return { id: idError, name: nameError }
230+
const ctx = m.context.trim()
231+
// Blank context is allowed (backend/catalog fills it); a non-empty value must be a positive int.
232+
const contextError = ctx && !/^\d+$/.test(ctx) ? input.t("provider.custom.error.context") : undefined
233+
return { id: idError, name: nameError, context: contextError }
205234
})
206-
const modelsValid = discoveryMode || models.every((m) => !m.id && !m.name)
207-
const modelConfig = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
235+
const modelsValid =
236+
(discoveryMode || models.every((m) => !m.id && !m.name)) && models.every((m) => !m.context)
237+
const modelConfig = Object.fromEntries(
238+
input.form.models.map((m) => {
239+
const ctx = m.context.trim()
240+
const spec: CustomModelConfig = {
241+
name: m.name.trim(),
242+
...(m.reasoning ? { reasoning: true } : {}),
243+
...(m.temperature ? { temperature: true } : {}),
244+
...(ctx ? { limit: { context: Number(ctx) } } : {}),
245+
}
246+
return [m.id.trim(), spec]
247+
}),
248+
)
208249

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

257304
return {
@@ -262,9 +309,58 @@ export function validateCustomProvider(input: ValidateArgs) {
262309
}
263310
}
264311

312+
// Build the dialog form state for editing an existing custom provider. Fields (URL/key/headers/name)
313+
// come from the raw config entry; model rows are seeded from the RESOLVED provider so the user sees the
314+
// actual context/reasoning/temperature values (a discovery provider has no models in config — its
315+
// specs only exist post-resolve). Each row is pre-filled so edits override just those fields.
316+
export function formStateFromProvider(input: {
317+
config: ProviderConfig
318+
resolved: ResolvedProvider | undefined
319+
}): FormState {
320+
const { config, resolved } = input
321+
const headers = config.options?.headers
322+
const headerRows =
323+
headers && typeof headers === "object" && Object.keys(headers).length
324+
? Object.entries(headers as Record<string, string>).map(([key, value]) =>
325+
headerRow2(String(key), String(value)),
326+
)
327+
: [headerRow()]
328+
329+
const resolvedModels = resolved?.models ?? {}
330+
const modelRows = Object.entries(resolvedModels).map(([id, m]) =>
331+
modelRow({
332+
id,
333+
name: m.name || id,
334+
context: m.limit?.context ? String(m.limit.context) : "",
335+
reasoning: !!m.capabilities?.reasoning,
336+
temperature: !!m.capabilities?.temperature,
337+
}),
338+
)
339+
340+
return {
341+
providerID: resolved?.id ?? config.id ?? "",
342+
name: config.name ?? resolved?.name ?? "",
343+
baseURL: (config.options?.baseURL as string | undefined) ?? "",
344+
apiKey: (config.options?.apiKey as string | undefined) ?? "",
345+
models: modelRows.length ? modelRows : [modelRow()],
346+
headers: headerRows,
347+
err: {},
348+
}
349+
}
350+
265351
let row = 0
266352

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

269-
export const modelRow = (): ModelRow => ({ row: nextRow(), id: "", name: "", err: {} })
355+
export const modelRow = (init?: Partial<ModelRow>): ModelRow => ({
356+
row: nextRow(),
357+
id: "",
358+
name: "",
359+
context: "",
360+
reasoning: false,
361+
temperature: false,
362+
err: {},
363+
...init,
364+
})
270365
export const headerRow = (): HeaderRow => ({ row: nextRow(), key: "", value: "", err: {} })
366+
const headerRow2 = (key: string, value: string): HeaderRow => ({ row: nextRow(), key, value, err: {} })

0 commit comments

Comments
 (0)