mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
8a0903ebf2
The root barrel is now core-only and side-effect free: types, createModels/createProvider, auth substrate, lazyStream/lazyApi, faux, utils. Generated catalogs, api-registry, env-api-keys, images, global stream functions, and per-API lazy wrappers leave the root. New @earendil-works/pi-ai/compat preserves the old surface verbatim as a strict superset of the root: api-dispatch stream/complete with env key injection, the builtin registration side effect (skip-if-present so it cannot clobber earlier overrides), deprecated getModel/getModels/ getProviders aliases of the new getBuiltin* reads in providers/all, lazy api wrappers + setBedrockProviderModule, and image generation. Compat dies with the coding-agent ModelManager migration. Packaging: exports map gains ./compat, ./providers/*, ./api/*; sideEffects array lists only the effectful modules. Old-global imports across agent/coding-agent/examples and pi-ai tests switch to /compat (path-only; compat is a superset). The coding-agent extension loader resolves the pi-ai ROOT specifier to compat, so existing user extensions using the old global API keep working at runtime until compat is removed. vitest configs alias /compat to src; browser smoke imports old globals from /compat.
135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
import type { AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { type CompactionPreparation, compact, generateSummary } from "../src/core/compaction/index.ts";
|
|
|
|
const { completeSimpleMock } = vi.hoisted(() => ({
|
|
completeSimpleMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@earendil-works/pi-ai/compat", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("@earendil-works/pi-ai/compat")>();
|
|
return {
|
|
...actual,
|
|
completeSimple: completeSimpleMock,
|
|
};
|
|
});
|
|
|
|
function createModel(reasoning: boolean, maxTokens = 8192): Model<"anthropic-messages"> {
|
|
return {
|
|
id: reasoning ? "reasoning-model" : "non-reasoning-model",
|
|
name: reasoning ? "Reasoning Model" : "Non-reasoning Model",
|
|
api: "anthropic-messages",
|
|
provider: "anthropic",
|
|
baseUrl: "https://api.anthropic.com",
|
|
reasoning,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 200000,
|
|
maxTokens,
|
|
};
|
|
}
|
|
|
|
const mockSummaryResponse: AssistantMessage = {
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "## Goal\nTest summary" }],
|
|
api: "anthropic-messages",
|
|
provider: "anthropic",
|
|
model: "claude-sonnet-4-5",
|
|
usage: {
|
|
input: 10,
|
|
output: 10,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 20,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "stop",
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
const messages: AgentMessage[] = [{ role: "user", content: "Summarize this.", timestamp: Date.now() }];
|
|
|
|
describe("generateSummary reasoning options", () => {
|
|
beforeEach(() => {
|
|
completeSimpleMock.mockReset();
|
|
completeSimpleMock.mockResolvedValue(mockSummaryResponse);
|
|
});
|
|
|
|
it("uses the provided thinking level for reasoning-capable models", async () => {
|
|
await generateSummary(
|
|
messages,
|
|
createModel(true),
|
|
2000,
|
|
"test-key",
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
"medium",
|
|
);
|
|
|
|
expect(completeSimpleMock).toHaveBeenCalledTimes(1);
|
|
expect(completeSimpleMock.mock.calls[0][2]).toMatchObject({
|
|
reasoning: "medium",
|
|
apiKey: "test-key",
|
|
});
|
|
});
|
|
|
|
it("does not set reasoning when thinking is off", async () => {
|
|
await generateSummary(
|
|
messages,
|
|
createModel(true),
|
|
2000,
|
|
"test-key",
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
"off",
|
|
);
|
|
|
|
expect(completeSimpleMock).toHaveBeenCalledTimes(1);
|
|
expect(completeSimpleMock.mock.calls[0][2]).toMatchObject({
|
|
apiKey: "test-key",
|
|
});
|
|
expect(completeSimpleMock.mock.calls[0][2]).not.toHaveProperty("reasoning");
|
|
});
|
|
|
|
it("does not set reasoning for non-reasoning models", async () => {
|
|
await generateSummary(
|
|
messages,
|
|
createModel(false),
|
|
2000,
|
|
"test-key",
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
"medium",
|
|
);
|
|
|
|
expect(completeSimpleMock).toHaveBeenCalledTimes(1);
|
|
expect(completeSimpleMock.mock.calls[0][2]).toMatchObject({
|
|
apiKey: "test-key",
|
|
});
|
|
expect(completeSimpleMock.mock.calls[0][2]).not.toHaveProperty("reasoning");
|
|
});
|
|
|
|
it("clamps compaction summary maxTokens to the model output cap", async () => {
|
|
const preparation: CompactionPreparation = {
|
|
firstKeptEntryId: "entry-keep",
|
|
messagesToSummarize: messages,
|
|
turnPrefixMessages: messages,
|
|
isSplitTurn: true,
|
|
tokensBefore: 600000,
|
|
fileOps: { read: new Set(), written: new Set(), edited: new Set() },
|
|
settings: { enabled: true, reserveTokens: 500000, keepRecentTokens: 20000 },
|
|
};
|
|
|
|
await compact(preparation, createModel(false, 128000), "test-key");
|
|
|
|
expect(completeSimpleMock.mock.calls.map((call) => call[2]?.maxTokens)).toEqual([128000, 128000]);
|
|
});
|
|
});
|