
















@@ -0,0 +1,195 @@
1+import { Agent, type StreamFn } from "@mariozechner/pi-agent-core";
2+import {
3+createAssistantMessageEventStream,
4+type AssistantMessage,
5+type Context,
6+type Model,
7+type SimpleStreamOptions,
8+} from "@mariozechner/pi-ai";
9+import { streamSimpleOpenAICodexResponses } from "@mariozechner/pi-ai/openai-codex-responses";
10+import { streamSimpleOpenAIResponses } from "@mariozechner/pi-ai/openai-responses";
11+import { describe, expect, it } from "vitest";
12+13+type ResponsesModel = Model<"openai-responses"> | Model<"openai-codex-responses">;
14+15+const openaiModel = {
16+api: "openai-responses",
17+provider: "openai",
18+id: "gpt-5.5",
19+input: ["text"],
20+reasoning: true,
21+} as Model<"openai-responses">;
22+23+const codexModel = {
24+api: "openai-codex-responses",
25+provider: "openai-codex",
26+id: "gpt-5.5",
27+input: ["text"],
28+reasoning: true,
29+baseUrl: "https://chatgpt.com/backend-api",
30+} as Model<"openai-codex-responses">;
31+32+const codexTestToken = [
33+"eyJhbGciOiJub25lIn0",
34+"eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdF90ZXN0In19",
35+"signature",
36+].join(".");
37+38+describe("OpenAI thinking contract", () => {
39+it.each([
40+{ model: openaiModel, expectedReasoning: "high" },
41+{ model: codexModel, expectedReasoning: "high" },
42+])(
43+"forwards enabled session thinkingLevel to pi-ai options for $model.provider/$model.id",
44+async ({ model, expectedReasoning }) => {
45+const capturedOptions: SimpleStreamOptions[] = [];
46+const agent = new Agent({
47+initialState: {
48+ model,
49+thinkingLevel: "high",
50+},
51+streamFn: createCapturingStreamFn(model, capturedOptions),
52+});
53+54+await agent.prompt("hello");
55+56+expect(capturedOptions).toHaveLength(1);
57+expect(capturedOptions[0]?.reasoning).toBe(expectedReasoning);
58+},
59+);
60+61+it.each([openaiModel, codexModel])(
62+"does not forward reasoning when session thinkingLevel is off for $provider/$id",
63+async (model) => {
64+const capturedOptions: SimpleStreamOptions[] = [];
65+const agent = new Agent({
66+initialState: {
67+ model,
68+thinkingLevel: "off",
69+},
70+streamFn: createCapturingStreamFn(model, capturedOptions),
71+});
72+73+await agent.prompt("hello");
74+75+expect(capturedOptions).toHaveLength(1);
76+expect(capturedOptions[0]?.reasoning).toBeUndefined();
77+},
78+);
79+80+it("serializes OpenAI Responses reasoning effort from pi-ai simple options", async () => {
81+const payload = await captureProviderPayload({
82+model: openaiModel,
83+streamFn: streamSimpleOpenAIResponses,
84+options: { reasoning: "high" },
85+});
86+87+expect(payload.reasoning).toEqual({ effort: "high", summary: "auto" });
88+});
89+90+it("serializes Codex Responses reasoning effort from pi-ai simple options", async () => {
91+const payload = await captureProviderPayload({
92+model: codexModel,
93+streamFn: streamSimpleOpenAICodexResponses,
94+options: { reasoning: "high", transport: "sse" },
95+});
96+97+expect(payload.reasoning).toEqual({ effort: "high", summary: "auto" });
98+});
99+100+it("leaves Codex Responses reasoning absent when pi-agent-core disables thinking", async () => {
101+const payload = await captureProviderPayload({
102+model: codexModel,
103+streamFn: streamSimpleOpenAICodexResponses,
104+options: { transport: "sse" },
105+});
106+107+expect(payload).not.toHaveProperty("reasoning");
108+});
109+110+it("keeps OpenAI Responses reasoning explicitly disabled when pi-agent-core disables thinking", async () => {
111+const payload = await captureProviderPayload({
112+model: openaiModel,
113+streamFn: streamSimpleOpenAIResponses,
114+options: {},
115+});
116+117+expect(payload.reasoning).toEqual({ effort: "none" });
118+});
119+});
120+121+function createCapturingStreamFn(
122+model: ResponsesModel,
123+capturedOptions: SimpleStreamOptions[],
124+): StreamFn {
125+return (_model, _context, options) => {
126+capturedOptions.push({ ...options });
127+const stream = createAssistantMessageEventStream();
128+queueMicrotask(() => {
129+stream.push({
130+type: "done",
131+reason: "stop",
132+message: createAssistantMessage(model),
133+});
134+});
135+return stream;
136+};
137+}
138+139+function createAssistantMessage(model: ResponsesModel): AssistantMessage {
140+return {
141+role: "assistant",
142+content: [{ type: "text", text: "ok" }],
143+api: model.api,
144+provider: model.provider,
145+model: model.id,
146+usage: {
147+input: 0,
148+output: 0,
149+cacheRead: 0,
150+cacheWrite: 0,
151+totalTokens: 0,
152+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
153+},
154+stopReason: "stop",
155+timestamp: 0,
156+};
157+}
158+159+async function captureProviderPayload<
160+TApi extends "openai-responses" | "openai-codex-responses",
161+>(params: {
162+model: Model<TApi>;
163+streamFn: (
164+model: Model<TApi>,
165+context: Context,
166+options?: SimpleStreamOptions,
167+) => ReturnType<StreamFn>;
168+options: SimpleStreamOptions;
169+}): Promise<Record<string, unknown>> {
170+const payloadPromise = new Promise<Record<string, unknown>>((resolve, reject) => {
171+const timeout = setTimeout(
172+() => reject(new Error(`provider payload callback was not invoked for ${params.model.api}`)),
173+1_000,
174+);
175+const stream = params.streamFn(
176+params.model,
177+{
178+messages: [{ role: "user", content: "hello", timestamp: 0 }],
179+},
180+{
181+apiKey: params.model.api === "openai-codex-responses" ? codexTestToken : "test-api-key",
182+cacheRetention: "none",
183+ ...params.options,
184+onPayload: (payload) => {
185+clearTimeout(timeout);
186+resolve(structuredClone(payload as Record<string, unknown>));
187+throw new Error("stop after payload capture");
188+},
189+},
190+);
191+void Promise.resolve(stream).then((resolvedStream) => resolvedStream.result());
192+});
193+194+return payloadPromise;
195+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。