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
20 changes: 19 additions & 1 deletion packages/hub-client/src/credential-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,20 @@ export async function fetchOllamaModelCapabilities(
* to fall back to a heuristic (name-sorting, a curated allowlist) to
* tell a chat model from an embedding one (CL-6351/CL-6366).
*/
/**
* Ollama's `-cloud` suffix names a model that this instance merely proxies
* to ollama.com — it answers `/api/tags` whether or not the box is signed
* in, but every actual inference call needs an ollama.com account and its
* own credential, which this catalog has no way to supply. Seeding one
* puts a keyed cloud upstream into what is otherwise an unauthenticated
* local chain (CL-6645). Excluded here rather than filtered by capability
* because a signed-in box's `-cloud` model still reports full
* capabilities — the suffix, not the capability probe, is what marks it.
*/
function isCloudProxyModel(modelName: string): boolean {
return modelName.endsWith("-cloud") || modelName.endsWith(":cloud");
}

export async function fetchOllamaModelCatalog(
baseURL: string,
fetchImpl: FetchLike = fetch as unknown as FetchLike,
Expand All @@ -634,8 +648,12 @@ export async function fetchOllamaModelCatalog(
if (parsed instanceof type.errors || parsed.models.length === 0) {
return undefined;
}
const localModels = parsed.models.filter(
(model) => !isCloudProxyModel(model.name),
);
if (localModels.length === 0) return undefined;
return await Promise.all(
parsed.models.map(async (model) => ({
localModels.map(async (model) => ({
canonicalName: model.name,
displayName: model.name,
capabilities: await fetchOllamaModelCapabilities(
Expand Down
45 changes: 45 additions & 0 deletions packages/hub-client/test/credential-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,51 @@ describe("fetchOllamaModelCatalog", () => {
]);
});

test("excludes ollama.com cloud-proxy models (a signed-out box can never serve them)", async () => {
const fetchImpl: FetchLike = async (url) => {
if (url.toString().endsWith("/api/tags")) {
return new Response(
JSON.stringify({
models: [
{ name: "gpt-oss:20b" },
{ name: "qwen3-coder:480b-cloud" },
{ name: "minimax-m2:cloud" },
],
}),
{ status: 200 },
);
}
return new Response(JSON.stringify({ capabilities: ["completion"] }), {
status: 200,
});
};

const models = await fetchOllamaModelCatalog(
"http://localhost:11434",
fetchImpl,
);
expect(models?.map((model) => model.canonicalName)).toEqual([
"gpt-oss:20b",
]);
});

test("returns undefined when every model is a cloud-proxy model", async () => {
const fetchImpl: FetchLike = async (url) => {
if (url.toString().endsWith("/api/tags")) {
return new Response(
JSON.stringify({ models: [{ name: "minimax-m2:cloud" }] }),
{ status: 200 },
);
}
return new Response(JSON.stringify({ capabilities: ["completion"] }), {
status: 200,
});
};
expect(
await fetchOllamaModelCatalog("http://localhost:11434", fetchImpl),
).toBeUndefined();
});

test("returns undefined when the instance is unreachable", async () => {
const fetchImpl: FetchLike = async () => {
throw new Error("ECONNREFUSED");
Expand Down
Loading