@@ -8,6 +8,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Anthropic SSE parsing to ignore unknown proxy events such as OpenAI-style `done` terminators ([#3708](https://github.com/badlogic/pi-mono/issues/3708)).
|
||||
- Fixed OpenAI-compatible prompt cache tests to cover proxies that explicitly disable long cache retention.
|
||||
- Stopped sending `tools: []` on OpenAI-compatible, Anthropic, OpenAI Responses, OpenAI Codex Responses, and Azure OpenAI Responses requests when no tools are active (e.g. `pi --no-tools`). DashScope/Aliyun Qwen (OpenAI-compatible) rejects empty tools arrays with `"[] is too short - 'tools'"` (HTTP 400); the field is now omitted unless the conversation has tool history (the existing LiteLLM/Anthropic-proxy workaround).
|
||||
- Fixed `supportsXhigh()` to recognize DeepSeek V4 Pro, preserving `xhigh` reasoning requests so they map to DeepSeek's `max` effort ([#3662](https://github.com/badlogic/pi-mono/issues/3662))
|
||||
|
||||
@@ -237,6 +237,15 @@ interface SseDecoderState {
|
||||
raw: string[];
|
||||
}
|
||||
|
||||
const ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([
|
||||
"message_start",
|
||||
"message_delta",
|
||||
"message_stop",
|
||||
"content_block_start",
|
||||
"content_block_delta",
|
||||
"content_block_stop",
|
||||
]);
|
||||
|
||||
function flushSseEvent(state: SseDecoderState): ServerSentEvent | null {
|
||||
if (!state.event && state.data.length === 0) {
|
||||
return null;
|
||||
@@ -376,14 +385,14 @@ async function* iterateAnthropicEvents(
|
||||
}
|
||||
|
||||
for await (const sse of iterateSseMessages(response.body, signal)) {
|
||||
if (!sse.event || sse.event === "ping") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sse.event === "error") {
|
||||
throw new Error(sse.data);
|
||||
}
|
||||
|
||||
if (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? "")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
yield parseJsonWithRepair<RawMessageStreamEvent>(sse.data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -13,6 +13,61 @@ function createSseResponse(events: Array<{ event: string; data: string }>): Resp
|
||||
});
|
||||
}
|
||||
|
||||
const minimalAnthropicEvents = [
|
||||
{
|
||||
event: "message_start",
|
||||
data: JSON.stringify({
|
||||
type: "message_start",
|
||||
message: {
|
||||
id: "msg_test",
|
||||
usage: {
|
||||
input_tokens: 12,
|
||||
output_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
event: "content_block_start",
|
||||
data: JSON.stringify({
|
||||
type: "content_block_start",
|
||||
index: 0,
|
||||
content_block: { type: "text", text: "" },
|
||||
}),
|
||||
},
|
||||
{
|
||||
event: "content_block_delta",
|
||||
data: JSON.stringify({
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: { type: "text_delta", text: "Hello" },
|
||||
}),
|
||||
},
|
||||
{
|
||||
event: "content_block_stop",
|
||||
data: JSON.stringify({ type: "content_block_stop", index: 0 }),
|
||||
},
|
||||
{
|
||||
event: "message_delta",
|
||||
data: JSON.stringify({
|
||||
type: "message_delta",
|
||||
delta: { stop_reason: "end_turn" },
|
||||
usage: {
|
||||
input_tokens: 12,
|
||||
output_tokens: 5,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
event: "message_stop",
|
||||
data: JSON.stringify({ type: "message_stop" }),
|
||||
},
|
||||
];
|
||||
|
||||
function createFakeAnthropicClient(response: Response): Anthropic {
|
||||
return {
|
||||
messages: {
|
||||
@@ -110,4 +165,25 @@ describe("Anthropic raw SSE parsing", () => {
|
||||
text: "col1\tcol2",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores unknown SSE events after message_stop", async () => {
|
||||
const model = getModel("anthropic", "claude-haiku-4-5");
|
||||
const context: Context = {
|
||||
messages: [{ role: "user", content: "Say hello.", timestamp: Date.now() }],
|
||||
};
|
||||
const response = createSseResponse([
|
||||
...minimalAnthropicEvents,
|
||||
{ event: "done", data: "[DONE]" },
|
||||
{ event: "proxy.stats", data: "not json" },
|
||||
]);
|
||||
|
||||
const stream = streamAnthropic(model, context, {
|
||||
client: createFakeAnthropicClient(response),
|
||||
});
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(result.errorMessage).toBeUndefined();
|
||||
expect(result.content).toEqual([{ type: "text", text: "Hello" }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ const oauthToken = await resolveApiKey("anthropic");
|
||||
* - Result: tool call has name "Glob" but no tool exists with that name
|
||||
*/
|
||||
describe.skipIf(!oauthToken)("Anthropic OAuth tool name normalization", () => {
|
||||
const model = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||
const model = getModel("anthropic", "claude-sonnet-4-6");
|
||||
|
||||
it("should normalize user-defined tool matching CC name (todowrite -> TodoWrite -> todowrite)", async () => {
|
||||
// User defines a tool named "todowrite" (lowercase)
|
||||
|
||||
@@ -113,7 +113,7 @@ describe("Context overflow error handling", () => {
|
||||
|
||||
describe.skipIf(!process.env.ANTHROPIC_OAUTH_TOKEN)("Anthropic (OAuth)", () => {
|
||||
it("claude-sonnet-4 - should detect overflow via isContextOverflow", async () => {
|
||||
const model = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||
const model = getModel("anthropic", "claude-sonnet-4-6");
|
||||
const result = await testContextOverflow(model, process.env.ANTHROPIC_OAUTH_TOKEN!);
|
||||
logResult(result);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("google-shared convertMessages — Gemini 3 unsigned tool calls", () =>
|
||||
],
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
model: "claude-sonnet-4-6",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
@@ -146,7 +146,7 @@ describe("google-shared convertMessages — Gemini 3 unsigned tool calls", () =>
|
||||
],
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
model: "claude-sonnet-4-6",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("lazy provider module loading", () => {
|
||||
it("loads only the Anthropic SDK when calling the root lazy wrapper", () => {
|
||||
const result = runProbe(`
|
||||
const model = {
|
||||
id: "claude-sonnet-4-20250514",
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
@@ -92,7 +92,7 @@ describe("lazy provider module loading", () => {
|
||||
|
||||
it("loads only the Anthropic SDK when dispatching through streamSimple", () => {
|
||||
const result = runProbe(`
|
||||
const model = mod.getModel("anthropic", "claude-sonnet-4-20250514");
|
||||
const model = mod.getModel("anthropic", "claude-sonnet-4-6");
|
||||
const context = { messages: [{ role: "user", content: "hi" }] };
|
||||
await mod.streamSimple(model, context).result();
|
||||
`);
|
||||
|
||||
@@ -879,8 +879,8 @@ describe("Generate E2E Tests", () => {
|
||||
// Tokens are resolved at module level (see oauthTokens above)
|
||||
// =========================================================================
|
||||
|
||||
describe("Anthropic OAuth Provider (claude-sonnet-4-20250514)", () => {
|
||||
const model = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||
describe("Anthropic OAuth Provider (claude-sonnet-4-6)", () => {
|
||||
const model = getModel("anthropic", "claude-sonnet-4-6");
|
||||
|
||||
it.skipIf(!anthropicOAuthToken)("should complete basic text generation", { retry: 3 }, async () => {
|
||||
await basicTextGeneration(model, { apiKey: anthropicOAuthToken });
|
||||
|
||||
@@ -132,7 +132,7 @@ describe("totalTokens field", () => {
|
||||
"claude-sonnet-4 - should return totalTokens equal to sum of components",
|
||||
{ retry: 3, timeout: 60000 },
|
||||
async () => {
|
||||
const llm = getModel("anthropic", "claude-sonnet-4-20250514");
|
||||
const llm = getModel("anthropic", "claude-sonnet-4-6");
|
||||
|
||||
console.log(`\nAnthropic OAuth / ${llm.id}:`);
|
||||
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: anthropicOAuthToken });
|
||||
|
||||
Reference in New Issue
Block a user