feat: added WebSocket transport support for OpenAI Codex with SSE fallback

- Added WebSocket transport support for OpenAI Codex responses with automatic fallback to SSE on connection failure.
- Added preferWebsockets option to Agent and Model configurations to hint that WebSocket transport should be preferred when supported by provider implementations.
- Added prewarmOpenAICodexResponses() function to pre-establish WebSocket connections for improved performance.
- Added getProviderDetails() function and getOpenAICodexTransportDetails() function to expose transport state and provider configuration information.
- Added provider details display in session info showing active provider configuration and authentication details.
- Added OpenAI websockets setting to enable WebSocket transport preference for OpenAI Codex models in coding agent configuration.
This commit is contained in:
can1357
2026-02-14 00:01:25 +01:00
parent d1e16d2101
commit 7bf5664690
17 changed files with 1092 additions and 49 deletions
@@ -592,6 +592,15 @@ export const SETTINGS_SCHEMA = {
submenu: true,
},
},
"providers.openaiWebsockets": {
type: "boolean",
default: false,
ui: {
tab: "services",
label: "OpenAI websockets",
description: "Prefer websocket transport for OpenAI Codex models",
},
},
// ─────────────────────────────────────────────────────────────────────────
// Exa settings
@@ -1,7 +1,7 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
import { getEnvApiKey, getProviderDetails, type ProviderDetails, type UsageLimit, type UsageReport } from "@oh-my-pi/pi-ai";
import { copyToClipboard } from "@oh-my-pi/pi-natives";
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
import { Snowflake } from "@oh-my-pi/pi-utils";
@@ -18,6 +18,7 @@ import { PythonExecutionComponent } from "../../modes/components/python-executio
import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
import type { InteractiveModeContext } from "../../modes/types";
import { createCompactionSummaryMessage } from "../../session/messages";
import type { AuthStorage } from "../../session/auth-storage";
import { outputMeta } from "../../tools/output-meta";
import { resolveToCwd } from "../../tools/path-utils";
import { getChangelogPath, parseChangelog } from "../../utils/changelog";
@@ -207,6 +208,21 @@ export class CommandController {
let info = `${theme.bold("Session Info")}\n\n`;
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
info += `\n${theme.bold("Provider")}\n`;
const model = this.ctx.session.model;
if (!model) {
info += `${theme.fg("dim", "No model selected")}\n`;
} else {
const authMode = resolveProviderAuthMode(this.ctx.session.modelRegistry.authStorage, model.provider);
const providerDetails = getProviderDetails({
model,
sessionId: stats.sessionId,
authMode,
preferWebsockets: this.ctx.settings.get("providers.openaiWebsockets") ?? false,
});
info += renderProviderSection(providerDetails, theme);
}
info += `\n`;
info += `${theme.bold("Messages")}\n`;
info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
@@ -740,6 +756,32 @@ function formatDurationShort(ms: number): string {
return `${totalSeconds}s`;
}
function resolveProviderAuthMode(authStorage: AuthStorage, provider: string): string {
if (authStorage.hasOAuth(provider)) {
return "oauth";
}
if (authStorage.has(provider)) {
return "api key";
}
if (getEnvApiKey(provider)) {
return "env api key";
}
if (authStorage.hasAuth(provider)) {
return "runtime/fallback";
}
return "unknown";
}
export function renderProviderSection(details: ProviderDetails, uiTheme: Pick<typeof theme, "fg">): string {
const lines: string[] = [];
lines.push(`${uiTheme.fg("dim", "Name:")} ${details.provider}`);
for (const field of details.fields) {
lines.push(`${uiTheme.fg("dim", `${field.label}:`)} ${field.value}`);
}
return `${lines.join("\n")}\n`;
}
function resolveFraction(limit: UsageLimit): number | undefined {
const amount = limit.amount;
if (amount.usedFraction !== undefined) return amount.usedFraction;
+22
View File
@@ -1,5 +1,6 @@
import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai";
import { prewarmOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import type { Component } from "@oh-my-pi/pi-tui";
import { $env, logger, postmortem } from "@oh-my-pi/pi-utils";
import { getAgentDbPath, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
@@ -1036,6 +1037,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
thinkingBudgets: settings.getGroup("thinkingBudgets"),
temperature: settings.get("temperature") >= 0 ? settings.get("temperature") : undefined,
kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
preferWebsockets: settings.get("providers.openaiWebsockets") ?? false,
getToolContext: tc => toolContextStore.getContext(tc),
getApiKey: async provider => {
// Use the provider argument from the in-flight request;
@@ -1087,6 +1089,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
debugStartup("sdk:createAgentSession");
time("createAgentSession");
if (model?.api === "openai-codex-responses") {
try {
debugStartup("sdk:prewarmCodexWebsocket:start");
await prewarmOpenAICodexResponses(model, {
apiKey: await modelRegistry.getApiKey(model, sessionId),
sessionId,
preferWebsockets: settings.get("providers.openaiWebsockets") ?? false,
});
debugStartup("sdk:prewarmCodexWebsocket:done");
time("prewarmCodexWebsocket");
} catch (error) {
logger.debug("Codex websocket prewarm failed", {
error: error instanceof Error ? error.message : String(error),
provider: model.provider,
model: model.id,
});
}
}
// Warm up LSP servers (connects to detected servers)
let lspServers: CreateAgentSessionResult["lspServers"];
if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {