Commit a190397d8 made `Model.compat` resolve to `OpenAICompat | AnthropicCompat`
under the default `TApi = any`. The widened union broke every site that treated
`compat` as openai-shaped: model-registry deep-merge, openai-completions resolved
compat, and ~20 test fixtures. This restores the assumption locally instead of
papering over it with casts.
- getBundledModel is now generic on TApi so test fixtures that spread it into
`Model<"openai-completions">` get the narrow compat back.
- mergeCompat is generic over TBase/TOverride; the schema-driven model-registry
override path keeps its OpenAICompat-shaped merge fields, anthropic overrides
pass through untouched.
- OpenAICompatSchema gains the openai-only fields it was missing
(requiresMistralToolIds, reasoningContentField, requiresReasoningContent*,
thinkingFormat, requiresThinkingAsText, disableReasoningOnForcedToolChoice).
- resolveOpenAICompat fills in disableReasoningOnForcedToolChoice so the
Required<OpenAICompat> shape stays satisfied.
- Anthropic tool-result block id assignment uses the proper unknown double-cast.
- isForcedToolChoice accepts unknown so it can read `params.tool_choice` whose
type comes from the OpenAI SDK ChatCompletionToolChoiceOption (now wider than
our local OpenAICompletionsToolChoice).
- Test fixtures and Required<OpenAICompat> literals updated for the field set.
Fixes CI red on main.
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { getBundledModel } from "@oh-my-pi/pi-ai/models";
|
|
import { stream } from "@oh-my-pi/pi-ai/stream";
|
|
import type { Context, Model } from "@oh-my-pi/pi-ai/types";
|
|
import { e2eApiKey } from "./oauth";
|
|
|
|
function makeContext(): Context {
|
|
return {
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: `What is ${(Math.random() * 100) | 0} + ${(Math.random() * 100) | 0}? Think step by step.`,
|
|
timestamp: Date.now(),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("xhigh reasoning", () => {
|
|
describe("codex-max (supports xhigh)", () => {
|
|
// Note: codex models only support the responses API, not chat completions
|
|
it("should work with openai-responses", async () => {
|
|
const model = getBundledModel("openai", "gpt-5.1-codex-max");
|
|
const s = stream(model, makeContext(), { reasoning: "xhigh" });
|
|
let hasThinking = false;
|
|
|
|
for await (const event of s) {
|
|
if (event.type === "thinking_start" || event.type === "thinking_delta") {
|
|
hasThinking = true;
|
|
}
|
|
}
|
|
|
|
const response = await s.result();
|
|
expect(response.stopReason, `Error: ${response.errorMessage}`).toBe("stop");
|
|
expect(response.content.some(b => b.type === "text")).toBe(true);
|
|
expect(hasThinking || response.content.some(b => b.type === "thinking")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("gpt-5-mini (does not support xhigh)", () => {
|
|
it("should error with openai-responses when using xhigh", async () => {
|
|
const model = getBundledModel("openai", "gpt-5-mini") as Model<"openai-responses">;
|
|
const s = stream(model, makeContext(), { reasoning: "xhigh" });
|
|
|
|
for await (const _ of s) {
|
|
// drain events
|
|
}
|
|
|
|
const response = await s.result();
|
|
expect(response.stopReason).toBe("error");
|
|
expect(response.errorMessage).toContain("xhigh");
|
|
});
|
|
|
|
it("should error with openai-completions when using xhigh", async () => {
|
|
const model: Model<"openai-completions"> = {
|
|
...(getBundledModel("openai", "gpt-5-mini") as Model<"openai-completions">),
|
|
api: "openai-completions",
|
|
};
|
|
const s = stream(model, makeContext(), { reasoning: "xhigh" });
|
|
|
|
for await (const _ of s) {
|
|
// drain events
|
|
}
|
|
|
|
const response = await s.result();
|
|
expect(response.stopReason).toBe("error");
|
|
expect(response.errorMessage).toContain("xhigh");
|
|
});
|
|
});
|
|
});
|