diff --git a/extensions/openai/provider-policy-api.test.ts b/extensions/openai/provider-policy-api.test.ts index e1f32970e3ed..842e3c5af0eb 100644 --- a/extensions/openai/provider-policy-api.test.ts +++ b/extensions/openai/provider-policy-api.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { normalizeModelCatalogId, + projectConfiguredModelRow, resolveModelRoutes, resolveThinkingProfile, } from "./provider-policy-api.js"; @@ -15,6 +16,58 @@ describe("OpenAI provider policy artifact", () => { vi.unstubAllEnvs(); }); + it("skips runtime normalization for canonical Responses rows", () => { + expect( + projectConfiguredModelRow({ + provider: "openai", + modelId: "gpt-5.5", + model: { + provider: "openai", + id: "gpt-5.5", + api: "openai-responses", + baseUrl: "http://127.0.0.1:8080/v1", + input: ["text"], + name: "GPT-5.5", + reasoning: true, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 96_000, + maxTokens: 128_000, + }, + }), + ).toBeNull(); + }); + + it.each([ + ["openai-completions", "https://api.openai.com/v1", "gpt-5.5"], + ["openai-chatgpt-responses", "https://chatgpt.com/backend-api/codex", "gpt-5.5"], + ["openai-responses", "https://chatgpt.com/backend-api/codex", "gpt-5.5"], + [undefined, "https://api.openai.com/v1", "gpt-5.5"], + ["anthropic-messages", "https://api.openai.com/v1", "gpt-5.5"], + ["openai-responses", "https://api.openai.com/v1", "gpt-5.4-codex"], + ] as const)( + "keeps runtime normalization for api=%s baseUrl=%s model=%s", + (api, baseUrl, modelId) => { + expect( + projectConfiguredModelRow({ + provider: "openai", + modelId, + model: { + provider: "openai", + id: modelId, + api, + baseUrl, + input: ["text"], + name: "OpenAI model", + reasoning: true, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 96_000, + maxTokens: 128_000, + }, + }), + ).toBeUndefined(); + }, + ); + it("normalizes the legacy Codex model alias at the provider boundary", () => { expect(normalizeModelCatalogId({ provider: " OpenAI ", modelId: "openai/GPT-5.4-CODEX" })).toBe( "gpt-5.4", diff --git a/extensions/openai/provider-policy-api.ts b/extensions/openai/provider-policy-api.ts index 95a3fa32c39c..e742383e0bed 100644 --- a/extensions/openai/provider-policy-api.ts +++ b/extensions/openai/provider-policy-api.ts @@ -1,5 +1,6 @@ // Openai API module exposes the plugin public contract. import type { ProviderDefaultThinkingPolicyContext } from "openclaw/plugin-sdk/core"; +import type { ProviderNormalizeResolvedModelContext } from "openclaw/plugin-sdk/plugin-entry"; import type { ModelApi, ModelProviderConfig, @@ -11,6 +12,7 @@ import type { } from "openclaw/plugin-sdk/provider-model-types"; import { classifyOpenAIBaseUrl, + isOpenAICodexBaseUrl, OPENAI_API_BASE_URL, OPENAI_CODEX_RESPONSES_BASE_URL, } from "./base-url.js"; @@ -52,6 +54,28 @@ export function normalizeModelCatalogId(params: ProviderNormalizeModelCatalogIdC : null; } +/** + * Skips full runtime loading only when OpenAI normalization is provably a no-op. + * Transport-sensitive routes and legacy model aliases still use the runtime hook. + */ +export function projectConfiguredModelRow(ctx: ProviderNormalizeResolvedModelContext) { + if (ctx.provider.trim().toLowerCase() !== "openai") { + return undefined; + } + const modelId = ctx.model.id; + const canonicalModelId = normalizeOpenAIModelRouteId(modelId); + const canonicalRouteId = normalizeOpenAIModelRouteId(ctx.modelId); + if ( + ctx.model.api !== OPENAI_RESPONSES_API || + isOpenAICodexBaseUrl(ctx.model.baseUrl) || + canonicalModelId !== modelId || + canonicalRouteId !== ctx.modelId + ) { + return undefined; + } + return null; +} + function firstRouteBaseUrl(...values: unknown[]): unknown { for (const value of values) { if (typeof value === "string") {