chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
+118
-20
@@ -3,8 +3,9 @@ import type { AddressInfo } from "node:net";
|
||||
import { Type } from "typebox";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts";
|
||||
import { getModel, streamSimple } from "../src/compat.ts";
|
||||
import { getModel, normalizeContext, streamSimple } from "../src/compat.ts";
|
||||
import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts";
|
||||
import { getSupportedThinkingLevels } from "../src/models.ts";
|
||||
import type { Context, Model, Tool } from "../src/types.ts";
|
||||
|
||||
const originalFireworksApiKey = process.env.FIREWORKS_API_KEY;
|
||||
@@ -18,8 +19,8 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("Fireworks models", () => {
|
||||
it("registers the default Kimi K2.6 model via Anthropic-compatible Messages API", () => {
|
||||
const model = getModel("fireworks", "accounts/fireworks/models/kimi-k2p6");
|
||||
it("registers non-GLM, non-Kimi-K3 models via Anthropic-compatible Messages API", () => {
|
||||
const model = getModel("fireworks", "accounts/fireworks/models/deepseek-v4p1-flash");
|
||||
|
||||
expect(model).toBeDefined();
|
||||
expect(model.api).toBe("anthropic-messages");
|
||||
@@ -27,19 +28,11 @@ describe("Fireworks models", () => {
|
||||
expect(model.baseUrl).toBe("https://api.fireworks.ai/inference");
|
||||
expect(model.reasoning).toBe(true);
|
||||
expect(model.input).toEqual(["text", "image"]);
|
||||
expect(model.contextWindow).toBe(262000);
|
||||
expect(model.maxTokens).toBe(262000);
|
||||
expect(model.cost).toEqual({
|
||||
input: 0.95,
|
||||
output: 4,
|
||||
cacheRead: 0.16,
|
||||
cacheWrite: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("aligns GLM 5.2 Fast with GLM 5.2's OpenAI-compatible config", () => {
|
||||
const base = getModel("fireworks", "accounts/fireworks/models/glm-5p2");
|
||||
const fast = getModel("fireworks", "accounts/fireworks/routers/glm-5p2-fast");
|
||||
it("aligns GLM 5.3 Fast with GLM 5.3's OpenAI-compatible config", () => {
|
||||
const base = getModel("fireworks", "accounts/fireworks/models/glm-5p3");
|
||||
const fast = getModel("fireworks", "accounts/fireworks/routers/glm-5p3-fast");
|
||||
|
||||
expect(fast.api).toBe(base.api);
|
||||
expect(fast.baseUrl).toBe(base.baseUrl);
|
||||
@@ -47,7 +40,7 @@ describe("Fireworks models", () => {
|
||||
expect(fast.thinkingLevelMap).toEqual(base.thinkingLevelMap);
|
||||
});
|
||||
|
||||
it.each(["accounts/fireworks/models/glm-5p2", "accounts/fireworks/routers/glm-5p2-fast"] as const)(
|
||||
it.each(["accounts/fireworks/models/glm-5p3", "accounts/fireworks/routers/glm-5p3-fast"] as const)(
|
||||
"omits unsupported long cache retention for %s",
|
||||
async (modelId) => {
|
||||
const model = getModel("fireworks", modelId);
|
||||
@@ -78,9 +71,11 @@ describe("Fireworks models", () => {
|
||||
const compat = {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsStrictMode: true,
|
||||
requiresReasoningContentOnAssistantMessages: true,
|
||||
thinkingFormat: "openai",
|
||||
deferredToolsMode: "kimi",
|
||||
supportsMidConvoSystemMessages: true,
|
||||
supportsMidConvoToolAdditions: true,
|
||||
sendSessionAffinityHeaders: true,
|
||||
supportsLongCacheRetention: false,
|
||||
};
|
||||
@@ -88,7 +83,7 @@ describe("Fireworks models", () => {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: "low",
|
||||
medium: "medium",
|
||||
medium: null,
|
||||
high: "high",
|
||||
xhigh: null,
|
||||
max: "max",
|
||||
@@ -121,6 +116,69 @@ describe("Fireworks models", () => {
|
||||
expect(payload?.reasoning_effort).toBe("max");
|
||||
});
|
||||
|
||||
// Regression for #9323: native effort must reach Messages without budget-based fallback.
|
||||
it.each([
|
||||
["accounts/fireworks/models/deepseek-v4p1-flash", ["off", "low", "high", "max"]],
|
||||
["accounts/fireworks/models/qwen3p8-max", ["off", "low", "medium", "xhigh"]],
|
||||
["accounts/fireworks/models/qwen3p8-2p4t-a95b", ["off", "low", "medium", "xhigh"]],
|
||||
] as const)("sends native Messages effort levels for %s", async (modelId, levels) => {
|
||||
const model = getModel("fireworks", modelId);
|
||||
expect(model.api).toBe("anthropic-messages");
|
||||
expect(model.compat?.forceAdaptiveThinking).toBe(true);
|
||||
expect(getSupportedThinkingLevels(model)).toEqual(levels);
|
||||
|
||||
for (const level of levels) {
|
||||
let payload: Record<string, unknown> | undefined;
|
||||
await streamSimple(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "test", timestamp: 0 }] },
|
||||
{
|
||||
apiKey: "test-fireworks-key",
|
||||
reasoning: level === "off" ? undefined : level,
|
||||
onPayload: (value) => {
|
||||
payload = value as Record<string, unknown>;
|
||||
throw new Error("payload captured");
|
||||
},
|
||||
},
|
||||
).result();
|
||||
expect(payload).toBeDefined();
|
||||
expect(payload?.thinking).toEqual(
|
||||
level === "off" ? { type: "disabled" } : { type: "adaptive", display: "summarized" },
|
||||
);
|
||||
expect(payload?.output_config).toEqual(level === "off" ? undefined : { effort: level });
|
||||
}
|
||||
});
|
||||
|
||||
// Regression for #9323: accepted aliases are not distinct native effort levels.
|
||||
it.each([
|
||||
["accounts/fireworks/models/glm-5p3", ["low", "high", "max"]],
|
||||
["accounts/fireworks/routers/glm-5p3-fast", ["low", "high", "max"]],
|
||||
["accounts/fireworks/models/kimi-k3", ["low", "high", "max"]],
|
||||
["accounts/fireworks/routers/kimi-k3-fast", ["low", "high", "max"]],
|
||||
] as const)("exposes distinct native effort levels for %s", (modelId, levels) => {
|
||||
expect(getSupportedThinkingLevels(getModel("fireworks", modelId))).toEqual(levels);
|
||||
});
|
||||
|
||||
it("keeps toggle-only Messages models without a verified fallback on budget-based thinking", async () => {
|
||||
const model = getModel("fireworks", "accounts/fireworks/models/nemotron-3-ultra-nvfp4");
|
||||
expect(model.compat?.forceAdaptiveThinking).toBeUndefined();
|
||||
let payload: Record<string, unknown> | undefined;
|
||||
await streamSimple(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "test", timestamp: 0 }] },
|
||||
{
|
||||
apiKey: "test-fireworks-key",
|
||||
reasoning: "high",
|
||||
onPayload: (value) => {
|
||||
payload = value as Record<string, unknown>;
|
||||
throw new Error("payload captured");
|
||||
},
|
||||
},
|
||||
).result();
|
||||
expect(payload?.thinking).toEqual({ type: "enabled", budget_tokens: 16384, display: "summarized" });
|
||||
expect(payload?.output_config).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves FIREWORKS_API_KEY from the environment", () => {
|
||||
process.env.FIREWORKS_API_KEY = "test-fireworks-key";
|
||||
|
||||
@@ -129,13 +187,14 @@ describe("Fireworks models", () => {
|
||||
});
|
||||
|
||||
it("sets Fireworks-specific compat for session affinity and unsupported tool fields", () => {
|
||||
const model = getModel("fireworks", "accounts/fireworks/models/kimi-k2p6");
|
||||
const model = getModel("fireworks", "accounts/fireworks/models/nemotron-3-ultra-nvfp4");
|
||||
|
||||
expect(model.compat).toBeDefined();
|
||||
expect(model.compat?.sendSessionAffinityHeaders).toBe(true);
|
||||
expect(model.compat?.supportsEagerToolInputStreaming).toBe(false);
|
||||
expect(model.compat?.supportsCacheControlOnTools).toBe(false);
|
||||
expect(model.compat?.supportsLongCacheRetention).toBe(false);
|
||||
expect(model.compat?.allowEmptySignature).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,6 +212,7 @@ const tool: Tool = {
|
||||
};
|
||||
|
||||
const FIREWORKS_ANTHROPIC_COMPAT = {
|
||||
allowEmptySignature: true,
|
||||
sendSessionAffinityHeaders: true,
|
||||
supportsEagerToolInputStreaming: false,
|
||||
supportsCacheControlOnTools: false,
|
||||
@@ -192,6 +252,15 @@ function createAnthropicModel(): Model<"anthropic-messages"> {
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenRouterModel(): Model<"anthropic-messages"> {
|
||||
return {
|
||||
...createAnthropicModel(),
|
||||
id: "anthropic/claude-opus-4.8",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api",
|
||||
};
|
||||
}
|
||||
|
||||
function createContext(tools: Tool[] = [tool]): Context {
|
||||
return {
|
||||
messages: [{ role: "user", content: "Use the tool", timestamp: Date.now() }],
|
||||
@@ -234,7 +303,7 @@ async function captureAnthropicRequest(
|
||||
// Override the model's baseUrl to point to the local test server
|
||||
const localModel = { ...model, baseUrl: `http://127.0.0.1:${address.port}` };
|
||||
|
||||
const stream = streamAnthropic(localModel, context, {
|
||||
const stream = streamAnthropic(localModel, normalizeContext(context), {
|
||||
apiKey: "test-key",
|
||||
cacheRetention: (options?.cacheRetention as "none" | "short" | "long") ?? "short",
|
||||
sessionId: options?.sessionId,
|
||||
@@ -263,7 +332,7 @@ function getTools(body: Record<string, unknown>): Record<string, unknown>[] {
|
||||
return tools as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
describe("Fireworks Anthropic session affinity and tool compat", () => {
|
||||
describe("Anthropic-compatible session affinity and tool compat", () => {
|
||||
it("sends x-session-affinity header for Fireworks models", async () => {
|
||||
const model = createFireworksModel();
|
||||
// Need a real port, capture will assign one
|
||||
@@ -293,6 +362,35 @@ describe("Fireworks Anthropic session affinity and tool compat", () => {
|
||||
expect(request.headers["x-session-affinity"]).toBeUndefined();
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/earendil-works/pi/issues/9102
|
||||
it("sends only x-session-id for OpenRouter models", async () => {
|
||||
const request = await captureAnthropicRequest(createOpenRouterModel(), createContext(), {
|
||||
sessionId: "openrouter-session-1",
|
||||
});
|
||||
|
||||
expect(request.headers["x-session-id"]).toBe("openrouter-session-1");
|
||||
expect(request.headers["x-session-affinity"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits OpenRouter session headers when cacheRetention is none", async () => {
|
||||
const request = await captureAnthropicRequest(createOpenRouterModel(), createContext(), {
|
||||
sessionId: "openrouter-session-2",
|
||||
cacheRetention: "none",
|
||||
});
|
||||
|
||||
expect(request.headers["x-session-id"]).toBeUndefined();
|
||||
expect(request.headers["x-session-affinity"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows OpenRouter session headers to be disabled", async () => {
|
||||
const model = { ...createOpenRouterModel(), compat: { sendSessionAffinityHeaders: false } };
|
||||
const request = await captureAnthropicRequest(model, createContext(), {
|
||||
sessionId: "openrouter-session-3",
|
||||
});
|
||||
|
||||
expect(request.headers["x-session-id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits cache_control on tools for Fireworks models", async () => {
|
||||
const model = createFireworksModel();
|
||||
const request = await captureAnthropicRequest(model, createContext());
|
||||
|
||||
Reference in new issue
Block a user