From 36f4913e301224fb61d7f64f79c1592ad2bb9bae Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 14 Apr 2026 16:39:55 +0100 Subject: [PATCH] fix(openai): share responses transport hooks --- extensions/openai/openai-codex-provider.ts | 14 ++------- extensions/openai/openai-provider.test.ts | 5 ++++ extensions/openai/openai-provider.ts | 15 ++-------- extensions/openai/shared.ts | 35 ++++++++++++++++++++++ 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index 43d1bd45ded..4dfedece13d 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -23,21 +23,15 @@ import { OPENAI_CODEX_DEFAULT_MODEL } from "./default-models.js"; import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js"; import { buildOpenAICodexProvider } from "./openai-codex-catalog.js"; import { CODEX_CLI_PROFILE_ID, readOpenAICodexCliOAuthProfile } from "./openai-codex-cli-auth.js"; -import { buildOpenAIReplayPolicy } from "./replay-policy.js"; import { + buildOpenAIResponsesProviderHooks, buildOpenAISyntheticCatalogEntry, cloneFirstTemplateModel, - defaultOpenAIResponsesExtraParams, findCatalogTemplate, isOpenAIApiBaseUrl, isOpenAICodexBaseUrl, matchesExactOrPrefix, - OPENAI_RESPONSES_STREAM_HOOKS, } from "./shared.js"; -import { - resolveOpenAITransportTurnState, - resolveOpenAIWebSocketSessionPolicy, -} from "./transport-policy.js"; const PROVIDER_ID = "openai-codex"; const OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api"; @@ -334,11 +328,7 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin { const id = ctx.modelId.trim().toLowerCase(); return id === OPENAI_CODEX_GPT_54_MODEL_ID || id === OPENAI_CODEX_GPT_54_PRO_MODEL_ID; }, - buildReplayPolicy: buildOpenAIReplayPolicy, - prepareExtraParams: (ctx) => defaultOpenAIResponsesExtraParams(ctx.extraParams), - ...OPENAI_RESPONSES_STREAM_HOOKS, - resolveTransportTurnState: (ctx) => resolveOpenAITransportTurnState(ctx), - resolveWebSocketSessionPolicy: (ctx) => resolveOpenAIWebSocketSessionPolicy(ctx), + ...buildOpenAIResponsesProviderHooks(), resolveReasoningOutputMode: () => "native", normalizeResolvedModel: (ctx) => { if (normalizeProviderId(ctx.provider) !== PROVIDER_ID) { diff --git a/extensions/openai/openai-provider.test.ts b/extensions/openai/openai-provider.test.ts index f5f848aff97..9a05bf1eaf5 100644 --- a/extensions/openai/openai-provider.test.ts +++ b/extensions/openai/openai-provider.test.ts @@ -403,6 +403,11 @@ describe("buildOpenAIProvider", () => { const codexProvider = buildOpenAICodexProviderPlugin(); expect(provider.wrapStreamFn).toBe(codexProvider.wrapStreamFn); + expect(provider.buildReplayPolicy).toBe(codexProvider.buildReplayPolicy); + expect(provider.resolveTransportTurnState).toBe(codexProvider.resolveTransportTurnState); + expect(provider.resolveWebSocketSessionPolicy).toBe( + codexProvider.resolveWebSocketSessionPolicy, + ); }); it("owns Azure OpenAI reasoning compatibility without forcing OpenAI transport defaults", () => { diff --git a/extensions/openai/openai-provider.ts b/extensions/openai/openai-provider.ts index 0ad77b1ccbf..deb4c27541f 100644 --- a/extensions/openai/openai-provider.ts +++ b/extensions/openai/openai-provider.ts @@ -11,20 +11,14 @@ import { } from "openclaw/plugin-sdk/provider-model-shared"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { applyOpenAIConfig, OPENAI_DEFAULT_MODEL } from "./default-models.js"; -import { buildOpenAIReplayPolicy } from "./replay-policy.js"; import { + buildOpenAIResponsesProviderHooks, buildOpenAISyntheticCatalogEntry, cloneFirstTemplateModel, - defaultOpenAIResponsesExtraParams, findCatalogTemplate, isOpenAIApiBaseUrl, matchesExactOrPrefix, - OPENAI_RESPONSES_STREAM_HOOKS, } from "./shared.js"; -import { - resolveOpenAITransportTurnState, - resolveOpenAIWebSocketSessionPolicy, -} from "./transport-policy.js"; const PROVIDER_ID = "openai"; const OPENAI_GPT_54_MODEL_ID = "gpt-5.4"; @@ -220,14 +214,9 @@ export function buildOpenAIProvider(): ProviderPlugin { shouldUseOpenAIResponsesTransport({ provider, api, baseUrl }) ? { api: "openai-responses", baseUrl } : undefined, - buildReplayPolicy: buildOpenAIReplayPolicy, - prepareExtraParams: (ctx) => - defaultOpenAIResponsesExtraParams(ctx.extraParams, { openaiWsWarmup: true }), - ...OPENAI_RESPONSES_STREAM_HOOKS, + ...buildOpenAIResponsesProviderHooks({ openaiWsWarmup: true }), matchesContextOverflowError: ({ errorMessage }) => /content_filter.*(?:prompt|input).*(?:too long|exceed)/i.test(errorMessage), - resolveTransportTurnState: (ctx) => resolveOpenAITransportTurnState(ctx), - resolveWebSocketSessionPolicy: (ctx) => resolveOpenAIWebSocketSessionPolicy(ctx), resolveReasoningOutputMode: () => "native", supportsXHighThinking: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_XHIGH_MODEL_IDS), isModernModelRef: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_MODERN_MODEL_IDS), diff --git a/extensions/openai/shared.ts b/extensions/openai/shared.ts index c34c0319a46..183c859ec6a 100644 --- a/extensions/openai/shared.ts +++ b/extensions/openai/shared.ts @@ -3,9 +3,15 @@ import { findCatalogTemplate } from "openclaw/plugin-sdk/provider-catalog-shared import { cloneFirstTemplateModel, matchesExactOrPrefix, + type ProviderPlugin, } from "openclaw/plugin-sdk/provider-model-shared"; import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { buildOpenAIReplayPolicy } from "./replay-policy.js"; +import { + resolveOpenAITransportTurnState, + resolveOpenAIWebSocketSessionPolicy, +} from "./transport-policy.js"; type SyntheticOpenAIModelCatalogCost = { input: number; @@ -78,6 +84,35 @@ export function defaultOpenAIResponsesExtraParams( }; } +type OpenAIResponsesProviderHooks = Pick< + ProviderPlugin, + | "buildReplayPolicy" + | "prepareExtraParams" + | "wrapStreamFn" + | "resolveTransportTurnState" + | "resolveWebSocketSessionPolicy" +>; + +const resolveOpenAIResponsesTransportTurnState: NonNullable< + OpenAIResponsesProviderHooks["resolveTransportTurnState"] +> = (ctx) => resolveOpenAITransportTurnState(ctx); + +const resolveOpenAIResponsesWebSocketSessionPolicy: NonNullable< + OpenAIResponsesProviderHooks["resolveWebSocketSessionPolicy"] +> = (ctx) => resolveOpenAIWebSocketSessionPolicy(ctx); + +export function buildOpenAIResponsesProviderHooks(options?: { + openaiWsWarmup?: boolean; +}): OpenAIResponsesProviderHooks { + return { + buildReplayPolicy: buildOpenAIReplayPolicy, + prepareExtraParams: (ctx) => defaultOpenAIResponsesExtraParams(ctx.extraParams, options), + ...OPENAI_RESPONSES_STREAM_HOOKS, + resolveTransportTurnState: resolveOpenAIResponsesTransportTurnState, + resolveWebSocketSessionPolicy: resolveOpenAIResponsesWebSocketSessionPolicy, + }; +} + export function buildOpenAISyntheticCatalogEntry( template: ReturnType, entry: {