fix: warm discovery backed providers
This commit is contained in:
parent
c6fe3b2b79
commit
6fee7e60c8
5 changed files with 127 additions and 23 deletions
|
|
@ -255,7 +255,7 @@ describe("singularity-memory discovery", () => {
|
|||
// ─── Ollama Cloud Adapter ───────────────────────────────────────────────────
|
||||
|
||||
describe("ollama-cloud discovery", () => {
|
||||
it("uses the live Ollama /api/tags endpoint", async () => {
|
||||
it("uses the live Ollama /v1/models endpoint", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: Array<{ url: string; headers?: RequestInit["headers"] }> = [];
|
||||
globalThis.fetch = (async (
|
||||
|
|
@ -265,10 +265,7 @@ describe("ollama-cloud discovery", () => {
|
|||
calls.push({ url: String(input), headers: init?.headers });
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
models: [
|
||||
{ name: "kimi-k2.5", size: 1 },
|
||||
{ model: "kimi-k2.6", size: 1 },
|
||||
],
|
||||
data: [{ id: "kimi-k2.5" }, { id: "kimi-k2.6" }],
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
|
|
@ -278,7 +275,7 @@ describe("ollama-cloud discovery", () => {
|
|||
const adapter = getDiscoveryAdapter("ollama-cloud");
|
||||
const models = await adapter.fetchModels("test-key");
|
||||
|
||||
assert.equal(calls[0]?.url, "https://ollama.com/api/tags");
|
||||
assert.equal(calls[0]?.url, "https://ollama.com/v1/models");
|
||||
assert.deepEqual(calls[0]?.headers, {
|
||||
Authorization: "Bearer test-key",
|
||||
});
|
||||
|
|
@ -286,6 +283,41 @@ describe("ollama-cloud discovery", () => {
|
|||
models.map((m) => m.id),
|
||||
["kimi-k2.5", "kimi-k2.6"],
|
||||
);
|
||||
assert.ok(models.every((m) => m.api === "openai-completions"));
|
||||
assert.ok(models.every((m) => m.baseUrl === "https://ollama.com/v1"));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to /api/tags when /v1/models is unavailable", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: string[] = [];
|
||||
globalThis.fetch = (async (input: string | URL | Request) => {
|
||||
calls.push(String(input));
|
||||
if (calls.length === 1) {
|
||||
return new Response("not found", { status: 404 });
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
models: [{ name: "glm-5.1" }, { model: "qwen3-coder:480b" }],
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const adapter = getDiscoveryAdapter("ollama-cloud");
|
||||
const models = await adapter.fetchModels("test-key");
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"https://ollama.com/v1/models",
|
||||
"https://ollama.com/api/tags",
|
||||
]);
|
||||
assert.deepEqual(
|
||||
models.map((m) => m.id),
|
||||
["glm-5.1", "qwen3-coder:480b"],
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,28 +117,54 @@ class OllamaCloudDiscoveryAdapter implements ProviderDiscoveryAdapter {
|
|||
baseUrl?: string,
|
||||
): Promise<DiscoveredModel[]> {
|
||||
const root = (baseUrl ?? "https://ollama.com").replace(/\/+$/, "");
|
||||
const url = root.endsWith("/api/tags") ? root : `${root}/api/tags`;
|
||||
const primaryUrl =
|
||||
root.endsWith("/v1/models") || root.endsWith("/api/tags")
|
||||
? root
|
||||
: `${root}/v1/models`;
|
||||
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
|
||||
const response = await fetchWithTimeout(url, { headers });
|
||||
const response = await fetchWithTimeout(primaryUrl, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Ollama Cloud tags API returned ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
if (response.ok) {
|
||||
return this.mapOpenAIModelsResponse(await response.json());
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
models: Array<{ name?: string; model?: string; size?: number }>;
|
||||
};
|
||||
if (primaryUrl.endsWith("/v1/models")) {
|
||||
const fallbackUrl = `${root}/api/tags`;
|
||||
const fallback = await fetchWithTimeout(fallbackUrl, { headers });
|
||||
if (fallback.ok) return this.mapTagsResponse(await fallback.json());
|
||||
}
|
||||
|
||||
return (data.models ?? [])
|
||||
throw new Error(
|
||||
`Ollama Cloud models API returned ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
private mapOpenAIModelsResponse(data: unknown): DiscoveredModel[] {
|
||||
const models = (data as { data?: Array<{ id?: string }> }).data ?? [];
|
||||
return models
|
||||
.map((m) => m.id)
|
||||
.filter((id): id is string => typeof id === "string" && id.length > 0)
|
||||
.map((id) => this.toOpenAICompletionsModel(id));
|
||||
}
|
||||
|
||||
private mapTagsResponse(data: unknown): DiscoveredModel[] {
|
||||
const models =
|
||||
(data as { models?: Array<{ name?: string; model?: string }> }).models ??
|
||||
[];
|
||||
return models
|
||||
.map((m) => m.name ?? m.model)
|
||||
.filter((id): id is string => typeof id === "string" && id.length > 0)
|
||||
.map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
input: ["text" as const],
|
||||
}));
|
||||
.map((id) => this.toOpenAICompletionsModel(id));
|
||||
}
|
||||
|
||||
private toOpenAICompletionsModel(id: string): DiscoveredModel {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://ollama.com/v1",
|
||||
input: ["text" as const],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ describe("ModelRegistry — public discovery providers", () => {
|
|||
globalThis.fetch = (async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
models: [{ name: "kimi-k2.5" }, { model: "kimi-k2.6" }],
|
||||
data: [{ id: "kimi-k2.5" }, { id: "kimi-k2.6" }],
|
||||
}),
|
||||
{ status: 200 },
|
||||
)) as typeof fetch;
|
||||
|
|
@ -157,6 +157,37 @@ describe("ModelRegistry — public discovery providers", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("makes discovered ollama-cloud models available when auth is configured", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
data: [{ id: "kimi-k2.6" }],
|
||||
}),
|
||||
{ status: 200 },
|
||||
)) as typeof fetch;
|
||||
|
||||
try {
|
||||
const registry = new ModelRegistry(
|
||||
AuthStorage.inMemory({
|
||||
"ollama-cloud": { type: "api_key", key: "ollama-test" },
|
||||
}),
|
||||
undefined,
|
||||
new ModelDiscoveryCache(join(testDir, "ollama-cloud-auth-cache.json")),
|
||||
);
|
||||
await registry.discoverModels(["ollama-cloud"]);
|
||||
|
||||
assert.ok(
|
||||
registry
|
||||
.getAvailable()
|
||||
.some((m) => m.provider === "ollama-cloud" && m.id === "kimi-k2.6"),
|
||||
"discovered Ollama Cloud models are available after discovery",
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it("discovers direct provider models when auth is configured", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
|
|
|
|||
|
|
@ -839,7 +839,9 @@ export class ModelRegistry {
|
|||
*/
|
||||
getAvailable(providerModelAllow?: ProviderModelAllowList): Model<Api>[] {
|
||||
return this.filterProviderModelAllow(
|
||||
this.models.filter((m) => this.isProviderRequestReady(m.provider)),
|
||||
this.getAllWithDiscovered().filter((m) =>
|
||||
this.isProviderRequestReady(m.provider),
|
||||
),
|
||||
providerModelAllow,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
13
src/cli.ts
13
src/cli.ts
|
|
@ -69,6 +69,17 @@ function exitIfManagedResourcesAreNewer(currentAgentDir: string): void {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
async function warmDiscoveryBackedProviders(
|
||||
modelRegistry: ModelRegistry,
|
||||
): Promise<void> {
|
||||
const providers = ["ollama-cloud"].filter((provider) =>
|
||||
modelRegistry.isProviderRequestReady(provider),
|
||||
);
|
||||
if (providers.length === 0) return;
|
||||
|
||||
await modelRegistry.discoverModels(providers);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers used by both the print and interactive code paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -603,6 +614,8 @@ const modelsJsonPath = resolveModelsJsonPath();
|
|||
|
||||
const modelRegistry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
markStartup("ModelRegistry");
|
||||
await warmDiscoveryBackedProviders(modelRegistry);
|
||||
markStartup("ModelRegistry.discovery");
|
||||
const settingsManager = SettingsManager.create(process.cwd(), agentDir);
|
||||
applySecurityOverrides(settingsManager);
|
||||
markStartup("SettingsManager.create");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue