release: WRNexusJS 0.8.0
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { AIError, createAI } from "../src/index.ts";
|
||||
import {
|
||||
AIError,
|
||||
createAI,
|
||||
createAIClient,
|
||||
deterministicAIProvider,
|
||||
type AIProvider,
|
||||
} from "../src/index.ts";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
afterEach(() => {
|
||||
@@ -54,3 +60,76 @@ test("streams fragmented SSE and keeps a final event without a newline", async (
|
||||
for await (const chunk of createAI({ apiKey: "secret" }).stream("Hi")) chunks.push(chunk);
|
||||
expect(chunks).toEqual(["A", "B"]);
|
||||
});
|
||||
|
||||
test("generates and validates structured output with an offline provider", async () => {
|
||||
const client = createAIClient({
|
||||
providers: [deterministicAIProvider({ responses: ['```json\n{"ok":true}\n```'] })],
|
||||
});
|
||||
const result = await client.generateObject<{ ok: boolean }>("ignored", {
|
||||
validate: (value): value is { ok: boolean } =>
|
||||
typeof value === "object" && value !== null && "ok" in value,
|
||||
});
|
||||
expect(result.value).toEqual({ ok: true });
|
||||
expect(client.capabilities().deterministic?.structuredOutput).toBe(true);
|
||||
});
|
||||
|
||||
test("retries transient failures and reports redacted metadata", async () => {
|
||||
let calls = 0;
|
||||
const events: unknown[] = [];
|
||||
const provider: AIProvider = {
|
||||
name: "retrying",
|
||||
async generate() {
|
||||
calls++;
|
||||
if (calls === 1) throw new AIError("temporary secret-token", 503, "overloaded");
|
||||
return { value: "done", provider: "retrying", usage: { totalTokens: 3, costUsd: 0.01 } };
|
||||
},
|
||||
};
|
||||
const result = await createAIClient({
|
||||
providers: [provider],
|
||||
retry: { attempts: 2, baseDelayMs: 0 },
|
||||
onAttempt: (event) => {
|
||||
events.push(event);
|
||||
},
|
||||
}).generate("private prompt");
|
||||
expect(result.value).toBe("done");
|
||||
expect(calls).toBe(2);
|
||||
expect(JSON.stringify(events)).not.toContain("private prompt");
|
||||
expect(JSON.stringify(events)).not.toContain("raw");
|
||||
});
|
||||
|
||||
test("opens a provider circuit and falls back", async () => {
|
||||
let failedCalls = 0;
|
||||
const failing: AIProvider = {
|
||||
name: "failing",
|
||||
async generate() {
|
||||
failedCalls++;
|
||||
throw new AIError("down", 503);
|
||||
},
|
||||
};
|
||||
const client = createAIClient({
|
||||
providers: [failing, deterministicAIProvider({ responses: ["one", "two"] })],
|
||||
retry: { attempts: 1 },
|
||||
circuitBreaker: { failureThreshold: 1, resetAfterMs: 60_000 },
|
||||
});
|
||||
expect((await client.generate("first")).value).toBe("one");
|
||||
expect((await client.generate("second")).value).toBe("two");
|
||||
expect(failedCalls).toBe(1);
|
||||
});
|
||||
|
||||
test("executes validated tool calls and forwards cancellation", async () => {
|
||||
const client = createAIClient({ providers: [deterministicAIProvider({ responses: ["ok"] })] });
|
||||
const result = {
|
||||
value: "",
|
||||
provider: "deterministic",
|
||||
toolCalls: [{ id: "1", name: "double", arguments: { value: 4 } }],
|
||||
};
|
||||
const output = await client.executeTools(result, [
|
||||
{
|
||||
name: "double",
|
||||
validate: (value): value is { value: number } =>
|
||||
typeof value === "object" && value !== null && "value" in value,
|
||||
execute: ({ value }) => value * 2,
|
||||
},
|
||||
]);
|
||||
expect(output[0]?.value).toBe(8);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
aiRateLimiter,
|
||||
createRagPipeline,
|
||||
evaluateAI,
|
||||
googleAIProvider,
|
||||
guardedProvider,
|
||||
localAIProvider,
|
||||
maxPromptLength,
|
||||
memoryConversationStore,
|
||||
memoryVectorStore,
|
||||
openAIProvider,
|
||||
promptTemplate,
|
||||
} from "../src/platform.ts";
|
||||
|
||||
describe("AI platform", () => {
|
||||
test("adapts OpenAI, Google and local providers", async () => {
|
||||
const openFetch = (async () =>
|
||||
Response.json({
|
||||
model: "gpt",
|
||||
choices: [{ message: { content: "hello" }, finish_reason: "stop" }],
|
||||
usage: { total_tokens: 3 },
|
||||
})) as unknown as typeof fetch;
|
||||
expect(
|
||||
(await openAIProvider({ model: "gpt", apiKey: "key", fetch: openFetch }).generate("hi"))
|
||||
.value,
|
||||
).toBe("hello");
|
||||
const googleFetch = (async () =>
|
||||
Response.json({
|
||||
candidates: [{ content: { parts: [{ text: "hola" }] } }],
|
||||
usageMetadata: { totalTokenCount: 2 },
|
||||
})) as unknown as typeof fetch;
|
||||
expect(
|
||||
(
|
||||
await googleAIProvider({ model: "gemini", apiKey: "key", fetch: googleFetch }).generate(
|
||||
"hi",
|
||||
)
|
||||
).value,
|
||||
).toBe("hola");
|
||||
expect(
|
||||
(await localAIProvider({ model: "llama", fetch: openFetch }).generate("hi")).provider,
|
||||
).toBe("local");
|
||||
});
|
||||
|
||||
test("indexes and retrieves a RAG answer", async () => {
|
||||
const store = memoryVectorStore<{ source: string }>();
|
||||
const embeddings = {
|
||||
name: "test",
|
||||
embed: async (values: string[]) => ({
|
||||
vectors: values.map((value) => (value.includes("Bun") ? [1, 0] : [0, 1])),
|
||||
}),
|
||||
};
|
||||
const rag = createRagPipeline({
|
||||
embeddings,
|
||||
store,
|
||||
generate: async (prompt) => ({
|
||||
value: prompt.includes("fast") ? "Bun [1]" : "none",
|
||||
provider: "test",
|
||||
}),
|
||||
});
|
||||
await rag.index([
|
||||
{ id: "bun", text: "Bun is fast", metadata: { source: "docs" } },
|
||||
{ id: "other", text: "Other", metadata: { source: "other" } },
|
||||
]);
|
||||
const result = await rag.ask("Bun runtime");
|
||||
expect(result.value).toBe("Bun [1]");
|
||||
expect(result.sources[0]?.id).toBe("bun");
|
||||
});
|
||||
|
||||
test("persists conversations, renders prompts, guards and rate limits", async () => {
|
||||
const conversations = memoryConversationStore(2);
|
||||
await conversations.append("one", [
|
||||
{ role: "user", content: "a" },
|
||||
{ role: "assistant", content: "b" },
|
||||
{ role: "user", content: "c" },
|
||||
]);
|
||||
expect(await conversations.load("one")).toHaveLength(2);
|
||||
expect(promptTemplate("Hello {{name}}")({ name: "Wire" })).toBe("Hello Wire");
|
||||
const provider = guardedProvider(
|
||||
{ name: "test", generate: async () => ({ value: "ok", provider: "test" }) },
|
||||
[maxPromptLength(3)],
|
||||
);
|
||||
await expect(provider.generate("long")).rejects.toThrow("guardrail");
|
||||
const limit = aiRateLimiter({ limit: 1, windowMs: 100, now: () => 0 });
|
||||
expect(limit("u").allowed).toBe(true);
|
||||
expect(limit("u").allowed).toBe(false);
|
||||
});
|
||||
|
||||
test("evaluates model output", async () => {
|
||||
const report = await evaluateAI(
|
||||
[{ name: "answer", prompt: "question", expected: "42" }],
|
||||
async () => "42",
|
||||
);
|
||||
expect(report.score).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user