Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,35 @@ describe('non-subscription account creation', () => {
),
);
});

it.each([
['openai-chat', 'https://api.deepseek.com', 0],
['openai-responses', 'https://api.deepseek.com', 1],
['anthropic', 'https://api.deepseek.com/anthropic', 2],
])('creates a DeepSeek account with its %s endpoint', async (protocol, baseUrl, variantIndex) => {
const onSubmit = vi.fn();
render(
<AddAccountForm
serviceId="deepseek"
runtimes={undefined}
onboarding={onboarding()}
busy={false}
onBack={vi.fn()}
onSubmit={onSubmit}
/>,
);

fireEvent.click(screen.getAllByRole('radio').at(variantIndex)!);
fireEvent.change(screen.getByPlaceholderText('sk-…'), { target: { value: 'sk-deepseek' } });
fireEvent.click(screen.getByRole('button', { name: 'form.submit' }));
await waitFor(() =>
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
service: 'deepseek',
credential: { type: 'api-key', key: 'sk-deepseek' },
endpoint: { baseUrl, protocol },
}),
),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,27 @@ describe('bindingAvailability', () => {
});
});

it('routes each DeepSeek protocol through the matching native agent', () => {
const chat = account({
service: 'deepseek',
endpoint: { baseUrl: 'https://api.deepseek.com', protocol: 'openai-chat' },
});
const responses = account({
service: 'deepseek',
endpoint: { baseUrl: 'https://api.deepseek.com', protocol: 'openai-responses' },
});
const anthropic = account({
service: 'deepseek',
endpoint: { baseUrl: 'https://api.deepseek.com/anthropic', protocol: 'anthropic' },
});
expect(bindingAvailability(chat, 'claude-code')).toEqual({ tier: 'translate' });
expect(bindingAvailability(chat, 'codex')).toEqual({ tier: 'native' });
expect(bindingAvailability(chat, 'opencode')).toEqual({ tier: 'native' });
expect(bindingAvailability(chat, 'pi')).toEqual({ tier: 'native' });
expect(bindingAvailability(responses, 'codex')).toEqual({ tier: 'native' });
expect(bindingAvailability(anthropic, 'claude-code')).toEqual({ tier: 'native' });
});

it('binds Grok Build to xAI catalog accounts', () => {
expect(bindingAvailability(account({ service: 'xai' }), 'grok-build')).toEqual({
tier: 'native',
Expand Down Expand Up @@ -131,7 +152,8 @@ describe('catalog helpers', () => {
it('implies a protocol from the service when the endpoint is absent', () => {
expect(accountProtocol(account({ service: 'anthropic-api' }))).toBe('anthropic');
expect(accountProtocol(account({ service: 'xai' }))).toBe('openai-chat');
// Dual-protocol gateways imply nothing without an endpoint.
// Multi-protocol services imply nothing without an endpoint.
expect(accountProtocol(account({ service: 'deepseek' }))).toBeUndefined();
expect(accountProtocol(account({ service: 'openrouter' }))).toBeUndefined();
expect(accountProtocol(account({}))).toBeUndefined();
// An explicit endpoint always wins.
Expand All @@ -144,4 +166,34 @@ describe('catalog helpers', () => {
),
).toBe('anthropic');
});

it('seeds all official DeepSeek protocol endpoints', () => {
const deepseek = serviceById('deepseek');
expect(deepseek).toMatchObject({
id: 'deepseek',
group: 'direct',
kind: 'endpoint',
});
if (deepseek?.kind !== 'endpoint') throw new Error('deepseek descriptor missing');
expect(deepseek.variants).toEqual([
{
id: 'openai',
protocol: 'openai-chat',
baseUrl: 'https://api.deepseek.com',
credentialType: 'api-key',
},
{
id: 'responses',
protocol: 'openai-responses',
baseUrl: 'https://api.deepseek.com',
credentialType: 'api-key',
},
{
id: 'anthropic',
protocol: 'anthropic',
baseUrl: 'https://api.deepseek.com/anthropic',
credentialType: 'api-key',
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { accountProtocol } from './catalog';
* adapters, packages/host/engine `translator.ts`):
* - claude-code speaks Anthropic natively (`ANTHROPIC_*` env); an `openai-chat` endpoint is
* reachable through the local aigateway translator, which needs a base URL AND a key.
* - codex reaches any OpenAI-shaped endpoint via `OPENAI_BASE_URL` + `CODEX_API_KEY`.
* - codex reaches Responses and compatible Chat endpoints via `OPENAI_BASE_URL` + `CODEX_API_KEY`.
* - opencode and pi: openai-chat is the verified path; an Anthropic endpoint plausibly works but
* is unverified against the SDK — flip it here once verified live.
* - grok-build only consumes an xAI key as `XAI_API_KEY`; it ignores custom endpoint base URLs.
Expand Down
31 changes: 29 additions & 2 deletions packages/client/workbench/src/settings/providers/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import type {
/** Grouping in the add-account catalog grid. */
export type ServiceGroup = 'subscription' | 'direct' | 'gateway' | 'custom';

/** One endpoint shape a service offers; dual-shape gateways expose one variant per protocol. */
/** One endpoint shape a service offers; multi-protocol services expose one variant per protocol. */
export interface ServiceVariant {
id: 'openai' | 'anthropic' | 'default';
id: 'openai' | 'responses' | 'anthropic' | 'default';
protocol: AccountProtocol;
/** Endpoint URL, possibly templated with `{placeholder}` segments. */
baseUrl: string;
Expand Down Expand Up @@ -75,6 +75,33 @@ export const SERVICE_CATALOG: ServiceDescriptor[] = [
],
secretPlaceholder: 'sk-…',
},
{
id: 'deepseek',
label: 'DeepSeek',
group: 'direct',
kind: 'endpoint',
variants: [
{
id: 'openai',
protocol: 'openai-chat',
baseUrl: 'https://api.deepseek.com',
credentialType: 'api-key',
},
{
id: 'responses',
protocol: 'openai-responses',
baseUrl: 'https://api.deepseek.com',
credentialType: 'api-key',
},
{
id: 'anthropic',
protocol: 'anthropic',
baseUrl: 'https://api.deepseek.com/anthropic',
credentialType: 'api-key',
},
],
secretPlaceholder: 'sk-…',
},
{
id: 'xai',
label: 'xAI (Grok)',
Expand Down
2 changes: 2 additions & 0 deletions packages/presentation/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,7 @@ export const en = {
'chatgpt-sub': 'ChatGPT subscription',
'anthropic-api': 'Anthropic API',
'openai-api': 'OpenAI API',
deepseek: 'DeepSeek',
xai: 'xAI (Grok)',
openrouter: 'OpenRouter',
'vercel-gateway': 'Vercel AI Gateway',
Expand All @@ -1070,6 +1071,7 @@ export const en = {
'chatgpt-sub': 'Codex login, Plus / Pro plans',
'anthropic-api': 'A key from console.anthropic.com',
'openai-api': 'A key from platform.openai.com',
deepseek: 'A DeepSeek API key, all three protocol shapes',
xai: 'A key from console.x.ai, OpenAI-compatible',
openrouter: 'Aggregation gateway, both protocol shapes',
'vercel-gateway': 'Server-side translation, both protocol shapes',
Expand Down
2 changes: 2 additions & 0 deletions packages/presentation/i18n/src/locales/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,7 @@ export const zhCN = {
'chatgpt-sub': 'ChatGPT 订阅',
'anthropic-api': 'Anthropic API',
'openai-api': 'OpenAI API',
deepseek: 'DeepSeek',
xai: 'xAI(Grok)',
openrouter: 'OpenRouter',
'vercel-gateway': 'Vercel AI Gateway',
Expand All @@ -1044,6 +1045,7 @@ export const zhCN = {
'chatgpt-sub': 'Codex 登录,Plus / Pro 计划',
'anthropic-api': 'console.anthropic.com 的密钥',
'openai-api': 'platform.openai.com 的密钥',
deepseek: 'DeepSeek API 密钥,三种协议形态',
xai: 'console.x.ai 的密钥,OpenAI 兼容',
openrouter: '聚合网关,双协议形态',
'vercel-gateway': '服务端翻译,双协议形态',
Expand Down