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.
110 lines
3.6 KiB
TypeScript
110 lines
3.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { getModel, streamSimple } from "../src/compat.ts";
|
|
import type { Context, Model, SimpleStreamOptions } from "../src/types.ts";
|
|
|
|
interface AnthropicThinkingPayload {
|
|
thinking?: { type: string; budget_tokens?: number; display?: string };
|
|
output_config?: { effort?: string };
|
|
}
|
|
|
|
class PayloadCaptured extends Error {
|
|
constructor() {
|
|
super("payload captured");
|
|
this.name = "PayloadCaptured";
|
|
}
|
|
}
|
|
|
|
function makeContext(): Context {
|
|
return {
|
|
messages: [{ role: "user", content: "Hello", timestamp: Date.now() }],
|
|
};
|
|
}
|
|
|
|
function makeCustomModel(compat?: Model<"anthropic-messages">["compat"]): Model<"anthropic-messages"> {
|
|
return {
|
|
// Id intentionally does not match any built-in adaptive substring. This
|
|
// mirrors corporate proxy schemes such as `anthropic--claude-opus-latest`.
|
|
id: "vendor--claude-opus-latest",
|
|
name: "Vendor Proxy Opus Latest",
|
|
api: "anthropic-messages",
|
|
provider: "vendor-proxy",
|
|
baseUrl: "http://127.0.0.1:9",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 200000,
|
|
maxTokens: 32000,
|
|
compat,
|
|
};
|
|
}
|
|
|
|
async function capturePayload(
|
|
model: Model<"anthropic-messages">,
|
|
options?: SimpleStreamOptions,
|
|
): Promise<AnthropicThinkingPayload> {
|
|
let capturedPayload: AnthropicThinkingPayload | undefined;
|
|
|
|
const payloadCaptureModel: Model<"anthropic-messages"> = {
|
|
...model,
|
|
baseUrl: "http://127.0.0.1:9",
|
|
};
|
|
|
|
const s = streamSimple(payloadCaptureModel, makeContext(), {
|
|
...options,
|
|
apiKey: "fake-key",
|
|
onPayload: (payload) => {
|
|
capturedPayload = payload as AnthropicThinkingPayload;
|
|
throw new PayloadCaptured();
|
|
},
|
|
});
|
|
|
|
await s.result();
|
|
|
|
if (!capturedPayload) {
|
|
throw new Error("Expected payload to be captured before request failure");
|
|
}
|
|
|
|
return capturedPayload;
|
|
}
|
|
|
|
describe("Anthropic forceAdaptiveThinking compat override", () => {
|
|
it("sends legacy thinking payload for custom model ids by default", async () => {
|
|
const payload = await capturePayload(makeCustomModel(), { reasoning: "medium" });
|
|
|
|
expect(payload.thinking?.type).toBe("enabled");
|
|
expect(payload.output_config).toBeUndefined();
|
|
});
|
|
|
|
it("sends adaptive thinking payload when compat.forceAdaptiveThinking is true", async () => {
|
|
const payload = await capturePayload(makeCustomModel({ forceAdaptiveThinking: true }), { reasoning: "medium" });
|
|
|
|
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
|
expect(payload.output_config).toEqual({ effort: "medium" });
|
|
});
|
|
|
|
it("uses adaptive thinking with native xhigh effort for Claude Fable 5", async () => {
|
|
const payload = await capturePayload(getModel("anthropic", "claude-fable-5"), { reasoning: "xhigh" });
|
|
|
|
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
|
expect(payload.output_config).toEqual({ effort: "xhigh" });
|
|
});
|
|
|
|
it("allows built-in adaptive models to opt out with compat.forceAdaptiveThinking false", async () => {
|
|
const model: Model<"anthropic-messages"> = {
|
|
...getModel("anthropic", "claude-opus-4-8"),
|
|
compat: { forceAdaptiveThinking: false },
|
|
};
|
|
const payload = await capturePayload(model, { reasoning: "medium" });
|
|
|
|
expect(payload.thinking?.type).toBe("enabled");
|
|
expect(payload.output_config).toBeUndefined();
|
|
});
|
|
|
|
it("preserves thinking.type=disabled when reasoning is off regardless of override", async () => {
|
|
const payload = await capturePayload(makeCustomModel({ forceAdaptiveThinking: true }));
|
|
|
|
expect(payload.thinking).toEqual({ type: "disabled" });
|
|
expect(payload.output_config).toBeUndefined();
|
|
});
|
|
});
|