feat: add DeepSeek and Moonshot as built-in Providers - #160
Merged
Conversation
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.
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.All three protocols work. Anthropic Messages is served at
/anthropic/v1/messages(plain/v1/messages404s), so the entry carriesanthropic_base_url = https://api.deepseek.com/anthropic. Real model IDs read from/v1/models:deepseek-v4-proanddeepseek-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:
So Anthropic Messages is genuinely absent, and the entry gets no
anthropic_base_url. Itsprotocolsblock saysroute-present-unverifiedfor 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_urlis not silent.Entry.BaseForfalls back toBaseURL, so a Claude Code probe against Moonshot hitshttps://api.moonshot.cn/v1/messages, gets 404, andunsupportedProtocolmaps that toPROTOCOL_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_keyagainst the serialized payload. DeepSeek's key page is literallyhttps://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:
{"api_key":"sk-secret"}{"fallbackModel":"m"}{"fallback_probe_model":"m"}key_management_urlending/api_keyskey_management_urlending/api-keysNew regression test
TestAnthropicBaseURLIsDistinctFromTheOpenAIBaseasserts that no Provider declaresanthropic_base_urlequal to itsbase_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.jsongains the two entries; that file is a deliberate tripwire, so the diff is the point.Still open, from #158
default_model(kimi-k2-0905-preview) andfallback_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.orderis 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.relationship/disclosurefor 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-codeagent in the same tree. Only these four files are mine;agents.lock.json,internal/catalog/public.go,internal/config/write.goand 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 ofSet(["ppio", "novita"]). Adding two entries toproviders.lock.jsontherefore classified them as user-defined, sorting them ahead of PPIO.That broke
providerForProtocolin 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.Publicalready reports this ascustom: !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_keysubstring 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 --noEmitclean, 14 Go packages pass.