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
23 changes: 23 additions & 0 deletions apps/desktop/e2e/providers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ test('adds Vercel AI Gateway with its exact identity, endpoint, model id, and sh
await expect(page.getByRole('textbox', { name: '模型密钥' })).toBeVisible();
});

test('adds Ollama Cloud as an independent remote provider with the shared Ollama 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('Ollama Cloud');
const catalogMark = page.locator('.providerCatalogRow[data-provider="ollama-cloud"] .providerLogo svg');
await expect(catalogMark).toBeVisible();
await page.getByRole('button', { name: /添加模型供应商:Ollama Cloud/ }).click();

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

await expect(page.getByRole('heading', { name: 'Ollama Cloud', exact: true }).first()).toBeVisible();
await expect(page.locator('.providerSubpageHeader .providerLogo[data-provider="ollama-cloud"] svg')).toBeVisible();
await expect(page.getByText('qwen3.5:397b', { exact: true }).first()).toBeVisible();
await expect(page.getByRole('textbox', { name: '模型密钥' })).toBeVisible();
});

test('adds Cerebras with its exact snapshot model and API-key credential field', async ({ window: page }) => {
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,11 @@ describe('icon + typography governance contract', () => {
),
'the shared Lobe Icons notice must inventory the vendored Ollama asset',
);
assert.match(marks, /case 'ollama':\s*return <Ollama \/>/);
assert.match(
marks,
/case 'ollama':\s*case 'ollama-cloud':\s*return <Ollama \/>/,
'local Ollama and the independent Ollama Cloud identity must reuse the same upstream monochrome mark',
);
assert.match(catalog, /<ProviderLogo type=\{props\.type\} \/>/);
assert.match(
providersPanel,
Expand Down
37 changes: 37 additions & 0 deletions apps/desktop/src/main/__tests__/model-catalog-choices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,43 @@ describe('model catalog picker helpers', () => {
});
});

it('keeps the remote Ollama Cloud connection distinct from local cloud aliases', async () => {
const { buildCatalogChatModelChoices, pickCatalogDefaultChatModel } = await importModelCatalogChoices();
const remote = connection({
slug: 'ollama-cloud',
providerType: 'ollama-cloud',
defaultModel: 'qwen3.5:397b',
models: [{ id: 'qwen3.5:397b' }, { id: 'gpt-oss:120b' }],
modelSource: 'fetched',
modelsFetchedAt: 1_800_000_000_000,
});
const local = connection({
slug: 'ollama-local',
providerType: 'ollama',
defaultModel: 'qwen3.5:cloud',
models: [{ id: 'qwen3.5:cloud' }],
modelSource: 'fetched',
modelsFetchedAt: 1_800_000_000_000,
});

assert.deepEqual(
buildCatalogChatModelChoices([remote, local]).map(({ connectionSlug, providerType, model }) => ({
connectionSlug,
providerType,
model,
})),
[
{ connectionSlug: 'ollama-cloud', providerType: 'ollama-cloud', model: 'qwen3.5:397b' },
{ connectionSlug: 'ollama-cloud', providerType: 'ollama-cloud', model: 'gpt-oss:120b' },
{ connectionSlug: 'ollama-local', providerType: 'ollama', model: 'qwen3.5:cloud' },
],
);
assert.deepEqual(pickCatalogDefaultChatModel(remote), {
llmConnectionSlug: 'ollama-cloud',
model: 'qwen3.5:397b',
});
});

it('keeps Chat choices on send-wired providers and filters unsupported Codex ChatGPT models', async () => {
const { buildCatalogChatModelChoices } = await importModelCatalogChoices();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ export function ProviderBrandMark({ type }: { type: ProviderType }): ReactElemen
case 'MiniMax-cn':
return <MiniMaxMark />;
case 'ollama':
case 'ollama-cloud':
return <Ollama />;
case 'lm-studio':
return <ProviderAssetMask src={lmStudioBrandMark} />;
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/__tests__/connection-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,32 @@ describe('default session target resolver', () => {
assert.equal(target.model, modelId);
});

test('resolves Ollama Cloud credentials without crossing into local Ollama state', async () => {
const modelId = 'qwen3.5:397b';
const connection = makeConnection({
slug: 'ollama-cloud',
name: 'Ollama Cloud',
providerType: 'ollama-cloud',
defaultModel: modelId,
});

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

assert.equal(target.connection.providerType, 'ollama-cloud');
assert.equal(target.apiKey, 'ollama-cloud-test-key');
assert.equal(target.model, modelId);
});

test('resolves Cloudflare Workers AI credentials without rewriting account scope or model id', async () => {
const modelId = '@cf/moonshotai/kimi-k2.6';
const baseUrl = 'https://api.cloudflare.com/client/v4/accounts/account-123/ai/v1';
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/__tests__/llm-connections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('provider compatibility contract', () => {
'volcengine-ark',
'deepinfra',
'cloudflare-workers-ai',
'ollama-cloud',
'ollama',
'lm-studio',
'localai',
Expand Down Expand Up @@ -102,6 +103,7 @@ describe('provider compatibility contract', () => {
'stepfun-ai-step-plan',
'cloudflare-workers-ai',
'huggingface',
'ollama-cloud',
]);
assert.deepEqual(CATALOG_PROVIDER_TYPES, [
'kimi-coding-plan',
Expand Down Expand Up @@ -139,6 +141,7 @@ describe('provider compatibility contract', () => {
'stepfun-ai-step-plan',
'cloudflare-workers-ai',
'huggingface',
'ollama-cloud',
]);

for (const orderField of ['readyOrder', 'catalogOrder', 'recommendedOrder'] as const) {
Expand Down Expand Up @@ -388,6 +391,32 @@ describe('provider compatibility contract', () => {
assert.ok(!huggingface.fallbackModels.includes('sentence-transformers/all-MiniLM-L6-v2'));
});

it('owns Ollama Cloud direct API separately from the local Ollama daemon', () => {
const providers = PROVIDER_REGISTRY as Partial<Record<string, (typeof PROVIDER_REGISTRY)[keyof typeof PROVIDER_REGISTRY]>>;
const cloud = providers['ollama-cloud'];

assert.ok(cloud, 'Ollama Cloud must be available through the shared provider registry');
assert.equal(cloud.label, 'Ollama Cloud');
assert.equal(cloud.baseUrl, 'https://ollama.com/v1');
assert.equal(cloud.authKind, 'api_key');
assert.equal(cloud.protocol, 'openai');
assert.deepEqual(cloud.runtimeAdapter, {
kind: 'openai-compatible',
name: 'provider',
replayAssistantReasoningAs: 'reasoning',
});
assert.deepEqual(cloud.modelDiscovery, { kind: 'protocol' });
assert.equal(cloud.category, 'overseas');
assert.equal(cloud.catalogGroup, 'api');
assert.equal(cloud.modelsDevId, 'ollama-cloud');
assert.equal(cloud.fallbackModels[0], 'qwen3.5:397b');
assert.ok(!cloud.fallbackModels.includes('cogito-2.1:671b'), 'deprecated snapshot models must not be fallback choices');
assert.notEqual(cloud, providers.ollama);
assert.equal(providers.ollama?.baseUrl, 'http://localhost:11434/v1');
assert.equal(providers.ollama?.authKind, 'none');
assert.deepEqual(providers.ollama?.modelDiscovery, { kind: 'ollama' });
});

it('owns the complete Together AI provider contract under the stable togetherai id', () => {
const together = PROVIDER_REGISTRY.togetherai;

Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/__tests__/model-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,27 @@ describe('ModelCatalogEntry', () => {
assert.ok(!entries.some((entry) => entry.id === 'BAAI/bge-m3'));
});

it('uses the checked-in Ollama Cloud snapshot with exact model ids until discovery succeeds', () => {
const entries = buildConnectionModelCatalogEntries({
connection: {
slug: 'ollama-cloud',
providerType: 'ollama-cloud',
defaultModel: 'qwen3.5:397b',
},
});

assert.equal(entries[0]?.id, 'qwen3.5:397b');
assert.equal(entries[0]?.source, 'static_catalog');
assert.equal(entries[0]?.provenance.modelSource, 'fallback');
assert.equal(entries[0]?.contextWindow, 262_144);
assert.deepEqual(entries[0]?.capabilities, {
vision: true,
reasoning: true,
functionCalling: true,
});
assert.ok(entries.some((entry) => entry.id === 'gpt-oss:120b'));
});

it('uses the checked-in Fireworks snapshot until live discovery succeeds', () => {
const entries = buildConnectionModelCatalogEntries({
connection: {
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/__tests__/model-thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ describe('thinkingOptionsForModel', () => {
);
});

test('Ollama Cloud exposes the documented OpenAI-compatible reasoning effort values', () => {
assert.deepEqual(
thinkingOptionsForModel('ollama-cloud', 'qwen3.5:397b'),
{ efforts: ['none', 'low', 'medium', 'high'], toggle: true },
);
assert.deepEqual(
[...thinkingVariantsForModel('ollama-cloud', 'qwen3.5:397b')],
['off', 'low', 'medium', 'high'],
);
});

test('claude-subscription inherits anthropic thinking options (displayMetadataOnly preserves them)', () => {
assert.deepEqual(thinkingOptionsForModel('claude-subscription', 'claude-opus-4-8'), { efforts: ['low', 'medium', 'high', 'xhigh', 'max'] });
assert.deepEqual(thinkingOptionsForModel('claude-subscription', 'claude-haiku-4-5'), { toggle: true, offBehavior: 'anthropic-thinking-disabled' });
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/__tests__/provider-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ import {
import type { LlmConnection } from '../llm-connections.js';

describe('ProviderAuth contract', () => {
test('Ollama Cloud requires its own API key and exposes remote model discovery', () => {
const missing = deriveProviderAuthContract({
providerType: 'ollama-cloud',
hasSecret: false,
});
const configured = deriveProviderAuthContract({
providerType: 'ollama-cloud',
hasSecret: true,
});

expect(missing.setupMode).toBe('api_key');
expect(missing.requiresSecret).toBe(true);
expect(missing.sendMayUseWithoutSecret).toBe(false);
expect(missing.actionAvailability.fetch_models).toBe('hidden');
expect(configured.actionAvailability.test_credentials).toBe('available');
expect(configured.actionAvailability.fetch_models).toBe('available');
});

test('DeepInfra uses the shared API-key credential and model-discovery flow', () => {
const contract = deriveProviderAuthContract({
providerType: 'deepinfra',
Expand Down
Loading
Loading