Skip to content
Merged
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 @@ -18,6 +18,7 @@ import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { describe, it } from 'node:test';
import { resolve } from 'node:path';
import { CATALOG_PROVIDER_TYPES } from '@maka/core';
import { readProviderSettingsCombinedSource } from './provider-contract-source-helpers.js';
import { readMainProcessCombinedSource } from './main-process-contract-source-helpers.js';

Expand Down Expand Up @@ -254,17 +255,11 @@ describe('experimental kill-switch (kenji 1da909d5 + 45b31e16)', () => {
});

it('ProvidersPanel keeps OAuth login out of CATALOG_PROVIDER_TYPES and surfaces it as account connections', async () => {
const [src, core] = await Promise.all([
readProviderSettingsCombinedSource(),
readFile(resolve(REPO_ROOT, 'packages', 'core', 'src', 'llm-connections.ts'), 'utf8'),
]);
const catalogMatch = core.match(/export const CATALOG_PROVIDER_TYPES: ProviderType\[] = \[([\s\S]*?)\];/);
assert.ok(catalogMatch, 'CATALOG_PROVIDER_TYPES must exist');
const catalogBody = catalogMatch[1]!;
const src = await readProviderSettingsCombinedSource();
for (const provider of ['claude-subscription', 'codex-subscription', 'gemini-cli']) {
assert.doesNotMatch(
catalogBody,
new RegExp(`'${provider}'`),
assert.equal(
CATALOG_PROVIDER_TYPES.includes(provider as (typeof CATALOG_PROVIDER_TYPES)[number]),
false,
`${provider} must stay out of the visible model provider catalog until its send path is actually open`,
);
}
Expand Down
22 changes: 2 additions & 20 deletions apps/desktop/src/renderer/settings/ProvidersPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ArrowLeft, ChevronRight, Plus, Search } from '@maka/ui/icons';
import {
CATALOG_PROVIDER_TYPES,
PROVIDER_DEFAULTS,
RECOMMENDED_PROVIDER_TYPES,
type LlmConnection,
type ProviderCatalogGroup,
type ProviderType,
Expand Down Expand Up @@ -48,16 +49,6 @@ const CATALOG_TABS: Array<{ id: CatalogCategory; label: string }> = [
{ id: 'local', label: '本地' },
];

const RECOMMENDED_PROVIDER_TYPES: ProviderType[] = [
'siliconflow',
'anthropic',
'openai',
'google',
'kimi-coding-plan',
'deepseek',
'ollama',
];

export function ProvidersPanel({ bridge }: { bridge: ConnectionsBridge }) {
const [connections, setConnections] = useState<LlmConnection[]>([]);
const [defaultSlug, setDefaultSlug] = useState<string | null>(null);
Expand Down Expand Up @@ -186,21 +177,12 @@ export function ProvidersPanel({ bridge }: { bridge: ConnectionsBridge }) {
const configuredByType = (type: ProviderType) =>
connections.filter((connection) => connection.providerType === type).length;

function providerCatalogGroup(type: ProviderType): Exclude<CatalogCategory, 'recommended'> {
const definition = PROVIDER_DEFAULTS[type];
if (definition.catalogGroup && definition.catalogGroup !== 'recommended') return definition.catalogGroup;
if (definition.category === 'local') return 'local';
if (definition.catalogBadge === 'Coding') return 'plans';
if (definition.category === 'custom') return 'aggregators';
return 'api';
}

function providersForCategory(category: CatalogCategory): ProviderType[] {
const source = category === 'recommended' ? RECOMMENDED_PROVIDER_TYPES : CATALOG_PROVIDER_TYPES;
const normalizedQuery = catalogQuery.trim().toLocaleLowerCase();
return source.filter((type) => {
if (!CATALOG_PROVIDER_TYPES.includes(type)) return false;
if (category !== 'recommended' && providerCatalogGroup(type) !== category) return false;
if (category !== 'recommended' && PROVIDER_DEFAULTS[type].catalogGroup !== category) return false;
if (!normalizedQuery) return true;
const display = providerDisplay(type);
return [type, display.name, display.description, PROVIDER_DEFAULTS[type].label]
Expand Down
77 changes: 77 additions & 0 deletions packages/core/src/__tests__/llm-connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,89 @@
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import {
CATALOG_PROVIDER_TYPES,
PROVIDER_DEFAULTS,
PROVIDER_REGISTRY,
READY_PROVIDER_TYPES,
RECOMMENDED_PROVIDER_TYPES,
normalizeConnectionBaseUrl,
persistedBaseUrl,
validateConnectionBaseUrl,
} from '../llm-connections.js';

describe('provider compatibility contract', () => {
it('keeps persisted provider ids and existing provider ordering stable', () => {
assert.deepEqual(Object.keys(PROVIDER_DEFAULTS), [
'anthropic',
'kimi-coding-plan',
'openai',
'google',
'deepseek',
'moonshot',
'zai-coding-plan',
'MiniMax',
'MiniMax-cn',
'siliconflow',
'litellm',
'ollama',
'openai-compatible',
'claude-subscription',
'codex-subscription',
'gemini-cli',
]);
assert.deepEqual(READY_PROVIDER_TYPES, [
'anthropic',
'openai',
'google',
'deepseek',
'moonshot',
'zai-coding-plan',
'MiniMax',
'MiniMax-cn',
'siliconflow',
'ollama',
'kimi-coding-plan',
'openai-compatible',
]);
assert.deepEqual(CATALOG_PROVIDER_TYPES, [
'kimi-coding-plan',
'deepseek',
'moonshot',
'zai-coding-plan',
'MiniMax',
'MiniMax-cn',
'siliconflow',
'anthropic',
'openai',
'google',
'ollama',
'litellm',
'openai-compatible',
]);
});

it('derives catalog, recommendation, runtime, and discovery behavior from one registry', () => {
assert.equal(PROVIDER_DEFAULTS, PROVIDER_REGISTRY, 'the compatibility export must not copy registry state');
assert.deepEqual(RECOMMENDED_PROVIDER_TYPES, [
'siliconflow',
'anthropic',
'openai',
'google',
'kimi-coding-plan',
'deepseek',
'ollama',
]);
assert.equal(PROVIDER_REGISTRY['kimi-coding-plan'].catalogGroup, 'plans');
assert.equal(PROVIDER_REGISTRY.siliconflow.catalogGroup, 'aggregators');
assert.equal(PROVIDER_REGISTRY.ollama.catalogGroup, 'local');
assert.equal(PROVIDER_REGISTRY.siliconflow.runtimeAdapter.kind, 'openai-compatible');
assert.equal(PROVIDER_REGISTRY.siliconflow.modelDiscovery.kind, 'protocol');
assert.deepEqual(PROVIDER_REGISTRY.siliconflow.modelDiscovery.query, { sub_type: 'chat' });
assert.equal(PROVIDER_REGISTRY.ollama.modelDiscovery.kind, 'ollama');
assert.equal(PROVIDER_REGISTRY['codex-subscription'].modelDiscovery.kind, 'fallback');
});
});

describe('validateConnectionBaseUrl (PR-UI-IPC-1, @kenji msg 35260e29)', () => {
describe('accept (returns null)', () => {
it('undefined → null (no override; fall back to provider default)', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,8 +766,10 @@ export type {
} from './llm-connections.js';
export {
CODEX_SUBSCRIPTION_UNSUPPORTED_CHATGPT_MODELS,
PROVIDER_REGISTRY,
PROVIDER_DEFAULTS,
CATALOG_PROVIDER_TYPES,
RECOMMENDED_PROVIDER_TYPES,
READY_PROVIDER_TYPES,
backendKindOf,
effectiveBaseUrl,
Expand Down
Loading
Loading