Skip to content

Commit b333e83

Browse files
lidge-junclaude
andcommitted
feat(providers): add Ollama Cloud with web-verified text/vision classification
Ollama Cloud (hosted, OpenAI-compatible at https://ollama.com/v1, Bearer key) added to the key-login catalog. noVisionModels reflects the live ollama.com cloud lineup, classified text-vs-vision by web-searching ollama.com's cloud∩vision filter (not a stale third-party list — which wrongly tagged gpt-oss as image-capable; ollama.com confirms gpt-oss is text-only). - text-only → vision sidecar describes images: glm-5.x/4.7, minimax-m2.x, nemotron-3-*, deepseek-v4-pro/flash, gpt-oss, qwen3-coder. - vision-native (excluded): kimi-k2.5/.6/.7-code, minimax-m3, gemma3/4, qwen3.5, gemini-3-flash-preview, ministral-3, devstral-small-2, mistral-large-3. enrichProviderFromCatalog copies the catalog's models/classification onto the created provider config (the GUI/API only send adapter/baseUrl/apiKey), so the sidecars are actually gated; wired into POST /api/providers. GUI picker auto-lists it via the existing /api/key-providers catalog merge. Verified end-to-end with a probe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f58f038 commit b333e83

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

src/oauth/key-providers.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { OcxProviderConfig } from "../types";
2+
13
/**
24
* API-key "login" providers: not OAuth — the flow opens the provider's dashboard so the user can
35
* create/copy a key, then validates + stores it as the provider's `apiKey` (authMode "key").
@@ -11,6 +13,13 @@ export interface KeyLoginProvider {
1113
dashboardUrl: string;
1214
models?: string[];
1315
defaultModel?: string;
16+
/**
17+
* Model ids that do NOT accept image input (the vision sidecar describes images for them) / do NOT
18+
* accept a reasoning param. Copied into the created provider config by `enrichProviderFromCatalog`,
19+
* so the classification actually gates the sidecars (matching is tolerant of an Ollama ":size" tag).
20+
*/
21+
noVisionModels?: string[];
22+
noReasoningModels?: string[];
1423
}
1524

1625
export const KEY_LOGIN_PROVIDERS: Record<string, KeyLoginProvider> = {
@@ -32,8 +41,44 @@ export const KEY_LOGIN_PROVIDERS: Record<string, KeyLoginProvider> = {
3241
parallel: { label: "Parallel", baseUrl: "https://platform.parallel.ai", adapter: "openai-chat", dashboardUrl: "https://platform.parallel.ai" },
3342
zenmux: { label: "ZenMux", baseUrl: "https://zenmux.ai/api/v1", adapter: "openai-chat", dashboardUrl: "https://zenmux.ai" },
3443
litellm: { label: "LiteLLM (self-hosted)", baseUrl: "http://localhost:4000/v1", adapter: "openai-chat", dashboardUrl: "https://docs.litellm.ai/docs/proxy/quick_start" },
44+
// Ollama Cloud — hosted (not local), OpenAI-compatible at /v1, Bearer key from ollama.com.
45+
// models/noVisionModels reflect the live ollama.com cloud lineup (the proxy still fetches /v1/models
46+
// live; this is the seed + the vision/text classification, web-verified against ollama.com search
47+
// filters). Vision-capable cloud models are EXCLUDED from noVisionModels: kimi-k2.5/.6/.7-code,
48+
// minimax-m3, gemma3/gemma4, qwen3.5, gemini-3-flash-preview, ministral-3, devstral-small-2,
49+
// mistral-large-3. gpt-oss is text-only despite a stale third-party list claiming otherwise.
50+
"ollama-cloud": {
51+
label: "Ollama Cloud",
52+
baseUrl: "https://ollama.com/v1",
53+
adapter: "openai-chat",
54+
dashboardUrl: "https://ollama.com/settings/keys",
55+
models: ["glm-5.2", "deepseek-v4-pro", "qwen3-coder", "gpt-oss:120b", "kimi-k2.6", "minimax-m3", "qwen3.5", "gemma4"],
56+
defaultModel: "glm-5.2",
57+
noVisionModels: [
58+
"glm-5.2", "glm-5.1", "glm-5", "glm-4.7",
59+
"minimax-m2.7", "minimax-m2.5", "minimax-m2.1",
60+
"nemotron-3-ultra", "nemotron-3-super",
61+
"deepseek-v4-pro", "deepseek-v4-flash",
62+
"gpt-oss", "qwen3-coder",
63+
],
64+
},
3565
};
3666

67+
/**
68+
* Copy a key-login catalog entry's seed/classification (`models`, `noVisionModels`,
69+
* `noReasoningModels`, `defaultModel`) onto a provider config being created, for any field the caller
70+
* didn't already supply. Lets the vision/reasoning classification actually reach the saved config
71+
* (the GUI/API only send adapter/baseUrl/apiKey/defaultModel). No-op for non-catalog provider names.
72+
*/
73+
export function enrichProviderFromCatalog(name: string, prov: OcxProviderConfig): void {
74+
const e = KEY_LOGIN_PROVIDERS[name];
75+
if (!e) return;
76+
if (!prov.models && e.models) prov.models = [...e.models];
77+
if (!prov.defaultModel && e.defaultModel) prov.defaultModel = e.defaultModel;
78+
if (!prov.noVisionModels && e.noVisionModels) prov.noVisionModels = [...e.noVisionModels];
79+
if (!prov.noReasoningModels && e.noReasoningModels) prov.noReasoningModels = [...e.noReasoningModels];
80+
}
81+
3782
export function isKeyLoginProvider(name: string): boolean {
3883
return name in KEY_LOGIN_PROVIDERS;
3984
}

src/server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { CatalogModel } from "./codex-catalog";
1818
import { buildWebSearchTool, planWebSearch, runWithWebSearch } from "./web-search";
1919
import { describeImagesInPlace, planVisionSidecar } from "./vision";
2020
import { removeCredential } from "./oauth/store";
21-
import { listKeyLoginProviders } from "./oauth/key-providers";
21+
import { enrichProviderFromCatalog, listKeyLoginProviders } from "./oauth/key-providers";
2222
import type { OcxConfig, OcxProviderConfig } from "./types";
2323

2424
const VERSION = "0.0.1";
@@ -300,6 +300,9 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
300300
if (!name || !prov?.adapter || !prov?.baseUrl) {
301301
return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
302302
}
303+
// Catalog providers (e.g. ollama-cloud) carry a models + vision/reasoning classification the GUI
304+
// doesn't send — merge it in so the sidecars are gated correctly.
305+
enrichProviderFromCatalog(name, prov);
303306
const { saveConfig: save } = await import("./config");
304307
config.providers[name] = prov;
305308
if (body.setDefault) config.defaultProvider = name;

0 commit comments

Comments
 (0)