feat(tui): implement live model fetching and enhance model selection menus
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Tests for LlmClient.listModels() — the live /models fetch backing the TUI
|
||||
* model picker. Mocks global fetch so no network is touched.
|
||||
*/
|
||||
import { describe, test, expect, mock } from "bun:test";
|
||||
import { LlmClient } from "./provider.ts";
|
||||
|
||||
/** Replace global fetch once, returning the given JSON body for /models. */
|
||||
async function listWith(fetchImpl: (url: string) => Promise<Response>): Promise<string[]> {
|
||||
const orig = globalThis.fetch;
|
||||
globalThis.fetch = fetchImpl as typeof fetch;
|
||||
try {
|
||||
const client = new LlmClient("key", "some-model", "https://api.example.com/v1");
|
||||
return await client.listModels();
|
||||
} finally {
|
||||
globalThis.fetch = orig;
|
||||
}
|
||||
}
|
||||
|
||||
describe("LlmClient.listModels", () => {
|
||||
test("parses OpenAI-style { data: [{id}] }", async () => {
|
||||
const rows = await listWith(async (url: string) => {
|
||||
expect(url).toBe("https://api.example.com/v1/models");
|
||||
return new Response(JSON.stringify({ data: [{ id: "gpt-5" }, { id: "deepseek-v3" }] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
expect(rows).toEqual(["gpt-5", "deepseek-v3"]);
|
||||
});
|
||||
|
||||
test("parses { models: [string] } variant", async () => {
|
||||
const rows = await listWith(async () =>
|
||||
new Response(JSON.stringify({ models: ["a", "b"] }), { status: 200 }),
|
||||
);
|
||||
expect(rows).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("filters out missing ids", async () => {
|
||||
const rows = await listWith(async () =>
|
||||
new Response(JSON.stringify({ data: [{ id: "x" }, {}, { id: "y" }] }), { status: 200 }),
|
||||
);
|
||||
expect(rows).toEqual(["x", "y"]);
|
||||
});
|
||||
|
||||
test("returns [] on non-OK or throw", async () => {
|
||||
const rows = await listWith(async () => new Response("forbidden", { status: 403 }));
|
||||
expect(rows).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
void mock;
|
||||
@@ -123,6 +123,35 @@ export class LlmClient implements ProviderService {
|
||||
return `API request failed: ${err?.message ?? String(e)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the model ids exposed by the provider's `/models` endpoint
|
||||
* (OpenAI/Anthropic-compatible). Returns an empty array when the endpoint
|
||||
* isn't exposed or the call fails — callers fall back to static catalogs.
|
||||
*/
|
||||
async listModels(): Promise<string[]> {
|
||||
const url = `${this.baseUrl}/models`;
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: this.headers(),
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
});
|
||||
if (!resp.ok) return [];
|
||||
const data = (await resp.json()) as {
|
||||
data?: { id?: string }[] | null;
|
||||
models?: (string | { id?: string })[] | null;
|
||||
};
|
||||
const fromData = (data.data ?? []).map((m) => m?.id ?? "").filter(Boolean);
|
||||
if (fromData.length) return fromData;
|
||||
const fromModels = (data.models ?? [])
|
||||
.map((m) => (typeof m === "string" ? m : m?.id ?? ""))
|
||||
.filter(Boolean);
|
||||
return fromModels;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async chat(
|
||||
messages: ChatMessage[],
|
||||
tools?: ToolDef[],
|
||||
|
||||
Reference in New Issue
Block a user