diff --git a/docs/user/guide/providers.i18n.yaml b/docs/user/guide/providers.i18n.yaml new file mode 100644 index 0000000000..04b27adb51 --- /dev/null +++ b/docs/user/guide/providers.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/user/guide/providers.md +providers.md: 2ae1093699d8eb26171a2403db155113d84e437e +providers.zh.md: 6ce513c659140ed18716bd5c8f75c428ad981f2b diff --git a/docs/user/guide/providers.md b/docs/user/guide/providers.md new file mode 100644 index 0000000000..2ae1093699 --- /dev/null +++ b/docs/user/guide/providers.md @@ -0,0 +1,113 @@ +# Configure model providers + +English | [中文](providers.zh.md) + +Harness ships with DeepSeek and mounts a generic multi-provider adapter alongside it, for the providers in pi-ai's installed catalog — Anthropic, OpenAI, and the rest — and for any OpenAI-compatible gateway or self-hosted server. You have two entry points: the **Models** page in the web UI, and `$DSH_HOME/settings.yaml`. Both write the same document, and a change takes effect on the next request without a restart. + +## Where providers come from + +`cordis.yml` decides which **adapters** are installed; the settings document decides which **providers** run. The shipped composition carries two LLM adapters: + +- `llm-deepseek` serves the `deepseek-official` route, the one available out of the box. +- `llm-pi-ai` mounts **dormant**: zero routes and no extra entries in the model picker until an `llm-pi-ai:` settings section supplies provider profiles, at which point those routes register live and drop again when the section empties. + +Adding a provider therefore rarely means editing `cordis.yml` — writing settings is enough, and that is exactly what the Models page does. + +## Configure from the web UI + +Start `pnpm run dsh web` and open **Settings → Models**. + +**Give DeepSeek its key.** The DeepSeek card carries one API-key field; fill it in, save, and the provider is ready. + +**Add a provider from the installed catalog.** Choose **Add provider**, pick one of pi-ai's catalog providers (anthropic, openai, and so on), and enter that provider's API key. The endpoint, protocol, and model catalog all come from the catalog; the key is the only thing you owe. + +**Add a custom provider.** Choose **Add a custom provider** for a route the catalog does not ship — a company gateway, a self-hosted server, or a provider newer than the installed catalog. It asks for a Provider ID (the lowercase identifier that names the route in requests and as its credential), a base URL, a protocol, and at least one model. + +**Let the endpoint report its models.** Expand **Model catalog** and choose **Fetch available models**: the interrogation asks the endpoint **the form currently shows** — including a base URL edited but not yet saved and a key typed but not yet stored — and offers what it reports as candidates to pick from. A route the installed catalog describes is answered from that catalog with no network call. Adopting a candidate only writes rows into the draft; nothing is stored until you save. + +Keys are write-only: the page only ever holds a redacted descriptor, never the literal secret. A key you enter is stored in `$DSH_HOME/.env`, and the profile records only the variable name that references it. + +## settings.yaml for advanced configuration + +The document lives at `$DSH_HOME/settings.yaml` (`$DSH_HOME` defaults to `~/.dsh`). The Models page writes this file, and you can edit it directly; neither source outranks the other. + +```yaml +llm-deepseek: + reasoningEffort: high + +llm-pi-ai: + providers: + # Catalog route: endpoint, protocol, and models come from pi-ai; you supply + # the credential. + openai: + apiKeyEnv: OPENAI_API_KEY + + # Also a catalog route, moved to a private proxy, with its catalog narrowed + # to one model and that model's capacity corrected. Every unset field still + # comes from the catalog. + anthropic: + apiKeyEnv: ANTHROPIC_API_KEY + baseURL: https://proxy.example.com:8443 + reasoning: high + models: + - id: claude-sonnet-4-5 + contextWindow: 200000 + + # Hand-declared route: pi-ai ships nothing under this key, so the profile + # supplies the whole provider. + acme-gateway: + displayName: Acme Gateway + apiKeyEnv: ACME_GATEWAY_API_KEY + api: openai-completions + baseURL: https://gateway.acme.example/v1 + models: + - id: acme-large + name: Acme Large + contextWindow: 65536 + maxTokens: 4096 +``` + +A settings section merges over the matching `cordis.yml` configuration **per provider**, so you can override one field of one route and leave the rest as the composition set them. + +A profile the adapter could not serve is refused **where it is written**: a hand-declared route needs `api`, `baseURL`, and at least one model, and a profile missing any of them fails naming the offending route and model rather than being stored and quietly disabling the whole namespace. When an already-stored document is broken by an external edit, settings keeps the last good value and warns. + +## The model catalog + +A profile's `models` list *replaces* that route's installed catalog rather than extending it; omitting it or leaving it empty serves the catalog unchanged. Each entry defaults its unset fields from the installed model of the same `id`, so narrowing a route to two models, correcting one capacity, or adding a model newer than the installed catalog are each a one-line edit. + +Only the four fields the harness consumes are configurable: `id`, `name`, `contextWindow`, and `maxTokens`. Pricing and input modalities have no consumer, and reasoning is not per-model configurable at all — it rides the installed catalog entry. + +A model neither the entry nor the catalog sizes takes the route's `defaultContextWindow` (262,144) and `defaultMaxTokens` (32,768). Both are guesses by construction, which is why they are route fields: a deployment whose gateway serves smaller models corrects them once. + +Model ids are not lifecycle configuration. Requesting a model the route does not configure fails with `UNKNOWN_MODEL` before any provider request goes out. + +## Credentials + +Prefer `apiKeyEnv`: it is a *reference* resolved per request, so no secret enters the configuration file. A literal `apiKey` is the escape hatch. Omitting both is what leaves a route unauthenticated, which for a catalog route means pi-ai's own environment discovery. A reference that resolves to nothing fails the request with `MISSING_CREDENTIAL` rather than falling through to whatever unrelated key the environment happens to hold. + +References resolve from `$DSH_HOME/.env` — what the Models page's key fields write — and from the matching environment variable when no credential service is mounted. One credential serves every model on its route. + +## Point an agent at the new provider + +A configured route appears in the web model picker and can be switched at any time. To change the default, edit the `agent-loop` entry's `provider` and `model` in `cordis.yml`: + +```yaml +- id: agent-loop + name: '@deepseek-ai/dsh-agent-loop' + config: + agents: + - id: main + provider: acme-gateway + model: acme-large +``` + +## Troubleshooting + +- **`MISSING_CREDENTIAL`** — the variable the profile's `apiKeyEnv` names holds no value. Store the key once through the Models page, or export the variable. +- **`UNKNOWN_MODEL`** — the requested model is not in the route's configured catalog. Add it to `models`, or use an id the catalog already carries. +- **`settings-rejected`** — the written profile cannot be served, and the message names the route and model. For a hand-declared route, check that `api`, `baseURL`, and `models` are all present. +- **Fetching available models answers 401** — the endpoint refused the interrogation. Check the key; if the base URL points at an Anthropic-style gateway, note that the interrogation reads only the OpenAI-compatible `GET /models`, so enter the models by hand instead. + +## Exact field reference + +The complete fields, types, and defaults each plugin currently supports live in the generated [plugin configuration catalog](../../config-catalog.md). Each adapter's own semantics belong to its README: [`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.md) and [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.md). For `cordis.yml` itself, see [Configuration](./config.md). diff --git a/docs/user/guide/providers.zh.md b/docs/user/guide/providers.zh.md new file mode 100644 index 0000000000..6ce513c659 --- /dev/null +++ b/docs/user/guide/providers.zh.md @@ -0,0 +1,113 @@ +# 配置模型提供方 + +[English](providers.md) | 中文 + +Harness 出厂就带 DeepSeek,同时挂着一个通用的多提供方适配器,用来接入 Anthropic、OpenAI 这类内置目录里的提供方,或任何 OpenAI 兼容的网关与自建服务。你有两个入口:Web 界面的**模型**页,以及 `$DSH_HOME/settings.yaml`。两者写的是同一份文档,改完下一次请求即生效,不用重启。 + +## 提供方从哪里来 + +`cordis.yml` 决定装了哪些**适配器**,settings 文档决定跑哪些**提供方**。出厂组合里有两个 LLM 适配器: + +- `llm-deepseek` 提供 `deepseek-official` 路由,是默认可用的那个。 +- `llm-pi-ai` 以**休眠**状态挂载:零路由,模型选择器里也不会多出条目,直到 settings 里的 `llm-pi-ai:` 段落给出 provider profile,路由才注册上来;段落清空则一并撤下。 + +因此新增一个提供方通常不需要改 `cordis.yml`,写 settings 就够了——而模型页做的正是这件事。 + +## 在 Web 界面里配置 + +启动 `pnpm run dsh web`,打开**设置 → 模型**。 + +**填 DeepSeek 的密钥。** DeepSeek 卡片上只有一个 API 密钥输入框,填好保存即可开始用。 + +**添加内置目录里的提供方。** 点**添加提供方**,从 pi-ai 内置目录中选一个(anthropic、openai 等),填入该提供方的 API 密钥。端点、协议和模型目录都由内置目录提供,你只需要给密钥。 + +**添加自定义提供方。** 点**添加自定义提供方**,用于内置目录没有的路由——公司网关、自建服务,或比内置目录更新的提供方。需要填 Provider ID(请求里点名它、也作为凭据名的小写标识)、API 地址、协议,以及至少一个模型。 + +**让端点自己报模型。** 展开**模型目录**后点**获取可用模型**,会按你**当前表单里**的地址与密钥去问端点(地址改了但没保存、密钥刚输入还没存下,都算数),把它报告的模型列成候选让你勾选。内置目录里的路由直接由目录作答,不联网。采纳只是把行写进草稿,最终还是你点保存才落盘。 + +密钥是只写的:页面拿到的永远是脱敏描述符,不是明文。写入的密钥存进 `$DSH_HOME/.env`,profile 里只记录引用它的变量名。 + +## settings.yaml:进阶配置 + +文档位于 `$DSH_HOME/settings.yaml`(`$DSH_HOME` 默认是 `~/.dsh`)。模型页写的就是这个文件,你也可以直接编辑它——两个来源没有主次之分。 + +```yaml +llm-deepseek: + reasoningEffort: high + +llm-pi-ai: + providers: + # Catalog route: endpoint, protocol, and models come from pi-ai; you supply + # the credential. + openai: + apiKeyEnv: OPENAI_API_KEY + + # Also a catalog route, moved to a private proxy, with its catalog narrowed + # to one model and that model's capacity corrected. Every unset field still + # comes from the catalog. + anthropic: + apiKeyEnv: ANTHROPIC_API_KEY + baseURL: https://proxy.example.com:8443 + reasoning: high + models: + - id: claude-sonnet-4-5 + contextWindow: 200000 + + # Hand-declared route: pi-ai ships nothing under this key, so the profile + # supplies the whole provider. + acme-gateway: + displayName: Acme Gateway + apiKeyEnv: ACME_GATEWAY_API_KEY + api: openai-completions + baseURL: https://gateway.acme.example/v1 + models: + - id: acme-large + name: Acme Large + contextWindow: 65536 + maxTokens: 4096 +``` + +settings 段落**逐个提供方**地盖在 `cordis.yml` 的同名配置之上,所以你可以只覆盖某个路由的一个字段,其余保持组合里的样子。 + +一份服务不了的 profile 会在**写入处**被拒绝:手工声明的路由必须给出 `api`、`baseURL` 和至少一个模型,缺了会带着路由名和模型名报错,而不是存下来再让整个命名空间静默失效。已经存好的文档被外部改坏时,settings 会保留上一次的好值并告警。 + +## 模型目录 + +`models` 是**替换**该路由的内置目录,不是往里追加;省略或留空则原样使用内置目录。每个条目会从同 `id` 的内置模型继承自己没写的字段,所以「收窄到两个模型」「更正一个容量」「加一个比内置目录更新的模型」都是一行编辑。 + +可配置的只有 harness 会消费的四个字段:`id`、`name`、`contextWindow`、`maxTokens`。定价与输入模态没有消费方,推理能力也不按模型配置——它随内置目录条目走。 + +两处容量都没给出的模型,取路由级兜底 `defaultContextWindow`(262144)与 `defaultMaxTokens`(32768)。这两个数按定义就是猜测,所以它们是路由字段:网关服务的模型更小时改一次即可。 + +模型 id 不是生命周期配置:请求一个该路由没有配置的模型,会在任何网络请求之前以 `UNKNOWN_MODEL` 失败。 + +## 凭据 + +优先用 `apiKeyEnv`——它是一个**引用**,每次请求时解析,密钥本身不进配置文件;`apiKey` 字面量是应急出口。两者都不给,才表示这个路由不带认证,对内置目录路由意味着交给 pi-ai 自己的环境发现。给了引用却解析不到,请求会以 `MISSING_CREDENTIAL` 失败,而不是退回去用环境里碰巧存在的某个不相干的 key 计费。 + +引用解析自 `$DSH_HOME/.env`(模型页的密钥输入框写的就是它),没有挂载凭据服务时则直接读同名环境变量。一份凭据供该路由上的所有模型使用。 + +## 让 agent 用上新提供方 + +配好的路由会出现在 Web 的模型选择器里,随时可切。要改默认值,就在 `cordis.yml` 里改 `agent-loop` 那条的 `provider` 与 `model`: + +```yaml +- id: agent-loop + name: '@deepseek-ai/dsh-agent-loop' + config: + agents: + - id: main + provider: acme-gateway + model: acme-large +``` + +## 排错 + +- **`MISSING_CREDENTIAL`** — profile 里的 `apiKeyEnv` 指向的变量没有值。用模型页存一次密钥,或导出该环境变量。 +- **`UNKNOWN_MODEL`** — 请求的模型不在该路由配置的目录里。把它加进 `models`,或改用目录里已有的 id。 +- **`settings-rejected`** — 写入的 profile 服务不了,错误信息会点名具体的路由和模型。手工声明的路由检查 `api`、`baseURL`、`models` 是否齐全。 +- **获取可用模型返回 401** — 端点拒绝了这次探测。检查密钥;若地址指向的是 Anthropic 风格网关,注意探测只读 OpenAI 兼容的 `GET /models`,此时手工填写模型即可。 + +## 精确字段参考 + +每个插件当前支持的完整字段、类型与默认值见自动生成的[插件配置目录](../../config-catalog.md)。两个适配器各自的语义由它们的 README 负责:[`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.md) 与 [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.md)。`cordis.yml` 本身的写法见[配置文件](./config.md)。 diff --git a/docs/user/guide/quickstart.i18n.yaml b/docs/user/guide/quickstart.i18n.yaml index 2cf494f71a..74fd06f83d 100644 --- a/docs/user/guide/quickstart.i18n.yaml +++ b/docs/user/guide/quickstart.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/guide/quickstart.md -quickstart.md: 199b3f092159fa6fbaf3ae298151487c924ac6f1 -quickstart.zh.md: 9327ed646ba211bcce6426beb6bf76fca50acbf6 +quickstart.md: 8a9ed716d9395448aadfb97d0935bd42ee06e6c1 +quickstart.zh.md: 3652b0453f870640b278ce6f1355e67e85983ffe diff --git a/docs/user/guide/quickstart.md b/docs/user/guide/quickstart.md index 199b3f0921..8a9ed716d9 100644 --- a/docs/user/guide/quickstart.md +++ b/docs/user/guide/quickstart.md @@ -57,5 +57,6 @@ headless-agent uses the `@deepseek-ai/dsh-cli-demo` app. `dsh web` instead compo ## Next steps +- [Model providers](./providers.md) — reach providers beyond DeepSeek, and custom gateways - [Configuration](./config.md) — understand the `cordis.yml` format - [Develop a plugin](../develop/basic/) — build your own tool or backend diff --git a/docs/user/guide/quickstart.zh.md b/docs/user/guide/quickstart.zh.md index 9327ed646b..3652b0453f 100644 --- a/docs/user/guide/quickstart.zh.md +++ b/docs/user/guide/quickstart.zh.md @@ -57,5 +57,6 @@ headless-agent 使用 `@deepseek-ai/dsh-cli-demo` app。`dsh web` 则组合 [`ap ## 下一步 +- [配置模型提供方](./providers.md) — 接入 DeepSeek 之外的提供方与自定义网关 - [配置文件](./config.md) — 了解 `cordis.yml` 的格式 - [开发插件](../develop/basic/) — 编写自己的 tool 或后端 diff --git a/website/docs.ts b/website/docs.ts index 8d42ae3209..2b9c4654c4 100644 --- a/website/docs.ts +++ b/website/docs.ts @@ -130,13 +130,21 @@ const homeAndGuide = pairedPages([ section: { root: '入门', en: 'Guide' }, order: 2, }, + { + source: 'docs/user/guide/providers.md', + route: 'guide/providers.md', + label: { root: '配置模型提供方', en: 'Model providers' }, + sidebar: { root: 'zh-guide', en: 'en-guide' }, + section: { root: '入门', en: 'Guide' }, + order: 3, + }, { source: 'docs/user/guide/config.md', route: 'guide/config.md', label: { root: '配置文件', en: 'Configuration' }, sidebar: { root: 'zh-guide', en: 'en-guide' }, section: { root: '入门', en: 'Guide' }, - order: 3, + order: 4, }, ])