diff --git a/apps/desktop/e2e/providers.spec.ts b/apps/desktop/e2e/providers.spec.ts
index 150c8a5744..110af33cf5 100644
--- a/apps/desktop/e2e/providers.spec.ts
+++ b/apps/desktop/e2e/providers.spec.ts
@@ -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();
diff --git a/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts b/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts
index 335cd65575..de83565ecb 100644
--- a/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts
+++ b/apps/desktop/src/main/__tests__/icon-governance-contract.test.ts
@@ -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 /);
+ assert.match(
+ marks,
+ /case 'ollama':\s*case 'ollama-cloud':\s*return /,
+ 'local Ollama and the independent Ollama Cloud identity must reuse the same upstream monochrome mark',
+ );
assert.match(catalog, //);
assert.match(
providersPanel,
diff --git a/apps/desktop/src/main/__tests__/model-catalog-choices.test.ts b/apps/desktop/src/main/__tests__/model-catalog-choices.test.ts
index 172a7f5fcd..e385417f0a 100644
--- a/apps/desktop/src/main/__tests__/model-catalog-choices.test.ts
+++ b/apps/desktop/src/main/__tests__/model-catalog-choices.test.ts
@@ -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();
diff --git a/apps/desktop/src/renderer/settings/provider-brand-marks.tsx b/apps/desktop/src/renderer/settings/provider-brand-marks.tsx
index 8a7321323d..0e7c7efbfe 100644
--- a/apps/desktop/src/renderer/settings/provider-brand-marks.tsx
+++ b/apps/desktop/src/renderer/settings/provider-brand-marks.tsx
@@ -334,6 +334,7 @@ export function ProviderBrandMark({ type }: { type: ProviderType }): ReactElemen
case 'MiniMax-cn':
return ;
case 'ollama':
+ case 'ollama-cloud':
return ;
case 'lm-studio':
return ;
diff --git a/packages/cli/src/__tests__/connection-target.test.ts b/packages/cli/src/__tests__/connection-target.test.ts
index c3ab605c5c..5dd2333e15 100644
--- a/packages/cli/src/__tests__/connection-target.test.ts
+++ b/packages/cli/src/__tests__/connection-target.test.ts
@@ -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';
diff --git a/packages/core/src/__tests__/llm-connections.test.ts b/packages/core/src/__tests__/llm-connections.test.ts
index 434d9087b9..cec7ddebe8 100644
--- a/packages/core/src/__tests__/llm-connections.test.ts
+++ b/packages/core/src/__tests__/llm-connections.test.ts
@@ -58,6 +58,7 @@ describe('provider compatibility contract', () => {
'volcengine-ark',
'deepinfra',
'cloudflare-workers-ai',
+ 'ollama-cloud',
'ollama',
'lm-studio',
'localai',
@@ -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',
@@ -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) {
@@ -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>;
+ 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;
diff --git a/packages/core/src/__tests__/model-catalog.test.ts b/packages/core/src/__tests__/model-catalog.test.ts
index 1dd94b64eb..ccc21a005c 100644
--- a/packages/core/src/__tests__/model-catalog.test.ts
+++ b/packages/core/src/__tests__/model-catalog.test.ts
@@ -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: {
diff --git a/packages/core/src/__tests__/model-thinking.test.ts b/packages/core/src/__tests__/model-thinking.test.ts
index 0a3815da18..bd464830de 100644
--- a/packages/core/src/__tests__/model-thinking.test.ts
+++ b/packages/core/src/__tests__/model-thinking.test.ts
@@ -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' });
diff --git a/packages/core/src/__tests__/provider-auth.test.ts b/packages/core/src/__tests__/provider-auth.test.ts
index 4e88731996..c41774fd97 100644
--- a/packages/core/src/__tests__/provider-auth.test.ts
+++ b/packages/core/src/__tests__/provider-auth.test.ts
@@ -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',
diff --git a/packages/core/src/model-metadata.generated.ts b/packages/core/src/model-metadata.generated.ts
index bbab5a07cc..6d90f89e5b 100644
--- a/packages/core/src/model-metadata.generated.ts
+++ b/packages/core/src/model-metadata.generated.ts
@@ -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" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "google" | "gemini-cli" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "openai" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "zai-coding-plan", Record> = {
+export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "google" | "gemini-cli" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "ollama-cloud" | "openai" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "zai-coding-plan", Record> = {
"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}},
@@ -380,6 +380,51 @@ export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "cerebras" | "c
"upstage/solar-10_7b-instruct": {"displayName":"solar-10.7b-instruct","lifecycle":"active","docsUrl":"https://docs.api.nvidia.com/nim/","contextWindow":128000,"maxOutputTokens":8192,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}},
"z-ai/glm-5.2": {"displayName":"GLM-5.2","lifecycle":"active","docsUrl":"https://docs.api.nvidia.com/nim/","contextWindow":1000000,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
},
+ "ollama-cloud": {
+ "cogito-2.1:671b": {"displayName":"cogito-2.1:671b","lifecycle":"deprecated","docsUrl":"https://docs.ollama.com/cloud","contextWindow":163840,"maxOutputTokens":32000,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "deepseek-v3.1:671b": {"displayName":"deepseek-v3.1:671b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":163840,"maxOutputTokens":163840,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "deepseek-v3.2": {"displayName":"deepseek-v3.2","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":163840,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "deepseek-v4-flash": {"displayName":"deepseek-v4-flash","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":1048576,"maxOutputTokens":1048576,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "deepseek-v4-pro": {"displayName":"deepseek-v4-pro","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":1048576,"maxOutputTokens":1048576,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "devstral-2:123b": {"displayName":"devstral-2:123b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}},
+ "devstral-small-2:24b": {"displayName":"devstral-small-2:24b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":true,"reasoning":false,"functionCalling":true}},
+ "gemini-3-flash-preview": {"displayName":"gemini-3-flash-preview","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":1048576,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
+ "gemma3:12b": {"displayName":"gemma3:12b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":131072,"maxOutputTokens":131072,"capabilities":{"vision":true,"reasoning":false,"functionCalling":false}},
+ "gemma3:27b": {"displayName":"gemma3:27b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":131072,"maxOutputTokens":131072,"capabilities":{"vision":true,"reasoning":false,"functionCalling":false}},
+ "gemma3:4b": {"displayName":"gemma3:4b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":131072,"maxOutputTokens":131072,"capabilities":{"vision":true,"reasoning":false,"functionCalling":false}},
+ "gemma4:31b": {"displayName":"gemma4:31b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
+ "glm-4.6": {"displayName":"glm-4.6","lifecycle":"deprecated","docsUrl":"https://docs.ollama.com/cloud","contextWindow":202752,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "glm-4.7": {"displayName":"glm-4.7","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":202752,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "glm-5": {"displayName":"glm-5","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":202752,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "glm-5.1": {"displayName":"glm-5.1","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":202752,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "glm-5.2": {"displayName":"GLM-5.2","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":976000,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "gpt-oss:120b": {"displayName":"gpt-oss:120b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":131072,"maxOutputTokens":32768,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "gpt-oss:20b": {"displayName":"gpt-oss:20b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":131072,"maxOutputTokens":32768,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "kimi-k2-thinking": {"displayName":"kimi-k2-thinking","lifecycle":"deprecated","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "kimi-k2:1t": {"displayName":"kimi-k2:1t","lifecycle":"deprecated","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}},
+ "kimi-k2.5": {"displayName":"kimi-k2.5","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
+ "kimi-k2.6": {"displayName":"kimi-k2.6","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
+ "kimi-k2.7-code": {"displayName":"kimi-k2.7-code","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
+ "minimax-m2": {"displayName":"minimax-m2","lifecycle":"deprecated","docsUrl":"https://docs.ollama.com/cloud","contextWindow":204800,"maxOutputTokens":128000,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "minimax-m2.1": {"displayName":"minimax-m2.1","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":204800,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "minimax-m2.5": {"displayName":"minimax-m2.5","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":204800,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "minimax-m2.7": {"displayName":"minimax-m2.7","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":196608,"maxOutputTokens":196608,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "minimax-m3": {"displayName":"minimax-m3","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":512000,"maxOutputTokens":131072,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
+ "ministral-3:14b": {"displayName":"ministral-3:14b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":128000,"capabilities":{"vision":true,"reasoning":false,"functionCalling":true}},
+ "ministral-3:3b": {"displayName":"ministral-3:3b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":128000,"capabilities":{"vision":true,"reasoning":false,"functionCalling":true}},
+ "ministral-3:8b": {"displayName":"ministral-3:8b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":128000,"capabilities":{"vision":true,"reasoning":false,"functionCalling":true}},
+ "mistral-large-3:675b": {"displayName":"mistral-large-3:675b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":262144,"capabilities":{"vision":true,"reasoning":false,"functionCalling":true}},
+ "nemotron-3-nano:30b": {"displayName":"nemotron-3-nano:30b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":1048576,"maxOutputTokens":131072,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "nemotron-3-super": {"displayName":"nemotron-3-super","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "nemotron-3-ultra": {"displayName":"nemotron-3-ultra","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":128000,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "qwen3-coder-next": {"displayName":"qwen3-coder-next","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}},
+ "qwen3-coder:480b": {"displayName":"qwen3-coder:480b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":65536,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}},
+ "qwen3-next:80b": {"displayName":"qwen3-next:80b","lifecycle":"deprecated","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":32768,"capabilities":{"vision":false,"reasoning":true,"functionCalling":true}},
+ "qwen3-vl:235b": {"displayName":"qwen3-vl:235b","lifecycle":"deprecated","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":32768,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
+ "qwen3-vl:235b-instruct": {"displayName":"qwen3-vl:235b-instruct","lifecycle":"deprecated","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":131072,"capabilities":{"vision":true,"reasoning":false,"functionCalling":true}},
+ "qwen3.5:397b": {"displayName":"qwen3.5:397b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":262144,"maxOutputTokens":65536,"capabilities":{"vision":true,"reasoning":true,"functionCalling":true}},
+ "rnj-1:8b": {"displayName":"rnj-1:8b","lifecycle":"active","docsUrl":"https://docs.ollama.com/cloud","contextWindow":32768,"maxOutputTokens":4096,"capabilities":{"vision":false,"reasoning":false,"functionCalling":true}},
+ },
"openai": {
"chatgpt-image-latest": {"displayName":"chatgpt-image-latest","lifecycle":"active","docsUrl":"https://platform.openai.com/docs/models","contextWindow":0,"maxOutputTokens":0,"capabilities":{"vision":true,"reasoning":false,"functionCalling":false}},
"gpt-3.5-turbo": {"displayName":"GPT-3.5-turbo","lifecycle":"active","docsUrl":"https://platform.openai.com/docs/models","contextWindow":16385,"maxOutputTokens":4096,"capabilities":{"vision":false,"reasoning":false,"functionCalling":false}},
@@ -898,7 +943,7 @@ export const GENERATED_MODELS_DEV_METADATA: Record<"anthropic" | "cerebras" | "c
},
};
-export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<"anthropic" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "google" | "gemini-cli" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "openai" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "xai" | "zai-coding-plan", { id: string; name: string; api?: string; doc: string }> = {
+export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<"anthropic" | "cerebras" | "cohere" | "cloudflare-workers-ai" | "deepinfra" | "deepseek" | "fireworks-ai" | "google" | "gemini-cli" | "huggingface" | "MiniMax" | "MiniMax-cn" | "mistral" | "moonshot" | "nvidia" | "ollama-cloud" | "openai" | "siliconflow" | "stepfun" | "stepfun-ai" | "stepfun-ai-step-plan" | "togetherai" | "tencent-coding-plan" | "tencent-token-plan" | "tencent-tokenhub" | "vercel" | "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"},
"cohere": {"id":"cohere","name":"Cohere","doc":"https://docs.cohere.com/docs/models"},
@@ -914,6 +959,7 @@ export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<"anthropic" | "cerebras
"mistral": {"id":"mistral","name":"Mistral","doc":"https://docs.mistral.ai/getting-started/models/"},
"moonshot": {"id":"moonshotai-cn","name":"Moonshot AI (China)","api":"https://api.moonshot.cn/v1","doc":"https://platform.moonshot.cn/docs/api/chat"},
"nvidia": {"id":"nvidia","name":"Nvidia","api":"https://integrate.api.nvidia.com/v1","doc":"https://docs.api.nvidia.com/nim/"},
+ "ollama-cloud": {"id":"ollama-cloud","name":"Ollama Cloud","api":"https://ollama.com/v1","doc":"https://docs.ollama.com/cloud"},
"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"},
"stepfun": {"id":"stepfun","name":"StepFun (China)","api":"https://api.stepfun.com/v1","doc":"https://platform.stepfun.com/docs/zh/overview/concept"},
diff --git a/packages/core/src/model-metadata.ts b/packages/core/src/model-metadata.ts
index 01cc5c8189..880f97ebd6 100644
--- a/packages/core/src/model-metadata.ts
+++ b/packages/core/src/model-metadata.ts
@@ -196,6 +196,11 @@ const STATIC_MODEL_METADATA: Partial model.lifecycle !== 'deprecated'),
+);
+const ollamaCloudModelIds = toolCallingModelIds(
+ 'Ollama Cloud',
+ ollamaCloudActiveMetadata,
+ ['qwen3.5:397b', 'gpt-oss:120b'],
+);
const fireworks = GENERATED_MODELS_DEV_PROVIDER_FACTS['fireworks-ai'];
if (fireworks.id !== 'fireworks-ai') {
throw new Error('models.dev Fireworks AI provider facts are missing stable id fireworks-ai');
@@ -889,6 +903,29 @@ const providerRegistry = {
readyOrder: 33,
catalogOrder: 33,
},
+ 'ollama-cloud': {
+ label: ollamaCloud.name,
+ description: 'Ollama-hosted cloud models over the official remote API.',
+ baseUrl: ollamaCloud.api,
+ authKind: 'api_key',
+ backendKind: 'ai-sdk',
+ fallbackModels: ollamaCloudModelIds,
+ status: 'ready',
+ protocol: 'openai',
+ runtimeAdapter: {
+ kind: 'openai-compatible',
+ name: 'provider',
+ replayAssistantReasoningAs: 'reasoning',
+ },
+ modelDiscovery: { kind: 'protocol' },
+ category: 'overseas',
+ catalogGroup: 'api',
+ catalogBadge: 'API',
+ signupUrl: 'https://ollama.com/settings/keys',
+ modelsDevId: ollamaCloud.id,
+ readyOrder: 35,
+ catalogOrder: 35,
+ },
ollama: {
label: 'Ollama',
description: 'Local models from Ollama on localhost.',
diff --git a/packages/headless/src/__tests__/harbor-cell.test.ts b/packages/headless/src/__tests__/harbor-cell.test.ts
index a172b38f5d..80d1cbb4c4 100644
--- a/packages/headless/src/__tests__/harbor-cell.test.ts
+++ b/packages/headless/src/__tests__/harbor-cell.test.ts
@@ -2276,6 +2276,34 @@ setTimeout(() => {
assert.equal(missing.apiKey, '');
});
+ test('resolves Ollama Cloud only from OLLAMA_API_KEY and preserves the exact model id', () => {
+ const resolved = resolveHarborCellAiSdkEnv({
+ provider: 'ollama-cloud',
+ model: 'qwen3.5:397b',
+ env: {
+ OLLAMA_API_KEY: 'ollama-cloud-key',
+ OPENAI_API_KEY: 'must-not-cross-provider-boundary',
+ },
+ ts: 1,
+ });
+
+ assert.equal(resolved.apiKey, 'ollama-cloud-key');
+ assert.equal(resolved.connection.providerType, 'ollama-cloud');
+ assert.equal(resolved.connection.defaultModel, 'qwen3.5:397b');
+ assert.equal(resolved.connection.baseUrl, 'https://ollama.com/v1');
+
+ const missing = resolveHarborCellAiSdkEnv({
+ provider: 'ollama-cloud',
+ model: 'qwen3.5:397b',
+ env: {
+ OPENAI_API_KEY: 'must-not-cross-provider-boundary',
+ MAKA_CREDENTIALS_PATH: join(tmpdir(), 'maka-headless-ollama-cloud-missing-credentials.json'),
+ },
+ ts: 1,
+ });
+ assert.equal(missing.apiKey, '');
+ });
+
test('resolves xAI only from xAI credential env without rewriting the model id', () => {
const resolved = resolveHarborCellAiSdkEnv({
provider: 'xai',
diff --git a/packages/headless/src/__tests__/provider-env.test.ts b/packages/headless/src/__tests__/provider-env.test.ts
index 979f5b09fc..0e79d1d245 100644
--- a/packages/headless/src/__tests__/provider-env.test.ts
+++ b/packages/headless/src/__tests__/provider-env.test.ts
@@ -75,6 +75,14 @@ test('DeepInfra keeps its official provider-scoped credential environment names'
});
});
+test('Ollama Cloud keeps its official provider-scoped credential environment names', () => {
+ assert.deepEqual(providerCredentialEnv('ollama-cloud'), {
+ apiKeys: ['OLLAMA_API_KEY'],
+ apiKeyFile: 'OLLAMA_API_KEY_FILE',
+ baseUrls: [],
+ });
+});
+
test('Cloudflare Workers AI separates account scope from API token credentials', () => {
assert.deepEqual(providerCredentialEnv('cloudflare-workers-ai'), {
apiKeys: ['CLOUDFLARE_API_KEY'],
diff --git a/packages/headless/src/provider-env.ts b/packages/headless/src/provider-env.ts
index baab324713..103350836b 100644
--- a/packages/headless/src/provider-env.ts
+++ b/packages/headless/src/provider-env.ts
@@ -39,6 +39,7 @@ const PROVIDER_CREDENTIAL_ENV = {
),
'fireworks-ai': env('FIREWORKS', ['FIREWORKS_BASE_URL']),
nvidia: env('NVIDIA', ['NVIDIA_BASE_URL']),
+ 'ollama-cloud': env('OLLAMA'),
'tencent-tokenhub': env('TENCENT_TOKENHUB', ['TENCENT_TOKENHUB_BASE_URL']),
stepfun: env('STEPFUN', ['STEPFUN_BASE_URL']),
'stepfun-step-plan': env('STEPFUN_STEP_PLAN', ['STEPFUN_STEP_PLAN_BASE_URL']),
diff --git a/packages/runtime/src/__tests__/provider-conformance.test.ts b/packages/runtime/src/__tests__/provider-conformance.test.ts
index bd8a079e03..c8d1ae22d7 100644
--- a/packages/runtime/src/__tests__/provider-conformance.test.ts
+++ b/packages/runtime/src/__tests__/provider-conformance.test.ts
@@ -135,6 +135,115 @@ describe('models.dev provider conformance', () => {
assert.equal(result.text, 'Echoed hello.');
});
+ test('Ollama Cloud authenticates discovery and preserves exact ids and reasoning through a tool loop', async () => {
+ const modelId = 'qwen3.5:397b';
+ const requestBodies: Array> = [];
+ const server = await startJsonServer(async (request, response) => {
+ assert.equal(request.headers.authorization, 'Bearer ollama-cloud-test-key');
+ if (request.method === 'GET' && request.url === '/v1/models') {
+ respondJson(response, 200, {
+ object: 'list',
+ data: [{ id: modelId }, { id: 'gpt-oss:120b' }],
+ });
+ return;
+ }
+
+ assert.equal(request.method, 'POST');
+ assert.equal(request.url, '/v1/chat/completions');
+ const body = JSON.parse(await readBody(request)) as Record;
+ requestBodies.push(body);
+ if (requestBodies.length === 1) {
+ respondJson(response, 200, {
+ id: 'chatcmpl-ollama-cloud-tool',
+ object: 'chat.completion',
+ created: 1,
+ model: modelId,
+ choices: [{
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: null,
+ reasoning: 'I should call echo with the requested text.',
+ tool_calls: [{
+ id: 'call_ollama_cloud_echo',
+ type: 'function',
+ function: { name: 'echo', arguments: '{"text":"hello"}' },
+ }],
+ },
+ finish_reason: 'tool_calls',
+ }],
+ usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 },
+ });
+ return;
+ }
+
+ respondJson(response, 200, {
+ id: 'chatcmpl-ollama-cloud-final',
+ object: 'chat.completion',
+ created: 2,
+ model: modelId,
+ choices: [{
+ index: 0,
+ message: { role: 'assistant', content: 'Echoed hello.' },
+ finish_reason: 'stop',
+ }],
+ usage: { prompt_tokens: 14, completion_tokens: 3, total_tokens: 17 },
+ });
+ });
+ const connection: LlmConnection = {
+ slug: 'ollama-cloud',
+ name: 'Ollama Cloud',
+ providerType: 'ollama-cloud',
+ baseUrl: `${server.url}/v1`,
+ defaultModel: modelId,
+ enabled: true,
+ createdAt: 1,
+ updatedAt: 1,
+ };
+
+ assert.deepEqual(await fetchProviderModels(connection, 'ollama-cloud-test-key'), [
+ { id: modelId },
+ { id: 'gpt-oss:120b' },
+ ]);
+
+ const result = await generateText({
+ model: getAIModel({ connection, apiKey: 'ollama-cloud-test-key', modelId }),
+ prompt: 'Call echo with hello.',
+ providerOptions: buildProviderOptions(connection, modelId, 'high'),
+ stopWhen: stepCountIs(2),
+ tools: {
+ echo: tool({
+ description: 'Echo text',
+ inputSchema: z.object({ text: z.string() }),
+ execute: async ({ text }) => ({ echoed: text }),
+ }),
+ },
+ });
+
+ assert.equal(requestBodies.length, 2);
+ assert.deepEqual(requestBodies.map((body) => body.model), [modelId, modelId]);
+ assert.equal(requestBodies[0]?.reasoning_effort, 'high');
+ assert.equal(result.steps[0]?.reasoningText, 'I should call echo with the requested text.');
+ assert.deepEqual(
+ (requestBodies[1]?.messages as Array>).find(({ role }) => role === 'assistant'),
+ {
+ role: 'assistant',
+ content: null,
+ reasoning: 'I should call echo with the requested text.',
+ tool_calls: [{
+ id: 'call_ollama_cloud_echo',
+ type: 'function',
+ function: { name: 'echo', arguments: '{"text":"hello"}' },
+ }],
+ },
+ );
+ assert.deepEqual(
+ (requestBodies[1]?.messages as Array<{ role: string; content: string }>).find(({ role }) => role === 'tool'),
+ { role: 'tool', content: '{"echoed":"hello"}', tool_call_id: 'call_ollama_cloud_echo' },
+ );
+ assert.equal(result.text, 'Echoed hello.');
+ });
+
test('LocalAI preserves a configured llama.cpp Qwen3 alias and reasoning through a two-stage tool-call loop', async () => {
const modelId = 'localai/Qwen3-8B-Instruct-GGUF:Q4_K_M';
const requestBodies: Array> = [];
diff --git a/packages/runtime/src/model-factory.ts b/packages/runtime/src/model-factory.ts
index c47373ee04..e48853d206 100644
--- a/packages/runtime/src/model-factory.ts
+++ b/packages/runtime/src/model-factory.ts
@@ -163,8 +163,9 @@ export function buildProviderOptions(
},
};
case 'vercel':
+ case 'ollama-cloud':
return level
- ? { vercel: { reasoningEffort: level === 'off' ? 'none' : level } }
+ ? { [openaiCompatibleNamespace(connection.providerType)]: { reasoningEffort: level === 'off' ? 'none' : level } }
: {};
case 'google':
return {
diff --git a/packages/storage/src/__tests__/connection-store.test.ts b/packages/storage/src/__tests__/connection-store.test.ts
index 8576f52ad2..0e4565be1b 100644
--- a/packages/storage/src/__tests__/connection-store.test.ts
+++ b/packages/storage/src/__tests__/connection-store.test.ts
@@ -335,6 +335,34 @@ describe('FileConnectionStore', () => {
});
});
+ test('persists Ollama Cloud independently from local Ollama with exact model ids', async () => {
+ await withConnectionStore(async (store, dir) => {
+ await store.create({
+ slug: 'ollama-cloud',
+ name: 'Ollama Cloud',
+ providerType: 'ollama-cloud',
+ defaultModel: 'qwen3.5:397b',
+ });
+ await store.create({
+ slug: 'ollama-local',
+ name: 'Ollama',
+ providerType: 'ollama',
+ defaultModel: 'qwen3.5:cloud',
+ });
+
+ const persisted = JSON.parse(await readFile(join(dir, 'llm-connections.json'), 'utf8')) as {
+ connections: Array<{ providerType: string; defaultModel: string }>;
+ };
+ assert.deepEqual(
+ persisted.connections.map(({ providerType, defaultModel }) => ({ providerType, defaultModel })),
+ [
+ { providerType: 'ollama-cloud', defaultModel: 'qwen3.5:397b' },
+ { providerType: 'ollama', defaultModel: 'qwen3.5:cloud' },
+ ],
+ );
+ });
+ });
+
test('persists the Cloudflare Workers AI id, account-scoped base URL, and exact model id', async () => {
await withConnectionStore(async (store, dir) => {
const baseUrl = 'https://api.cloudflare.com/client/v4/accounts/account-123/ai/v1';
diff --git a/packages/storage/src/__tests__/credential-store.test.ts b/packages/storage/src/__tests__/credential-store.test.ts
index 68e26cb7b7..53df221e26 100644
--- a/packages/storage/src/__tests__/credential-store.test.ts
+++ b/packages/storage/src/__tests__/credential-store.test.ts
@@ -35,6 +35,16 @@ describe('FileCredentialStore', () => {
});
});
+ test('keeps Ollama Cloud credentials separate from local Ollama state', async () => {
+ await withTempDir(async (dir) => {
+ const store = createFileCredentialStore(dir);
+ await store.setSecret('ollama-cloud', 'api_key', 'ollama-cloud-test-key');
+
+ assert.equal(await store.getSecret('ollama-cloud', 'api_key'), 'ollama-cloud-test-key');
+ assert.equal(await store.getSecret('ollama-local', 'api_key'), null);
+ });
+ });
+
test('deleteSecret(slug) with no kind clears every kind for that slug only', async () => {
await withTempDir(async (dir) => {
const store = createFileCredentialStore(dir);
diff --git a/scripts/sync-model-metadata.mjs b/scripts/sync-model-metadata.mjs
index 665263a0cd..1de82941a7 100644
--- a/scripts/sync-model-metadata.mjs
+++ b/scripts/sync-model-metadata.mjs
@@ -18,6 +18,7 @@ const PROVIDERS = {
mistral: 'mistral',
moonshot: 'moonshotai-cn',
nvidia: 'nvidia',
+ 'ollama-cloud': 'ollama-cloud',
openai: 'openai',
siliconflow: 'siliconflow',
stepfun: 'stepfun',
diff --git a/scripts/sync-model-metadata.test.mjs b/scripts/sync-model-metadata.test.mjs
index c225a435df..c93cbc9107 100644
--- a/scripts/sync-model-metadata.test.mjs
+++ b/scripts/sync-model-metadata.test.mjs
@@ -7,7 +7,7 @@ import { promisify } from 'node:util';
import test from 'node:test';
const execFileAsync = promisify(execFile);
-const PROVIDER_IDS = ['anthropic', 'cerebras', 'cloudflare-workers-ai', 'cohere', 'deepinfra', 'deepseek', 'fireworks-ai', 'google', 'huggingface', 'minimax', 'minimax-cn', 'mistral', 'moonshotai-cn', 'nvidia', 'openai', 'siliconflow', 'stepfun', 'stepfun-ai', 'stepfun-ai-step-plan', 'tencent-coding-plan', 'tencent-token-plan', 'tencent-tokenhub', 'togetherai', 'vercel', 'xai', 'zai-coding-plan'];
+const PROVIDER_IDS = ['anthropic', 'cerebras', 'cloudflare-workers-ai', 'cohere', 'deepinfra', 'deepseek', 'fireworks-ai', 'google', 'huggingface', 'minimax', 'minimax-cn', 'mistral', 'moonshotai-cn', 'nvidia', 'ollama-cloud', 'openai', 'siliconflow', 'stepfun', 'stepfun-ai', 'stepfun-ai-step-plan', 'tencent-coding-plan', 'tencent-token-plan', 'tencent-tokenhub', 'togetherai', 'vercel', 'xai', 'zai-coding-plan'];
function withRequiredProviders(openai) {
return Object.fromEntries(PROVIDER_IDS.map((id) => {
@@ -91,6 +91,36 @@ test('sync-model-metadata vendors xAI provider facts and exact model ids', async
assert.match(generated, /"grok-4\.5": \{"displayName":"Grok 4\.5"/);
});
+test('sync-model-metadata vendors Ollama Cloud provider facts and exact model ids', async () => {
+ const directory = await mkdtemp(join(tmpdir(), 'maka-model-metadata-'));
+ const input = join(directory, 'api.json');
+ const output = join(directory, 'generated.ts');
+ const catalog = withRequiredProviders({});
+ catalog['ollama-cloud'] = {
+ ...catalog['ollama-cloud'],
+ name: 'Ollama Cloud',
+ api: 'https://ollama.com/v1',
+ doc: 'https://docs.ollama.com/cloud',
+ models: {
+ 'qwen3.5:397b': {
+ id: 'qwen3.5:397b', name: 'Qwen 3.5 397B', reasoning: true, tool_call: true,
+ modalities: { input: ['text', 'image'], output: ['text'] },
+ limit: { context: 262_144, output: 65_536 },
+ },
+ },
+ };
+ await writeFile(input, JSON.stringify(catalog));
+
+ await execFileAsync(process.execPath, [
+ 'scripts/sync-model-metadata.mjs', '--input', input, '--output', output,
+ ]);
+
+ const generated = await readFile(output, 'utf8');
+ assert.match(generated, /"ollama-cloud": \{/);
+ assert.match(generated, /"ollama-cloud": \{"id":"ollama-cloud","name":"Ollama Cloud","api":"https:\/\/ollama\.com\/v1"/);
+ assert.match(generated, /"qwen3\.5:397b": \{"displayName":"Qwen 3\.5 397B"/);
+});
+
test('sync-model-metadata vendors the stable Vercel AI Gateway id and exact creator/model ids', async () => {
const directory = await mkdtemp(join(tmpdir(), 'maka-model-metadata-'));
const input = join(directory, 'api.json');