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
30 changes: 30 additions & 0 deletions apps/desktop/e2e/providers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,36 @@ test('adds Mistral with its exact snapshot model, API-key field, and shared offi
await expect(page.getByRole('textbox', { name: '模型密钥' })).toBeVisible();
});

test('adds Tencent TokenHub with its exact snapshot model, API-key field, and shared official mark', async ({ window: page }) => {
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await page.locator('[aria-label="设置分组"]').getByText('模型', { exact: true }).click();
await page.getByRole('button', { name: '添加服务商' }).click();

await page.getByRole('tab', { name: 'API', exact: true }).click();
await page.getByPlaceholder('搜索服务商').fill('Tencent');
const catalogMark = page.locator(
'.providerCatalogRow[data-provider="tencent-tokenhub"] .providerLogo .providerAssetMask',
);
await expect(catalogMark).toBeVisible();
expect(await catalogMark.evaluate(maskRenderContract)).toEqual({ usesAssetMask: true, followsForeground: true });
await page.getByRole('button', { name: /添加模型供应商:Tencent TokenHub/ }).click();

await expect(page.getByLabel('模型供应商连接标识')).toHaveValue('tencent-tokenhub');
await expect(page.getByLabel('模型供应商服务地址')).toHaveValue('https://tokenhub.tencentmaas.com/v1');
await expect(page.getByLabel('模型供应商默认模型')).toHaveValue('hy3');
await page.getByRole('button', { name: '保存供应商' }).click();

await expect(page.getByRole('heading', { name: 'Tencent TokenHub', exact: true }).first()).toBeVisible();
const detailMark = page.locator(
'.providerSubpageHeader .providerLogo[data-provider="tencent-tokenhub"] .providerAssetMask',
);
await expect(detailMark).toBeVisible();
expect(await detailMark.evaluate(maskRenderContract)).toEqual({ usesAssetMask: true, followsForeground: true });
await expect(page.getByText('hy3', { exact: true }).first()).toBeVisible();
await expect(page.getByRole('textbox', { name: '模型密钥' })).toBeVisible();
});

test('restores keyboard focus across provider child pages', async ({ window: page }) => {
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
Expand Down
26 changes: 26 additions & 0 deletions apps/desktop/src/main/__tests__/icon-governance-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const FIREWORKS_BRAND_MARK_FILE = resolve(
'apps/desktop/src/renderer/assets/provider-brands/fireworks.svg',
);
const NVIDIA_BRAND_MARK_FILE = resolve(REPO_ROOT, 'apps/desktop/src/renderer/assets/provider-brands/nvidia.svg');
const TENCENT_HUNYUAN_BRAND_MARK_FILE = resolve(REPO_ROOT, 'apps/desktop/src/renderer/assets/provider-brands/hunyuan.svg');
const DESKTOP_PACKAGE_FILE = resolve(REPO_ROOT, 'apps/desktop/package.json');
const THIRD_PARTY_NOTICES_FILE = resolve(REPO_ROOT, 'apps/desktop/src/renderer/public/THIRD_PARTY_LICENSES.txt');
const ONBOARDING_HERO_FILE = resolve(REPO_ROOT, 'apps/desktop/src/renderer/OnboardingHero.tsx');
Expand Down Expand Up @@ -377,6 +378,31 @@ describe('icon + typography governance contract', () => {
assert.doesNotMatch(marks, /NvidiaMask|nvidiaAssetMask/);
});

it('vendors and routes the byte-exact upstream Hunyuan mark through the shared asset-mask seam', async () => {
const [hunyuanMark, componentSrc, notices] = await Promise.all([
readFile(TENCENT_HUNYUAN_BRAND_MARK_FILE),
readFile(PROVIDER_BRAND_MARKS_FILE, 'utf8'),
readFile(THIRD_PARTY_NOTICES_FILE, 'utf8'),
]);

assert.equal(
createHash('sha256').update(hunyuanMark).digest('hex'),
'7306a65eb71c4de61e21a637e5c4fef94afde823678e225c46f891cc783f6531',
'the vendored Hunyuan mark must remain byte-identical to @lobehub/icons-static-svg@1.91.0 hunyuan.svg',
);
assert.match(
notices,
/apps\/desktop\/src\/renderer\/assets\/provider-brands\/hunyuan\.svg[\s\S]*e4302041fbb3039608d25f9f618bd462783b875e[\s\S]*packages\/static-svg\/icons\/hunyuan\.svg[\s\S]*7306a65eb71c4de61e21a637e5c4fef94afde823678e225c46f891cc783f6531/,
'Tencent must append byte-exact Hunyuan provenance to the existing Lobe Icons notice entry',
);
assert.match(componentSrc, /import hunyuanBrandMark from '\.\.\/assets\/provider-brands\/hunyuan\.svg';/);
assert.match(
componentSrc,
/Hunyuan mark vendored byte-for-byte from Lobe Icons:[\s\S]*@lobehub\/icons-static-svg@1\.91\.0[\s\S]*e4302041fbb3039608d25f9f618bd462783b875e[\s\S]*packages\/static-svg\/icons\/hunyuan\.svg[\s\S]*MIT[\s\S]*7306a65eb71c4de61e21a637e5c4fef94afde823678e225c46f891cc783f6531/,
);
assert.match(componentSrc, /case 'tencent-tokenhub':\s*return <ProviderAssetMask src=\{hunyuanBrandMark\} \/>/);
});

it('ships the Lobe Icons MIT notice beside vendored provider assets', async () => {
const [desktopPackage, notices] = await Promise.all([
readFile(DESKTOP_PACKAGE_FILE, 'utf8'),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/desktop/src/renderer/public/THIRD_PARTY_LICENSES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ This file records notices for third-party source assets vendored into the Maka d
- Upstream commit: `e4302041fbb3039608d25f9f618bd462783b875e`
- Upstream path: `packages/static-svg/icons/nvidia.svg`
- SHA-256: `5a419b99e0ffdbfbe8caa7ec25581054eae03024da59cb860c54ea55ac8e7e73`
- `apps/desktop/src/renderer/assets/provider-brands/hunyuan.svg`
- Upstream commit: `e4302041fbb3039608d25f9f618bd462783b875e`
- Upstream path: `packages/static-svg/icons/hunyuan.svg`
- SHA-256: `7306a65eb71c4de61e21a637e5c4fef94afde823678e225c46f891cc783f6531`

MIT License

Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/renderer/settings/provider-brand-marks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { ProviderType } from '@maka/core';
import type { ReactElement } from 'react';
import cerebrasMarkUrl from '../assets/provider-brands/cerebras.svg';
import fireworksMarkUrl from '../assets/provider-brands/fireworks.svg';
import hunyuanBrandMark from '../assets/provider-brands/hunyuan.svg';
import lmStudioBrandMark from '../assets/provider-brands/lmstudio.svg';
import minimaxBrandMark from '../assets/provider-brands/minimax-logo-only-vertical-color-bg-white-text.svg';
import mistralBrandMark from '../assets/provider-brands/mistral.svg';
Expand Down Expand Up @@ -65,6 +66,14 @@ function XAI(): ReactElement {
// - license: MIT (repository LICENSE)
// - SHA-256: a06cfa54e7deff7f7544175b006b7f8a03fbc5624c44f7d553a44d07ea96e629

// Hunyuan mark vendored byte-for-byte from Lobe Icons:
// - repository: https://github.com/lobehub/lobe-icons
// - package/version: @lobehub/icons-static-svg@1.91.0
// - commit: e4302041fbb3039608d25f9f618bd462783b875e
// - path: packages/static-svg/icons/hunyuan.svg
// - license: MIT (repository LICENSE)
// - SHA-256: 7306a65eb71c4de61e21a637e5c4fef94afde823678e225c46f891cc783f6531

function ProviderAssetMask({ src }: { src: string }): ReactElement {
const mask = `url("${src}")`;
return <span className="providerAssetMask" style={{ maskImage: mask, WebkitMaskImage: mask }} aria-hidden="true" />;
Expand Down Expand Up @@ -244,6 +253,8 @@ export function ProviderBrandMark({ type }: { type: ProviderType }): ReactElemen
return <ProviderAssetMask src={lmStudioBrandMark} />;
case 'mistral':
return <ProviderAssetMask src={mistralBrandMark} />;
case 'tencent-tokenhub':
return <ProviderAssetMask src={hunyuanBrandMark} />;
default:
return <GenericProviderMark />;
}
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/src/__tests__/connection-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@ describe('default session target resolver', () => {
assert.equal(target.model, 'accounts/fireworks/models/kimi-k2p6');
});

test('resolves Tencent TokenHub credentials without rewriting the selected model id', async () => {
const connection = makeConnection({
slug: 'tencent-tokenhub',
name: 'Tencent TokenHub',
providerType: 'tencent-tokenhub',
defaultModel: 'hy3-preview',
});

const target = await resolveDefaultSessionTarget({
connectionStore: {
getDefault: async () => 'tencent-tokenhub',
get: async (slug) => slug === 'tencent-tokenhub' ? connection : null,
},
credentialStore: {
getSecret: async (_slug, kind) => kind === 'api_key' ? 'tencent-tokenhub-test-key' : null,
},
});

assert.equal(target.connection.providerType, 'tencent-tokenhub');
assert.equal(target.apiKey, 'tencent-tokenhub-test-key');
assert.equal(target.model, 'hy3-preview');
});

test('resolves LM Studio without reading a credential or rewriting the selected model id', async () => {
const connection = makeConnection({
slug: 'lm-studio',
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/__tests__/llm-connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('provider compatibility contract', () => {
'togetherai',
'fireworks-ai',
'nvidia',
'tencent-tokenhub',
'ollama',
'lm-studio',
'openai-compatible',
Expand Down Expand Up @@ -72,6 +73,7 @@ describe('provider compatibility contract', () => {
'togetherai',
'fireworks-ai',
'nvidia',
'tencent-tokenhub',
]);
assert.deepEqual(CATALOG_PROVIDER_TYPES, [
'kimi-coding-plan',
Expand All @@ -94,6 +96,7 @@ describe('provider compatibility contract', () => {
'openai-compatible',
'fireworks-ai',
'nvidia',
'tencent-tokenhub',
]);

for (const orderField of ['readyOrder', 'catalogOrder', 'recommendedOrder'] as const) {
Expand Down Expand Up @@ -275,6 +278,23 @@ describe('provider compatibility contract', () => {
assert.equal(provider.signupUrl, 'https://build.nvidia.com/');
assert.equal(provider.modelsDevId, 'nvidia');
});

it('owns Tencent direct API behavior under the stable tencent-tokenhub id', () => {
const tencent = (PROVIDER_REGISTRY as Partial<Record<string, (typeof PROVIDER_REGISTRY)[keyof typeof PROVIDER_REGISTRY]>>)['tencent-tokenhub'];

assert.ok(tencent, 'Tencent TokenHub must be available through the shared provider registry');
assert.equal(tencent.label, 'Tencent TokenHub');
assert.equal(tencent.baseUrl, 'https://tokenhub.tencentmaas.com/v1');
assert.equal(tencent.authKind, 'api_key');
assert.equal(tencent.protocol, 'openai');
assert.deepEqual(tencent.runtimeAdapter, { kind: 'openai-compatible', name: 'provider' });
assert.deepEqual(tencent.modelDiscovery, { kind: 'protocol' });
assert.equal(tencent.category, 'domestic');
assert.equal(tencent.catalogGroup, 'api');
assert.equal(tencent.signupUrl, 'https://cloud.tencent.com/document/product/1823/130090');
assert.equal(tencent.modelsDevId, 'tencent-tokenhub');
assert.deepEqual(tencent.fallbackModels, ['hy3', 'hy3-preview']);
});
});

describe('validateConnectionBaseUrl (PR-UI-IPC-1, @kenji msg 35260e29)', () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/__tests__/model-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ import { isConnectionReady } from '../connection-readiness.js';
import type { LlmConnection, ModelInfo, ProviderType } from '../llm-connections.js';

describe('ModelCatalogEntry', () => {
it('uses the checked-in Tencent TokenHub snapshot until account discovery succeeds', () => {
const entries = buildConnectionModelCatalogEntries({
connection: {
slug: 'tencent-tokenhub',
providerType: 'tencent-tokenhub',
defaultModel: 'hy3',
},
});

assert.deepEqual(entries.map((entry) => entry.id), ['hy3', 'hy3-preview']);
assert.equal(entries[0]?.source, 'static_catalog');
assert.equal(entries[0]?.provenance.modelSource, 'fallback');
assert.equal(entries[0]?.contextWindow, 256_000);
assert.equal(entries[0]?.maxOutputTokens, 64_000);
assert.deepEqual(entries[0]?.capabilities, {
reasoning: true,
functionCalling: true,
});
});

it('uses the checked-in Mistral snapshot until account discovery succeeds', () => {
const entries = buildConnectionModelCatalogEntries({
connection: {
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/model-metadata.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Do not edit by hand; put access-path-specific facts in model-metadata.ts.
import type { ModelMetadata } from './model-metadata.js';

export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "cerebras" | "deepseek" | "fireworks-ai" | "google" | "gemini-cli" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "openai" | "siliconflow" | "togetherai" | "xai" | "zai-coding-plan", Record<string, ModelMetadata>> = {
export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "cerebras" | "deepseek" | "fireworks-ai" | "google" | "gemini-cli" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "openai" | "siliconflow" | "togetherai" | "tencent-tokenhub" | "xai" | "zai-coding-plan", Record<string, ModelMetadata>> = {
"anthropic": {
"claude-fable-5": {"displayName":"Claude Fable 5","lifecycle":"active","docsUrl":"https://docs.anthropic.com/en/docs/about-claude/models","contextWindow":1000000,"maxOutputTokens":128000,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
"claude-haiku-4-5": {"displayName":"Claude Haiku 4.5 (latest)","lifecycle":"active","docsUrl":"https://docs.anthropic.com/en/docs/about-claude/models","contextWindow":200000,"maxOutputTokens":64000,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
Expand Down Expand Up @@ -388,6 +388,10 @@ export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "cerebras" | "d
"zai-org/GLM-5.1": {"displayName":"GLM-5.1","lifecycle":"active","docsUrl":"https://docs.together.ai/docs/serverless-models","contextWindow":202752,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
"zai-org/GLM-5.2": {"displayName":"GLM-5.2","lifecycle":"active","docsUrl":"https://docs.together.ai/docs/serverless-models","contextWindow":262144,"maxOutputTokens":164000,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
},
"tencent-tokenhub": {
"hy3": {"displayName":"Hy3","lifecycle":"active","docsUrl":"https://cloud.tencent.com/document/product/1823/130050","contextWindow":256000,"maxOutputTokens":64000,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
"hy3-preview": {"displayName":"Hy3 preview","lifecycle":"active","docsUrl":"https://cloud.tencent.com/document/product/1823/130050","contextWindow":256000,"maxOutputTokens":64000,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
},
"xai": {
"grok-4.20-0309-non-reasoning": {"displayName":"Grok 4.20 (Non-Reasoning)","lifecycle":"active","docsUrl":"https://docs.x.ai/docs/models","contextWindow":1000000,"maxOutputTokens":30000,"capabilities":{"vision":true,"reasoning":false,"functionCalling":true}},
"grok-4.20-0309-reasoning": {"displayName":"Grok 4.20 (Reasoning)","lifecycle":"active","docsUrl":"https://docs.x.ai/docs/models","contextWindow":1000000,"maxOutputTokens":30000,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
Expand All @@ -409,7 +413,7 @@ export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "cerebras" | "d
},
};

export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<"anthropic" | "cerebras" | "deepseek" | "fireworks-ai" | "google" | "gemini-cli" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "openai" | "siliconflow" | "togetherai" | "xai" | "zai-coding-plan", { id: string; name: string; api?: string; doc: string }> = {
export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<"anthropic" | "cerebras" | "deepseek" | "fireworks-ai" | "google" | "gemini-cli" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "openai" | "siliconflow" | "togetherai" | "tencent-tokenhub" | "xai" | "zai-coding-plan", { id: string; name: string; api?: string; doc: string }> = {
"anthropic": {"id":"anthropic","name":"Anthropic","doc":"https://docs.anthropic.com/en/docs/about-claude/models"},
"cerebras": {"id":"cerebras","name":"Cerebras","doc":"https://inference-docs.cerebras.ai/models/overview"},
"deepseek": {"id":"deepseek","name":"DeepSeek","api":"https://api.deepseek.com","doc":"https://api-docs.deepseek.com/quick_start/pricing"},
Expand All @@ -424,6 +428,7 @@ export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<"anthropic" | "cerebras
"openai": {"id":"openai","name":"OpenAI","doc":"https://platform.openai.com/docs/models"},
"siliconflow": {"id":"siliconflow","name":"SiliconFlow","api":"https://api.siliconflow.com/v1","doc":"https://cloud.siliconflow.com/models"},
"togetherai": {"id":"togetherai","name":"Together AI","doc":"https://docs.together.ai/docs/serverless-models"},
"tencent-tokenhub": {"id":"tencent-tokenhub","name":"Tencent TokenHub","api":"https://tokenhub.tencentmaas.com/v1","doc":"https://cloud.tencent.com/document/product/1823/130050"},
"xai": {"id":"xai","name":"xAI","doc":"https://docs.x.ai/docs/models"},
"zai-coding-plan": {"id":"zai-coding-plan","name":"Z.AI Coding Plan","api":"https://api.z.ai/api/coding/paas/v4","doc":"https://docs.z.ai/devpack/overview"},
};
29 changes: 29 additions & 0 deletions packages/core/src/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ const fireworksModelIds = toolCallingModelIds(
GENERATED_MODELS_DEV_METADATA['fireworks-ai'],
['accounts/fireworks/models/kimi-k2p6'],
);
const tencentTokenHub = GENERATED_MODELS_DEV_PROVIDER_FACTS['tencent-tokenhub'];
if (tencentTokenHub.id !== 'tencent-tokenhub') {
throw new Error('models.dev Tencent TokenHub provider facts are missing stable id tencent-tokenhub');
}
if (!tencentTokenHub.api) throw new Error('models.dev Tencent TokenHub provider facts are missing api');
const tencentTokenHubModelIds = toolCallingModelIds(
'Tencent TokenHub',
GENERATED_MODELS_DEV_METADATA['tencent-tokenhub'],
['hy3', 'hy3-preview'],
);

const together = GENERATED_MODELS_DEV_PROVIDER_FACTS.togetherai;
if (together.id !== 'togetherai') {
Expand Down Expand Up @@ -456,6 +466,25 @@ const providerRegistry = {
readyOrder: 20,
catalogOrder: 20,
},
'tencent-tokenhub': {
label: tencentTokenHub.name,
description: 'Tencent TokenHub models for reasoning and tool-use agents.',
baseUrl: tencentTokenHub.api,
authKind: 'api_key',
backendKind: 'ai-sdk',
fallbackModels: tencentTokenHubModelIds,
status: 'ready',
protocol: 'openai',
runtimeAdapter: { kind: 'openai-compatible', name: 'provider' },
modelDiscovery: { kind: 'protocol' },
category: 'domestic',
catalogGroup: 'api',
catalogBadge: 'API',
signupUrl: 'https://cloud.tencent.com/document/product/1823/130090',
modelsDevId: tencentTokenHub.id,
readyOrder: 21,
catalogOrder: 21,
},
ollama: {
label: 'Ollama',
description: 'Local models from Ollama on localhost.',
Expand Down
Loading
Loading