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
@@ -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;