feat(ai): add packages/ai provider layer (Chunk #8)
Self-contained SSE-based provider layer for OpenAI-compatible endpoints (Ollama/llama.cpp/LM Studio/vLLM), Anthropic, Gemini, and OpenRouter. - budget.ts: re-export core token-budget engine (countTokens, allocateBudget, DEFAULT_BUDGET) - client.ts: streamCompletion/collectStream/complete with budget enforcement via enforceBudget + measureBudget - discover.ts: auto-detect local providers by probing common /v1/models - adapters.ts: per-provider param mapping + streaming adapters + factory - thinking.ts: thinking/reasoning-param normalization per provider - http.ts: SSE parsing + 429 retry - errors.ts: BudgetExceededError 35 unit/integration tests pass; typecheck and lint clean.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { discoverLocalProviders } from "./index.js";
|
||||
|
||||
/** Build a mock fetch that returns a models catalog for a given URL. */
|
||||
function mockFetch(
|
||||
catalog: Record<string, unknown>,
|
||||
): typeof fetch {
|
||||
return ((url: string) => {
|
||||
if (catalog[url]) {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify(catalog[url]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.reject(new Error("not found"));
|
||||
}) as unknown as typeof fetch;
|
||||
}
|
||||
|
||||
describe("discoverLocalProviders", () => {
|
||||
it("detects a running Ollama server", async () => {
|
||||
const results = await discoverLocalProviders({
|
||||
timeoutMs: 200,
|
||||
fetchImpl: mockFetch({
|
||||
"http://localhost:11434/v1/models": {
|
||||
object: "list",
|
||||
data: [
|
||||
{ id: "llama3.1", owned_by: "ollama" },
|
||||
{ id: "mistral", owned_by: "ollama" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0]!.endpoint).toBe("http://localhost:11434/v1/models");
|
||||
expect(results[0]!.kind).toBe("openai-compatible");
|
||||
expect(results[0]!.models.map((m) => m.id)).toEqual(["llama3.1", "mistral"]);
|
||||
});
|
||||
|
||||
it("returns an empty array when no endpoints respond", async () => {
|
||||
const results = await discoverLocalProviders({
|
||||
timeoutMs: 100,
|
||||
fetchImpl: mockFetch({}),
|
||||
});
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores endpoints that return no models", async () => {
|
||||
const results = await discoverLocalProviders({
|
||||
timeoutMs: 200,
|
||||
fetchImpl: mockFetch({
|
||||
"http://localhost:1234/v1/models": { object: "list", data: [] },
|
||||
}),
|
||||
});
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it("probes multiple endpoints and collects all responders", async () => {
|
||||
const results = await discoverLocalProviders({
|
||||
timeoutMs: 200,
|
||||
fetchImpl: mockFetch({
|
||||
"http://localhost:11434/v1/models": {
|
||||
object: "list",
|
||||
data: [{ id: "llama3.1" }],
|
||||
},
|
||||
"http://localhost:1234/v1/models": {
|
||||
object: "list",
|
||||
data: [{ id: "qwen2.5" }],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
const ids = results.flatMap((r) => r.models.map((m) => m.id));
|
||||
expect(ids).toContain("llama3.1");
|
||||
expect(ids).toContain("qwen2.5");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user