diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 8144058c..f3d625f4 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Changed the default agent transport to `auto` so providers can use their best available transport by default ([#4083](https://github.com/badlogic/pi-mono/issues/4083)). + ## [0.72.0] - 2026-05-01 ### Added diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 1017239b..53cb57f7 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -201,7 +201,7 @@ export class Agent { this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time"); this.sessionId = options.sessionId; this.thinkingBudgets = options.thinkingBudgets; - this.transport = options.transport ?? "sse"; + this.transport = options.transport ?? "auto"; this.maxRetryDelayMs = options.maxRetryDelayMs; this.toolExecution = options.toolExecution ?? "parallel"; } diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index d483baf9..bf6a8b75 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -23,6 +23,7 @@ ### Fixed +- Fixed OpenAI Codex Responses `streamSimple()` to honor the configured transport instead of always using SSE, and made `auto` the default transport with cached WebSocket context when available ([#4083](https://github.com/badlogic/pi-mono/issues/4083)). - Fixed Xiaomi MiMo model catalog to use the Token Plan Anthropic endpoint instead of the direct API ([#3912](https://github.com/badlogic/pi-mono/issues/3912)). ## [0.71.1] - 2026-05-01 diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index d57411f3..7c2adef5 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -165,7 +165,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses" websocketRequestId, ); const bodyJson = JSON.stringify(body); - const transport = options?.transport || "sse"; + const transport = options?.transport || "auto"; if (transport !== "sse") { let websocketStarted = false; @@ -992,7 +992,7 @@ async function processWebSocketStream( ): Promise { const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal); let keepConnection = true; - const useCachedContext = options?.transport === "websocket-cached"; + const useCachedContext = options?.transport === "websocket-cached" || options?.transport === "auto"; // ChatGPT Codex Responses rejects `store: true` ("Store must be set to false"). // WebSocket continuation still works via connection-scoped previous_response_id state. const fullBody = body; diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/providers/simple-options.ts index 8e99ad37..914ba5e7 100644 --- a/packages/ai/src/providers/simple-options.ts +++ b/packages/ai/src/providers/simple-options.ts @@ -6,6 +6,7 @@ export function buildBaseOptions(model: Model, options?: SimpleStreamOption maxTokens: options?.maxTokens ?? (model.maxTokens > 0 ? Math.min(model.maxTokens, 32000) : undefined), signal: options?.signal, apiKey: apiKey || options?.apiKey, + transport: options?.transport, cacheRetention: options?.cacheRetention, sessionId: options?.sessionId, headers: options?.headers, diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index ab51333a..49981cbf 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -451,6 +451,7 @@ describe("openai-codex streaming", () => { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", reasoning: true, + thinkingLevelMap: { xhigh: "xhigh" }, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 400000, @@ -751,6 +752,113 @@ describe("openai-codex streaming", () => { const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token }); await streamResult.result(); }); + it("forwards auto transport from streamSimple options and uses cached websocket context", async () => { + const token = mockToken(); + const sentBodies: unknown[] = []; + + global.fetch = vi.fn(async () => new Response("unexpected fetch", { status: 500 })) as typeof fetch; + + class MockWebSocket { + private listeners = new Map void>>(); + + constructor(_url: string, _protocols?: string | string[] | { headers?: Record }) { + queueMicrotask(() => this.dispatch("open", {})); + } + + addEventListener(type: string, listener: (event: unknown) => void): void { + let listeners = this.listeners.get(type); + if (!listeners) { + listeners = new Set(); + this.listeners.set(type, listeners); + } + listeners.add(listener); + } + + removeEventListener(type: string, listener: (event: unknown) => void): void { + this.listeners.get(type)?.delete(listener); + } + + send(data: string): void { + sentBodies.push(JSON.parse(data)); + const events = [ + { + type: "response.output_item.added", + item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] }, + }, + { type: "response.content_part.added", part: { type: "output_text", text: "" } }, + { type: "response.output_text.delta", delta: "Hello" }, + { + type: "response.output_item.done", + item: { + type: "message", + id: "msg_1", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "Hello" }], + }, + }, + { + type: "response.completed", + response: { + status: "completed", + usage: { + input_tokens: 5, + output_tokens: 3, + total_tokens: 8, + input_tokens_details: { cached_tokens: 0 }, + }, + }, + }, + ]; + queueMicrotask(() => { + for (const event of events) { + this.dispatch("message", { data: JSON.stringify(event) }); + } + }); + } + + close(): void {} + + private dispatch(type: string, event: unknown): void { + for (const listener of this.listeners.get(type) ?? []) { + listener(event); + } + } + } + + globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket; + + const model: Model<"openai-codex-responses"> = { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; + const context: Context = { + systemPrompt: "You are a helpful assistant.", + messages: [{ role: "user", content: "Say hello", timestamp: 1 }], + }; + + await streamSimpleOpenAICodexResponses(model, context, { + apiKey: token, + sessionId: "session-auto", + transport: "auto", + }).result(); + + expect(sentBodies).toHaveLength(1); + expect(global.fetch).not.toHaveBeenCalled(); + expect(getOpenAICodexWebSocketDebugStats("session-auto")).toMatchObject({ + cachedContextRequests: 1, + fullContextRequests: 1, + }); + }); + it("sends only response input deltas in websocket-cached mode", async () => { const token = mockToken(); const sentBodies: unknown[] = []; diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 48822147..86097d17 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -24,6 +24,7 @@ ### Fixed +- Fixed the default transport setting to use `auto`, allowing OpenAI Codex to use cached WebSocket context when available ([#4083](https://github.com/badlogic/pi-mono/issues/4083)). - Fixed `pi.registerProvider()` to honor per-model `baseUrl` overrides ([#4063](https://github.com/badlogic/pi-mono/issues/4063)). - Fixed self-update detection so `pi` correctly identifies when a newer version is available and applies updates ([#3942](https://github.com/badlogic/pi-mono/issues/3942), [#3980](https://github.com/badlogic/pi-mono/issues/3980), [#3922](https://github.com/badlogic/pi-mono/issues/3922)). diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index e82defe2..abedb9f2 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -78,7 +78,7 @@ export interface Settings { defaultProvider?: string; defaultModel?: string; defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; - transport?: TransportSetting; // default: "sse" + transport?: TransportSetting; // default: "auto" steeringMode?: "all" | "one-at-a-time"; followUpMode?: "all" | "one-at-a-time"; theme?: string; @@ -656,7 +656,7 @@ export class SettingsManager { } getTransport(): TransportSetting { - return this.settings.transport ?? "sse"; + return this.settings.transport ?? "auto"; } setTransport(transport: TransportSetting): void {