Skip to content

feat: add DeepSeek and Moonshot as built-in Providers - #160

Merged
yujiezhang-ops merged 2 commits into
mainfrom
feat/more-builtin-providers
Aug 12, 2026
Merged

feat: add DeepSeek and Moonshot as built-in Providers#160
yujiezhang-ops merged 2 commits into
mainfrom
feat/more-builtin-providers

Conversation

@yujiezhang-ops

@yujiezhang-ops yujiezhang-ops commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Addresses part of #158.

What was measured, and how

DeepSeek — verified with a real key through provider.Client. Not curl: the probe went through OneAgent's own client so routing and header handling are exercised the way a user's connection test would be.

openai     ok=true reachable=true status=200  OpenAI Chat Completions connection test passed.
responses  ok=true reachable=true status=200  OpenAI Responses connection test passed.
anthropic  ok=true reachable=true status=200  Anthropic Messages connection test passed.

All three protocols work. Anthropic Messages is served at /anthropic/v1/messages (plain /v1/messages 404s), so the entry carries anthropic_base_url = https://api.deepseek.com/anthropic. Real model IDs read from /v1/models: deepseek-v4-pro and deepseek-v4-flash.

Moonshot — entered without a key, route existence only. Unauthenticated probing needs a control, because a 401 can mean either "exists but needs auth" or "this host authenticates before routing". Against a deliberately bogus path:

/v1/nonexistent-xyz  -> 404   (control: this host routes before authenticating)
/v1/chat/completions -> 401   (route exists)
/v1/responses        -> 401   (route exists)
/v1/messages         -> 404   (route absent)

So Anthropic Messages is genuinely absent, and the entry gets no anthropic_base_url. Its protocols block says route-present-unverified for the two OpenAI-shaped protocols — route existence is not proof the protocol works, and I could not verify further without a Moonshot key.

Worth noting the same control on DeepSeek returns 401 for a bogus path, so unauthenticated probing tells you nothing there. That is why the real key mattered.

A correction to the request

The task said to enter both as chat-only. DeepSeek measurably serves Responses and Anthropic Messages, so recording it as chat-only would have made Codex and Claude Code unusable against a Provider that supports them. It is recorded as it actually behaves.

Behaviour when a protocol is missing

Omitting anthropic_base_url is not silent. Entry.BaseFor falls back to BaseURL, so a Claude Code probe against Moonshot hits https://api.moonshot.cn/v1/messages, gets 404, and unsupportedProtocol maps that to PROTOCOL_UNSUPPORTED. The user is told the protocol is unsupported rather than getting a config that cannot work.

Test guard fixed (please review this bit)

Three status assertions matched the bare substring api_key against the serialized payload. DeepSeek's key page is literally https://platform.deepseek.com/api_keys, so the guard now trips on a public URL the payload is supposed to carry — nothing to do with a leaked credential.

They now match the quoted JSON key. I verified the tightened guard still catches real leaks rather than just weakening the check:

input caught expected
{"api_key":"sk-secret"} yes yes
{"fallbackModel":"m"} yes yes
{"fallback_probe_model":"m"} yes yes
key_management_url ending /api_keys no no
key_management_url ending /api-keys no no

New regression test

TestAnthropicBaseURLIsDistinctFromTheOpenAIBase asserts that no Provider declares anthropic_base_url equal to its base_url. That pairing would be a no-op that reads as Anthropic support. Driven off the manifest, so any future Provider is covered on landing.

The frozen fixture status-empty-linux-arm64.json gains the two entries; that file is a deliberate tripwire, so the diff is the point.

Still open, from #158

  • Moonshot's default_model (kimi-k2-0905-preview) and fallback_probe_model (moonshot-v1-8k) are from public docs, not verified against a live account. Both fields are required and shape the first-run experience, so they should be confirmed with a key before release.
  • order is 3 and 4, after the two sponsors. [Feature]: 增加更多内置 Provider(DeepSeek 等),并明确准入标准 #158 raises whether that ordering needs a stated basis now that non-sponsors are in the list.
  • No relationship/disclosure for either (both empty, none) — correct as far as I know, but worth confirming there is no commercial arrangement.

Verification

go build ./..., go vet ./..., go test ./... — 14 packages pass.

Note: this branch was developed alongside another session working on a kimi-code agent in the same tree. Only these four files are mine; agents.lock.json, internal/catalog/public.go, internal/config/write.go and the frontend changes belong to that work and are deliberately not included.

🤖 Generated with Claude Code


Follow-up: a hardcoded Provider list had to be fixed too

The first push failed the Wails E2E suite, and it was my change, not a flake — the two specs pass on clean main. Root cause:

byProviderCreatedAt (frontend/src/state/ranking.ts) decided which Providers were built-in by testing membership of Set(["ppio", "novita"]). Adding two entries to providers.lock.json therefore classified them as user-defined, sorting them ahead of PPIO.

That broke providerForProtocol in the Profile editor (ProfilesPage.tsx:106), which picks the first Provider serving a protocol. It became DeepSeek — which has no saved key — so no model list loaded and both specs timed out waiting for 找到 N 个模型.

The manifest change was correct; the hardcoded list was the stale half. provider.Store.Public already reports this as custom: !builtIn (omitted for built-ins), so the sort now derives it from the DTO instead of maintaining a parallel list that must be updated in lockstep.

The new unit test asserts the rule without naming any Provider id, so the next Provider added cannot reintroduce it.

This is worth noting beyond this PR: it is the same class of problem as the api_key substring guard above. Both were places where knowledge the backend owns had been copied into a check that then went stale. Adding a Provider is not purely a data change while such copies exist — a grep for other hardcoded Provider ids may be worth doing separately.

Verified locally after the fix: all 6 Wails E2E specs pass, 340 frontend unit tests pass, tsc --noEmit clean, 14 Go packages pass.

DeepSeek was probed with a real key through provider.Client, not curl, so the
result reflects what a user's connection test would report. All three protocols
pass: Chat Completions and Responses on https://api.deepseek.com, and Anthropic
Messages on https://api.deepseek.com/anthropic. Its protocols block therefore
says implementation-supported for all three, and it carries anthropic_base_url.

Moonshot is entered without a key. Its routes were probed unauthenticated
against a known-bogus path as a control: the control 404s, so 401 on
/v1/chat/completions and /v1/responses means those routes exist, while 404 on
/v1/messages means Anthropic Messages does not. It therefore gets no
anthropic_base_url, which makes a Claude Code probe fall back to base_url, hit
/v1/messages, and return PROTOCOL_UNSUPPORTED -- the honest answer rather than a
config that cannot work. Its protocols block says route-present-unverified for
the two OpenAI-shaped protocols, because route existence is not proof the
protocol works.

Note the request said to enter both as chat-only. DeepSeek measurably serves
Responses and Anthropic Messages, so recording it as chat-only would have made
Codex and Claude Code unusable against a Provider that supports them.

Three status assertions matched the bare word "api_key" against the serialized
payload. DeepSeek's key page really is platform.deepseek.com/api_keys, so that
check now fails on a public URL the payload is supposed to carry. They match the
quoted JSON key instead; verified that a real api_key field, a fallbackModel
field, and fallback_probe_model are all still caught.
@yujiezhang-ops
yujiezhang-ops requested a review from a team August 12, 2026 02:31
… list

byProviderCreatedAt sorted user-defined Providers ahead of built-in ones by
testing membership of Set(["ppio", "novita"]). Adding deepseek and moonshot to
providers.lock.json therefore classified them as user-defined, so they sorted
ahead of PPIO and became the first entry matching a protocol.

That broke the Profile editor: providerForProtocol picks the first Provider
serving the requested protocol, which became DeepSeek -- a Provider with no
saved key -- so no model list loaded and the two Wails E2E specs timed out
waiting for it. The manifest change was correct; this list was the stale half.

provider.Store.Public already answers the question as `custom: !builtIn`,
omitted for built-ins, so the sort now reads that instead of maintaining a
parallel list that has to be updated in lockstep. The new test asserts the rule
without naming any id, which is what keeps the next added Provider from
reintroducing this.
@yujiezhang-ops
yujiezhang-ops merged commit 2518cc3 into main Aug 12, 2026
4 checks passed
@yujiezhang-ops
yujiezhang-ops deleted the feat/more-builtin-providers branch August 12, 2026 03:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant