custom-providers: live loaded-model status + thinking support for llamacpp
- Extend ProviderStatus with loadedModels[] populated from /running; llamacpp panel now shows active model name instead of just "N models" - Return DiscoverResult from discoverProvider (models + loadedModels) - All llamacpp models get reasoning:true + compat.thinkingFormat:"qwen-chat-template" so Ctrl+T / Shift+Tab work for both Qwen3 and Gemma4 (verified live)
This commit is contained in:
@@ -105,11 +105,12 @@ describe("discoverProvider dispatch", () => {
|
||||
apiKey: "ollama",
|
||||
defaultCtx: 8192,
|
||||
};
|
||||
const models = await discoverProvider(p);
|
||||
expect(models?.length).toBe(2);
|
||||
const result = await discoverProvider(p);
|
||||
expect(result?.models.length).toBe(2);
|
||||
expect(calls.some((u) => u.endsWith("/running"))).toBe(false);
|
||||
expect(calls.filter((u) => u.endsWith("/api/show")).length).toBe(2);
|
||||
expect(models?.[0].contextWindow).toBe(16384);
|
||||
expect(result?.models[0].contextWindow).toBe(16384);
|
||||
expect(result?.loadedModels).toEqual([]);
|
||||
});
|
||||
|
||||
it("llamacpp provider: probes /running once, never /api/show", async () => {
|
||||
@@ -120,13 +121,14 @@ describe("discoverProvider dispatch", () => {
|
||||
apiKey: "llamacpp",
|
||||
defaultCtx: 60000,
|
||||
};
|
||||
const models = await discoverProvider(p);
|
||||
expect(models?.length).toBe(2);
|
||||
const result = await discoverProvider(p);
|
||||
expect(result?.models.length).toBe(2);
|
||||
expect(calls.some((u) => u.endsWith("/api/show"))).toBe(false);
|
||||
expect(calls.filter((u) => u.endsWith("/running")).length).toBe(1);
|
||||
const byId = new Map(models?.map((m) => [m.id, m.contextWindow]));
|
||||
const byId = new Map(result?.models.map((m) => [m.id, m.contextWindow]));
|
||||
expect(byId.get("llama3")).toBe(32768);
|
||||
expect(byId.get("qwen3")).toBe(65536);
|
||||
expect(result?.loadedModels).toEqual(["llama3", "qwen3"]);
|
||||
});
|
||||
|
||||
it("strips a trailing /v1 from baseUrl when deriving the native URL", async () => {
|
||||
@@ -157,7 +159,37 @@ describe("discoverProvider dispatch", () => {
|
||||
apiKey: "ollama",
|
||||
defaultCtx: 4242,
|
||||
};
|
||||
const models = await discoverProvider(p);
|
||||
expect(models?.[0].contextWindow).toBe(4242);
|
||||
const result = await discoverProvider(p);
|
||||
expect(result?.models[0].contextWindow).toBe(4242);
|
||||
});
|
||||
|
||||
it("marks all llamacpp models as reasoning with qwen-chat-template compat", async () => {
|
||||
const p: CustomProvider = {
|
||||
name: "llamacpp",
|
||||
kind: "llamacpp",
|
||||
baseUrl: "http://localhost:9000/v1",
|
||||
apiKey: "llamacpp",
|
||||
defaultCtx: 60000,
|
||||
};
|
||||
const result = await discoverProvider(p);
|
||||
for (const m of result?.models ?? []) {
|
||||
expect(m.reasoning).toBe(true);
|
||||
expect(m.compat?.thinkingFormat).toBe("qwen-chat-template");
|
||||
}
|
||||
});
|
||||
|
||||
it("ollama models are not marked as reasoning", async () => {
|
||||
const p: CustomProvider = {
|
||||
name: "ollama",
|
||||
kind: "ollama",
|
||||
baseUrl: "http://localhost:11434/v1",
|
||||
apiKey: "ollama",
|
||||
defaultCtx: 8192,
|
||||
};
|
||||
const result = await discoverProvider(p);
|
||||
for (const m of result?.models ?? []) {
|
||||
expect(m.reasoning).toBe(false);
|
||||
expect(m.compat).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface DiscoveredModel {
|
||||
id: string;
|
||||
name: string;
|
||||
reasoning: boolean;
|
||||
compat?: { thinkingFormat: "qwen-chat-template" };
|
||||
input: Array<"text" | "image">;
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow: number;
|
||||
@@ -13,6 +14,12 @@ export interface DiscoveredModel {
|
||||
export interface ProviderStatus {
|
||||
ok: boolean;
|
||||
modelCount: number;
|
||||
loadedModels: string[];
|
||||
}
|
||||
|
||||
export interface DiscoverResult {
|
||||
models: DiscoveredModel[];
|
||||
loadedModels: string[];
|
||||
}
|
||||
|
||||
async function fetchJson<T>(
|
||||
@@ -74,21 +81,31 @@ export function parseContextFromCmd(cmd: string): number | null {
|
||||
return Number.isFinite(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
async function fetchLlamaSwapContexts(nativeUrl: string): Promise<Map<string, number> | null> {
|
||||
interface LlamaSwapInfo {
|
||||
contexts: Map<string, number>;
|
||||
loadedModels: string[];
|
||||
}
|
||||
|
||||
async function fetchLlamaSwapContexts(nativeUrl: string): Promise<LlamaSwapInfo | null> {
|
||||
const running = await fetchJson<{ running?: Array<{ model?: string; cmd?: string }> }>(
|
||||
`${nativeUrl}/running`,
|
||||
);
|
||||
if (!running || !Array.isArray(running.running)) return null;
|
||||
const map = new Map<string, number>();
|
||||
const contexts = new Map<string, number>();
|
||||
const loadedModels: string[] = [];
|
||||
for (const entry of running.running) {
|
||||
if (!entry.model || !entry.cmd) continue;
|
||||
const ctx = parseContextFromCmd(entry.cmd);
|
||||
if (ctx !== null) map.set(entry.model, ctx);
|
||||
if (!entry.model) continue;
|
||||
loadedModels.push(entry.model);
|
||||
if (entry.cmd) {
|
||||
const ctx = parseContextFromCmd(entry.cmd);
|
||||
if (ctx !== null) contexts.set(entry.model, ctx);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
return { contexts, loadedModels };
|
||||
}
|
||||
|
||||
export async function discoverProvider(p: CustomProvider): Promise<DiscoveredModel[] | null> {
|
||||
|
||||
export async function discoverProvider(p: CustomProvider): Promise<DiscoverResult | null> {
|
||||
const baseUrl = p.baseUrl.replace(/\/$/, "");
|
||||
const list = await fetchJson<{ data?: Array<{ id: string }> }>(`${baseUrl}/models`);
|
||||
if (!list?.data?.length) return null;
|
||||
@@ -96,22 +113,27 @@ export async function discoverProvider(p: CustomProvider): Promise<DiscoveredMod
|
||||
if (!chatModels.length) return null;
|
||||
|
||||
const nativeUrl = baseUrl.replace(/\/v1$/, "");
|
||||
const llamaSwapContexts =
|
||||
const llamaSwapInfo =
|
||||
p.kind === "llamacpp" ? await fetchLlamaSwapContexts(nativeUrl) : null;
|
||||
const loadedModels = llamaSwapInfo?.loadedModels ?? [];
|
||||
|
||||
return Promise.all(
|
||||
const models = await Promise.all(
|
||||
chatModels.map(async (m) => {
|
||||
let ctx = p.defaultCtx;
|
||||
if (p.kind === "llamacpp" && llamaSwapContexts) {
|
||||
ctx = llamaSwapContexts.get(m.id) ?? p.defaultCtx;
|
||||
if (p.kind === "llamacpp" && llamaSwapInfo) {
|
||||
ctx = llamaSwapInfo.contexts.get(m.id) ?? p.defaultCtx;
|
||||
} else if (p.kind === "ollama") {
|
||||
const real = await ollamaContext(nativeUrl, m.id);
|
||||
if (real) ctx = real;
|
||||
}
|
||||
const thinking: { reasoning: true; compat: { thinkingFormat: "qwen-chat-template" } } | { reasoning: false } =
|
||||
p.kind === "llamacpp"
|
||||
? { reasoning: true, compat: { thinkingFormat: "qwen-chat-template" } }
|
||||
: { reasoning: false };
|
||||
return {
|
||||
id: m.id,
|
||||
name: m.id,
|
||||
reasoning: false,
|
||||
...thinking,
|
||||
input: ["text"] as Array<"text" | "image">,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: ctx,
|
||||
@@ -119,4 +141,5 @@ export async function discoverProvider(p: CustomProvider): Promise<DiscoveredMod
|
||||
};
|
||||
}),
|
||||
);
|
||||
return { models, loadedModels };
|
||||
}
|
||||
|
||||
@@ -12,19 +12,19 @@ export default async function (pi: ExtensionAPI) {
|
||||
pi.unregisterProvider(p.name);
|
||||
registered.delete(p.name);
|
||||
}
|
||||
const models = await discoverProvider(p);
|
||||
if (models && models.length > 0) {
|
||||
const result = await discoverProvider(p);
|
||||
if (result && result.models.length > 0) {
|
||||
pi.registerProvider(p.name, {
|
||||
name: p.name,
|
||||
api: "openai-completions",
|
||||
baseUrl: p.baseUrl,
|
||||
apiKey: p.apiKey || p.name,
|
||||
models,
|
||||
models: result.models,
|
||||
});
|
||||
registered.add(p.name);
|
||||
statusByName.set(p.name, { ok: true, modelCount: models.length });
|
||||
statusByName.set(p.name, { ok: true, modelCount: result.models.length, loadedModels: result.loadedModels });
|
||||
} else {
|
||||
statusByName.set(p.name, { ok: false, modelCount: 0 });
|
||||
statusByName.set(p.name, { ok: false, modelCount: 0, loadedModels: [] });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,19 @@ type PanelAction =
|
||||
| { kind: "remove"; name: string }
|
||||
| { kind: "refresh"; name: string };
|
||||
|
||||
function statusLabel(status: ProviderStatus | undefined): string {
|
||||
function statusLabel(p: CustomProvider, status: ProviderStatus | undefined): string {
|
||||
if (!status) return "not checked";
|
||||
return status.ok ? `${status.modelCount} models` : "unreachable";
|
||||
if (!status.ok) return "unreachable";
|
||||
if (p.kind === "llamacpp" && status.loadedModels.length > 0) {
|
||||
const active = status.loadedModels[0];
|
||||
const rest = status.modelCount - 1;
|
||||
return rest > 0 ? `${active} (+${rest})` : active;
|
||||
}
|
||||
return `${status.modelCount} models`;
|
||||
}
|
||||
|
||||
function providerLine(p: CustomProvider, status: ProviderStatus | undefined): string {
|
||||
return `${p.name.padEnd(12)} [${p.kind.padEnd(8)}] ${p.baseUrl} [${statusLabel(status)}]`;
|
||||
return `${p.name.padEnd(12)} [${p.kind.padEnd(8)}] ${p.baseUrl} [${statusLabel(p, status)}]`;
|
||||
}
|
||||
|
||||
function showList(
|
||||
|
||||
Generated
-54
@@ -1456,9 +1456,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1476,9 +1473,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1496,9 +1490,6 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1516,9 +1507,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1536,9 +1524,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1788,9 +1773,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1805,9 +1787,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1822,9 +1801,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1839,9 +1815,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1856,9 +1829,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1873,9 +1843,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1890,9 +1857,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1907,9 +1871,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1924,9 +1885,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1941,9 +1899,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1958,9 +1913,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1975,9 +1927,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1992,9 +1941,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
Reference in New Issue
Block a user