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
+138
-27
@@ -2,8 +2,8 @@ import { arch, platform, release } from "node:os";
|
||||
import { Type } from "typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { stream as streamMistral } from "../src/api/mistral-conversations.ts";
|
||||
import { getModel } from "../src/compat.ts";
|
||||
import type { Context, FetchFunction, ProviderResponse } from "../src/types.ts";
|
||||
import { getModel, normalizeContext } from "../src/compat.ts";
|
||||
import type { Api, FetchFunction, Model, ProviderResponse } from "../src/types.ts";
|
||||
|
||||
const PI_USER_AGENT = `pi (${platform()} ${release()}; ${arch()})`;
|
||||
|
||||
@@ -39,7 +39,7 @@ function createTerminalEvent(finishReason = "stop") {
|
||||
describe("Mistral HTTP transport", () => {
|
||||
it("serializes SDK-style payloads to the Mistral wire format", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context: Context = {
|
||||
const context = normalizeContext({
|
||||
systemPrompt: "Be precise",
|
||||
messages: [
|
||||
{
|
||||
@@ -58,7 +58,7 @@ describe("Mistral HTTP transport", () => {
|
||||
parameters: Type.Object({ query: Type.String() }),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
let requestUrl: string | undefined;
|
||||
let requestInit: RequestInit | undefined;
|
||||
let callbackPayload: Record<string, unknown> | undefined;
|
||||
@@ -160,7 +160,7 @@ describe("Mistral HTTP transport", () => {
|
||||
|
||||
it("serializes assistant thinking, tool calls, and tool results for replay", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context: Context = {
|
||||
const context = normalizeContext({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
@@ -195,7 +195,7 @@ describe("Mistral HTTP transport", () => {
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
let requestInit: RequestInit | undefined;
|
||||
const fetch: FetchFunction = async (_input, init) => {
|
||||
requestInit = init;
|
||||
@@ -237,9 +237,9 @@ describe("Mistral HTTP transport", () => {
|
||||
|
||||
it("parses native thinking, text, fragmented tool calls, and cached-token usage", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context: Context = {
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
};
|
||||
});
|
||||
const events = [
|
||||
{
|
||||
id: "response-1",
|
||||
@@ -322,11 +322,88 @@ describe("Mistral HTTP transport", () => {
|
||||
expect(message.usage).toMatchObject({ input: 7, output: 4, cacheRead: 3, cacheWrite: 0, totalTokens: 14 });
|
||||
});
|
||||
|
||||
// #9674: GLM models on Mistral send empty content deltas at the start, around tool calls,
|
||||
// and sometimes mid-thinking. They must not open blocks or split thinking.
|
||||
it("ignores empty content deltas", async () => {
|
||||
const model = getModel("mistral", "zai-glm-5-3");
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
});
|
||||
const thinking = (text: string) => ({ type: "thinking", thinking: [{ type: "text", text }] });
|
||||
const toolCall = (args: string, first: boolean) => ({
|
||||
index: 0,
|
||||
...(first ? { id: "abc123456" } : {}),
|
||||
function: { name: first ? "read" : "", arguments: args },
|
||||
});
|
||||
const deltas = [
|
||||
{ content: "" },
|
||||
{ content: [thinking("first part,")] },
|
||||
{ content: "" },
|
||||
{ content: [{ type: "text", text: "" }] },
|
||||
{ content: [thinking(" second part."), { type: "text", text: "Reading." }] },
|
||||
{ content: "", tool_calls: [toolCall("", true)] },
|
||||
{ content: "", tool_calls: [toolCall('{"path":', false)] },
|
||||
{ content: "", tool_calls: [toolCall('"a.txt"}', false)] },
|
||||
{ content: "" },
|
||||
];
|
||||
const events = deltas.map((delta, i) => ({
|
||||
id: "response-1",
|
||||
model: model.id,
|
||||
choices: [{ index: 0, finish_reason: i === deltas.length - 1 ? "tool_calls" : null, delta }],
|
||||
}));
|
||||
|
||||
const message = await streamMistral(model, context, {
|
||||
apiKey: "test",
|
||||
fetch: async () => createSseResponse(events),
|
||||
}).result();
|
||||
|
||||
expect(message.content).toEqual([
|
||||
{ type: "thinking", thinking: "first part, second part." },
|
||||
{ type: "text", text: "Reading." },
|
||||
{ type: "toolCall", id: "abc123456", name: "read", arguments: { path: "a.txt" } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("forwards each parsed SSE payload before normalizing it", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
});
|
||||
const events = [
|
||||
{
|
||||
id: "response-1",
|
||||
provider_metadata: { request: "test" },
|
||||
choices: [{ index: 0, finish_reason: null, delta: { content: "hello" } }],
|
||||
},
|
||||
{
|
||||
id: "response-1",
|
||||
choices: [{ index: 0, finish_reason: "stop", delta: {} }],
|
||||
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
||||
},
|
||||
];
|
||||
const received: unknown[] = [];
|
||||
const eventModels: Model<Api>[] = [];
|
||||
const result = await streamMistral(model, context, {
|
||||
apiKey: "test",
|
||||
fetch: async () => createSseResponse(events),
|
||||
onProviderStreamEvent: async (event, eventModel) => {
|
||||
await Promise.resolve();
|
||||
received.push(event);
|
||||
eventModels.push(eventModel);
|
||||
},
|
||||
}).result();
|
||||
|
||||
expect(received).toEqual(events);
|
||||
expect(eventModels).toEqual([model, model]);
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(result.content).toEqual([{ type: "text", text: "hello" }]);
|
||||
});
|
||||
|
||||
it("parses SSE and UTF-8 sequences split across transport chunks", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context: Context = {
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
};
|
||||
});
|
||||
const fetch: FetchFunction = async () =>
|
||||
createBytewiseSseResponse({
|
||||
id: "response-bytewise",
|
||||
@@ -346,9 +423,9 @@ describe("Mistral HTTP transport", () => {
|
||||
...getModel("mistral", "mistral-large-latest"),
|
||||
headers: { Authorization: "Bearer model-key", "X-Affinity": "model-affinity" },
|
||||
};
|
||||
const context: Context = {
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
};
|
||||
});
|
||||
let requestHeaders: Headers | undefined;
|
||||
const fetch: FetchFunction = async (_input, init) => {
|
||||
requestHeaders = new Headers(init?.headers);
|
||||
@@ -369,9 +446,9 @@ describe("Mistral HTTP transport", () => {
|
||||
|
||||
it("aborts while waiting for an SSE chunk", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context: Context = {
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
};
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const fetch: FetchFunction = async () =>
|
||||
new Response(
|
||||
@@ -392,18 +469,15 @@ describe("Mistral HTTP transport", () => {
|
||||
expect(message.stopReason).toBe("aborted");
|
||||
});
|
||||
|
||||
it("applies the request timeout while waiting for an SSE chunk", async () => {
|
||||
it("applies the request timeout while waiting for response headers", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context: Context = {
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
};
|
||||
const fetch: FetchFunction = async () =>
|
||||
new Response(
|
||||
new ReadableStream({
|
||||
start() {},
|
||||
}),
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
);
|
||||
});
|
||||
const fetch: FetchFunction = (_url, init) =>
|
||||
new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true });
|
||||
});
|
||||
|
||||
const message = await streamMistral(model, context, {
|
||||
apiKey: "test",
|
||||
@@ -412,14 +486,51 @@ describe("Mistral HTTP transport", () => {
|
||||
}).result();
|
||||
|
||||
expect(message.stopReason).toBe("error");
|
||||
expect(message.errorMessage).toMatch(/timeout/i);
|
||||
expect(message.errorMessage).toBe("Mistral response headers timed out after 5ms");
|
||||
});
|
||||
|
||||
// Regression test for #10609: an active stream must not be cut off after timeoutMs.
|
||||
it("does not abort an active stream that lasts longer than the request timeout", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
});
|
||||
const encoder = new TextEncoder();
|
||||
const thinkingEvent = {
|
||||
choices: [{ index: 0, delta: { content: [{ type: "thinking", thinking: [{ type: "text", text: "x" }] }] } }],
|
||||
};
|
||||
const fetch: FetchFunction = async () =>
|
||||
new Response(
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(thinkingEvent)}\n\n`));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify(createTerminalEvent())}\n\ndata: [DONE]\n\n`),
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
);
|
||||
|
||||
const message = await streamMistral(model, context, {
|
||||
apiKey: "test",
|
||||
fetch,
|
||||
timeoutMs: 20,
|
||||
}).result();
|
||||
|
||||
expect(message.stopReason).toBe("stop");
|
||||
expect(message.content).toEqual([{ type: "thinking", thinking: "xxxxx" }]);
|
||||
});
|
||||
|
||||
it("preserves HTTP status and response bodies in errors", async () => {
|
||||
const model = getModel("mistral", "mistral-large-latest");
|
||||
const context: Context = {
|
||||
const context = normalizeContext({
|
||||
messages: [{ role: "user", content: "hello", timestamp: 1 }],
|
||||
};
|
||||
});
|
||||
const fetch: FetchFunction = async () =>
|
||||
new Response('{"message":"blocked by gateway"}', { status: 403, statusText: "Forbidden" });
|
||||
|
||||
|
||||
Reference in new issue
Block a user