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
@@ -0,0 +1,280 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getModel, normalizeContext } from "../src/compat.ts";
|
||||
import { azureProvider } from "../src/providers/azure.ts";
|
||||
import type { AssistantMessage, Context } from "../src/types.ts";
|
||||
|
||||
interface FakeOpenAIClientOptions {
|
||||
apiKey: string;
|
||||
baseURL: string;
|
||||
}
|
||||
|
||||
interface CapturedCompletionsPayload {
|
||||
model?: string;
|
||||
messages?: Array<{ role: string; content?: unknown; reasoning_content?: string }>;
|
||||
reasoning_effort?: string;
|
||||
thinking?: unknown;
|
||||
prompt_cache_key?: string;
|
||||
prompt_cache_retention?: string;
|
||||
}
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
lastParams: undefined as CapturedCompletionsPayload | undefined,
|
||||
lastClientOptions: undefined as FakeOpenAIClientOptions | undefined,
|
||||
dispatchedTo: undefined as "chat.completions" | "responses" | undefined,
|
||||
}));
|
||||
|
||||
vi.mock("openai", () => {
|
||||
class FakeOpenAI {
|
||||
chat = {
|
||||
completions: {
|
||||
create: (params: CapturedCompletionsPayload) => {
|
||||
mockState.lastParams = params;
|
||||
mockState.dispatchedTo = "chat.completions";
|
||||
const stream = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
choices: [{ delta: {}, finish_reason: "stop" }],
|
||||
usage: {
|
||||
prompt_tokens: 1,
|
||||
completion_tokens: 1,
|
||||
prompt_tokens_details: { cached_tokens: 0 },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const promise = Promise.resolve(stream) as Promise<typeof stream> & {
|
||||
withResponse: () => Promise<{
|
||||
data: typeof stream;
|
||||
response: { status: number; headers: Headers };
|
||||
}>;
|
||||
};
|
||||
promise.withResponse = async () => ({
|
||||
data: stream,
|
||||
response: { status: 200, headers: new Headers() },
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
constructor(options: FakeOpenAIClientOptions) {
|
||||
mockState.lastClientOptions = options;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeAzureOpenAI {
|
||||
responses = {
|
||||
create: () => {
|
||||
mockState.dispatchedTo = "responses";
|
||||
throw new Error("responses reached");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { default: FakeOpenAI, AzureOpenAI: FakeAzureOpenAI };
|
||||
});
|
||||
|
||||
const azure = azureProvider();
|
||||
|
||||
const originalBaseUrl = process.env.AZURE_OPENAI_BASE_URL;
|
||||
const originalCacheRetention = process.env.PI_CACHE_RETENTION;
|
||||
const originalDeploymentMap = process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP;
|
||||
|
||||
beforeEach(() => {
|
||||
mockState.lastParams = undefined;
|
||||
mockState.lastClientOptions = undefined;
|
||||
mockState.dispatchedTo = undefined;
|
||||
delete process.env.PI_CACHE_RETENTION;
|
||||
delete process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP;
|
||||
process.env.AZURE_OPENAI_BASE_URL = "https://my-resource.services.ai.azure.com";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalBaseUrl === undefined) delete process.env.AZURE_OPENAI_BASE_URL;
|
||||
else process.env.AZURE_OPENAI_BASE_URL = originalBaseUrl;
|
||||
if (originalCacheRetention === undefined) delete process.env.PI_CACHE_RETENTION;
|
||||
else process.env.PI_CACHE_RETENTION = originalCacheRetention;
|
||||
if (originalDeploymentMap === undefined) delete process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP;
|
||||
else process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP = originalDeploymentMap;
|
||||
});
|
||||
|
||||
const context = normalizeContext({
|
||||
systemPrompt: "sys",
|
||||
messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
|
||||
});
|
||||
|
||||
function deepSeekModel() {
|
||||
return getModel("azure", "deepseek-v4-pro");
|
||||
}
|
||||
|
||||
// Regression for #9645: Azure Foundry rejects DeepSeek's thinking field and every prompt cache parameter here.
|
||||
describe("azure deepseek-v4-pro over Chat Completions", () => {
|
||||
it("turns thinking on with reasoning_effort instead of DeepSeek's thinking field", async () => {
|
||||
await azure.streamSimple(deepSeekModel(), context, { apiKey: "test-key", reasoning: "high" }).result();
|
||||
|
||||
expect(mockState.lastParams?.reasoning_effort).toBe("high");
|
||||
expect(mockState.lastParams?.thinking).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clamps thinking levels the deployment does not accept", async () => {
|
||||
await azure.streamSimple(deepSeekModel(), context, { apiKey: "test-key", reasoning: "max" }).result();
|
||||
|
||||
expect(mockState.lastParams?.reasoning_effort).toBe("high");
|
||||
});
|
||||
|
||||
it("sends no reasoning_effort when no thinking level is requested", async () => {
|
||||
await azure.streamSimple(deepSeekModel(), context, { apiKey: "test-key" }).result();
|
||||
|
||||
expect(mockState.lastParams?.reasoning_effort).toBeUndefined();
|
||||
expect(mockState.lastParams?.thinking).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits prompt cache parameters when long retention comes from PI_CACHE_RETENTION", async () => {
|
||||
process.env.PI_CACHE_RETENTION = "long";
|
||||
|
||||
await azure.stream(deepSeekModel(), context, { apiKey: "test-key", sessionId: "session-env" }).result();
|
||||
|
||||
expect(mockState.lastParams?.prompt_cache_key).toBeUndefined();
|
||||
expect(mockState.lastParams?.prompt_cache_retention).toBeUndefined();
|
||||
});
|
||||
|
||||
// The deployment discards a `developer` system message once reasoning_effort is set, without
|
||||
// billing it, so the system prompt has to go out under the system role.
|
||||
it("sends the system prompt under the system role", async () => {
|
||||
await azure.stream(deepSeekModel(), context, { apiKey: "test-key", reasoningEffort: "low" }).result();
|
||||
|
||||
expect(mockState.lastParams?.messages?.[0]).toMatchObject({ role: "system", content: "sys" });
|
||||
});
|
||||
|
||||
// The deployment honours system messages sent mid-conversation, so pi must not collapse them.
|
||||
it("keeps mid-conversation system messages in place", async () => {
|
||||
const resumed = normalizeContext({
|
||||
systemPrompt: "first",
|
||||
messages: [
|
||||
{ role: "user", content: "hi", timestamp: Date.now() },
|
||||
{ role: "system", content: "second", timestamp: Date.now() },
|
||||
{ role: "user", content: "again", timestamp: Date.now() },
|
||||
],
|
||||
});
|
||||
|
||||
await azure.stream(deepSeekModel(), resumed, { apiKey: "test-key" }).result();
|
||||
|
||||
expect(mockState.lastParams?.messages?.map((message) => message.role)).toEqual([
|
||||
"system",
|
||||
"user",
|
||||
"system",
|
||||
"user",
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits prompt cache parameters even when long retention is requested", async () => {
|
||||
await azure
|
||||
.stream(deepSeekModel(), context, { apiKey: "test-key", cacheRetention: "long", sessionId: "session-1" })
|
||||
.result();
|
||||
|
||||
expect(mockState.lastParams?.prompt_cache_key).toBeUndefined();
|
||||
expect(mockState.lastParams?.prompt_cache_retention).toBeUndefined();
|
||||
});
|
||||
|
||||
it("replays reasoning_content on assistant turns so the cached prefix is unchanged", async () => {
|
||||
const assistant: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "internal reasoning", thinkingSignature: "reasoning_content" },
|
||||
{ type: "text", text: "answer" },
|
||||
],
|
||||
provider: "azure",
|
||||
api: "openai-completions",
|
||||
model: "deepseek-v4-pro",
|
||||
timestamp: Date.now(),
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
};
|
||||
const resumed: Context = {
|
||||
systemPrompt: "sys",
|
||||
messages: [
|
||||
{ role: "user", content: "first", timestamp: Date.now() },
|
||||
assistant,
|
||||
{ role: "user", content: "second", timestamp: Date.now() },
|
||||
],
|
||||
};
|
||||
|
||||
await azure.stream(deepSeekModel(), normalizeContext(resumed), { apiKey: "test-key" }).result();
|
||||
|
||||
const assistantMessage = mockState.lastParams?.messages?.find((message) => message.role === "assistant");
|
||||
expect(assistantMessage?.reasoning_content).toBe("internal reasoning");
|
||||
});
|
||||
});
|
||||
|
||||
describe("azure Chat Completions endpoint resolution", () => {
|
||||
it("normalizes the Azure endpoint the completions client is built with", async () => {
|
||||
await azure.stream(deepSeekModel(), context, { apiKey: "test-key" }).result();
|
||||
|
||||
expect(mockState.lastClientOptions?.baseURL).toBe("https://my-resource.services.ai.azure.com/openai/v1");
|
||||
});
|
||||
|
||||
it("surfaces an unconfigured endpoint as an error event rather than throwing out of stream()", async () => {
|
||||
delete process.env.AZURE_OPENAI_BASE_URL;
|
||||
|
||||
const result = await azure.stream(deepSeekModel(), context, { apiKey: "test-key" }).result();
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(result.errorMessage).toContain("Azure OpenAI base URL is required");
|
||||
});
|
||||
|
||||
// The id is persisted on the assistant message and read back by name, so it has to stay a catalog id.
|
||||
it("sends the model id as the request model", async () => {
|
||||
const result = await azure.stream(deepSeekModel(), context, { apiKey: "test-key" }).result();
|
||||
|
||||
expect(mockState.lastParams?.model).toBe("deepseek-v4-pro");
|
||||
expect(result.model).toBe("deepseek-v4-pro");
|
||||
});
|
||||
|
||||
it("sends the mapped deployment name while keeping the catalog id on the message", async () => {
|
||||
process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP = "deepseek-v4-pro=my-deepseek";
|
||||
|
||||
const result = await azure.streamSimple(deepSeekModel(), context, { apiKey: "test-key" }).result();
|
||||
|
||||
expect(mockState.lastParams?.model).toBe("my-deepseek");
|
||||
expect(result.model).toBe("deepseek-v4-pro");
|
||||
});
|
||||
|
||||
it("passes the deployment name through a caller's onPayload", async () => {
|
||||
process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP = "deepseek-v4-pro=my-deepseek";
|
||||
let seenModel: unknown;
|
||||
|
||||
await azure
|
||||
.stream(deepSeekModel(), context, {
|
||||
apiKey: "test-key",
|
||||
onPayload: (payload) => {
|
||||
seenModel = (payload as CapturedCompletionsPayload).model;
|
||||
return { ...(payload as object), temperature: 0.1 };
|
||||
},
|
||||
})
|
||||
.result();
|
||||
|
||||
expect(seenModel).toBe("my-deepseek");
|
||||
expect(mockState.lastParams).toMatchObject({ model: "my-deepseek", temperature: 0.1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("azure api map", () => {
|
||||
it("still routes Responses models to the Responses api", async () => {
|
||||
await azure.stream(getModel("azure", "gpt-4o-mini"), context, { apiKey: "test-key" }).result();
|
||||
|
||||
expect(mockState.dispatchedTo).toBe("responses");
|
||||
});
|
||||
|
||||
it("routes openai-completions models to chat completions", async () => {
|
||||
await azure.stream(deepSeekModel(), context, { apiKey: "test-key" }).result();
|
||||
|
||||
expect(mockState.dispatchedTo).toBe("chat.completions");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user