diff --git a/src/agents/openai-responses-payload-policy.ts b/src/agents/openai-responses-payload-policy.ts index 07d69a01eba..0540e6778f7 100644 --- a/src/agents/openai-responses-payload-policy.ts +++ b/src/agents/openai-responses-payload-policy.ts @@ -1,7 +1,5 @@ import { readStringValue } from "../shared/string-coerce.js"; import { supportsOpenAIReasoningEffort } from "./openai-reasoning-effort.js"; -import { isOpenAIResponsesApi } from "./provider-attribution.js"; -import { resolveProviderRequestPolicyConfig } from "./provider-request-config.js"; type OpenAIResponsesPayloadModel = { api?: unknown; @@ -19,6 +17,30 @@ type OpenAIResponsesPayloadPolicyOptions = { enableServerCompaction?: boolean; }; +type OpenAIResponsesEndpointClass = + | "default" + | "anthropic-public" + | "cerebras-native" + | "chutes-native" + | "deepseek-native" + | "github-copilot-native" + | "groq-native" + | "mistral-public" + | "moonshot-native" + | "modelstudio-native" + | "openai-public" + | "openai-codex" + | "opencode-native" + | "azure-openai" + | "openrouter" + | "xai-native" + | "zai-native" + | "google-generative-ai" + | "google-vertex" + | "local" + | "custom" + | "invalid"; + export type OpenAIResponsesPayloadPolicy = { allowsServiceTier: boolean; compactThreshold: number; @@ -29,6 +51,214 @@ export type OpenAIResponsesPayloadPolicy = { useServerCompaction: boolean; }; +type OpenAIResponsesPayloadCapabilities = { + allowsOpenAIServiceTier: boolean; + allowsResponsesStore: boolean; + shouldStripResponsesPromptCache: boolean; + supportsResponsesStoreField: boolean; + usesKnownNativeOpenAIRoute: boolean; +}; + +const OPENAI_RESPONSES_APIS = new Set([ + "openai-responses", + "azure-openai-responses", + "openai-codex-responses", +]); +const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]); +const LOCAL_ENDPOINT_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]); +const MODELSTUDIO_NATIVE_BASE_URLS = new Set([ + "https://coding-intl.dashscope.aliyuncs.com/v1", + "https://coding.dashscope.aliyuncs.com/v1", + "https://dashscope.aliyuncs.com/compatible-mode/v1", + "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", +]); +const MOONSHOT_NATIVE_BASE_URLS = new Set([ + "https://api.moonshot.ai/v1", + "https://api.moonshot.cn/v1", +]); + +function normalizeLowercaseString(value: unknown): string | undefined { + const stringValue = readStringValue(value)?.trim().toLowerCase(); + return stringValue ? stringValue : undefined; +} + +function normalizeComparableBaseUrl(value: unknown): string | undefined { + const trimmed = readStringValue(value)?.trim(); + if (!trimmed) { + return undefined; + } + const parsedValue = /^[a-z0-9.[\]-]+(?::\d+)?(?:[/?#].*)?$/i.test(trimmed) + ? `https://${trimmed}` + : trimmed; + try { + const url = new URL(parsedValue); + if (url.protocol !== "http:" && url.protocol !== "https:") { + return undefined; + } + url.hash = ""; + url.search = ""; + return url.toString().replace(/\/+$/, "").toLowerCase(); + } catch { + return undefined; + } +} + +function resolveUrlHostname(value: unknown): string | undefined { + const trimmed = readStringValue(value)?.trim(); + if (!trimmed) { + return undefined; + } + try { + return new URL(trimmed).hostname.toLowerCase(); + } catch { + try { + return new URL(`https://${trimmed}`).hostname.toLowerCase(); + } catch { + return undefined; + } + } +} + +function hostMatchesSuffix(host: string, suffix: string): boolean { + return suffix.startsWith(".") || suffix.startsWith("-") + ? host.endsWith(suffix) + : host === suffix || host.endsWith(`.${suffix}`); +} + +function isLocalEndpointHost(host: string): boolean { + return ( + LOCAL_ENDPOINT_HOSTS.has(host) || + host.endsWith(".localhost") || + host.endsWith(".local") || + host.endsWith(".internal") + ); +} + +function resolveBundledOpenAIResponsesEndpointClass( + baseUrl: unknown, +): OpenAIResponsesEndpointClass { + const trimmed = readStringValue(baseUrl)?.trim(); + if (!trimmed) { + return "default"; + } + const host = resolveUrlHostname(trimmed); + if (!host) { + return "invalid"; + } + const comparableBaseUrl = normalizeComparableBaseUrl(trimmed); + + switch (host) { + case "api.anthropic.com": + return "anthropic-public"; + case "api.cerebras.ai": + return "cerebras-native"; + case "llm.chutes.ai": + return "chutes-native"; + case "api.deepseek.com": + return "deepseek-native"; + case "api.groq.com": + return "groq-native"; + case "api.mistral.ai": + return "mistral-public"; + case "api.openai.com": + return "openai-public"; + case "chatgpt.com": + return "openai-codex"; + case "generativelanguage.googleapis.com": + return "google-generative-ai"; + case "aiplatform.googleapis.com": + return "google-vertex"; + case "api.x.ai": + case "api.grok.x.ai": + return "xai-native"; + case "api.z.ai": + return "zai-native"; + } + + if (hostMatchesSuffix(host, ".githubcopilot.com")) { + return "github-copilot-native"; + } + if (hostMatchesSuffix(host, ".openai.azure.com")) { + return "azure-openai"; + } + if (hostMatchesSuffix(host, "openrouter.ai")) { + return "openrouter"; + } + if (hostMatchesSuffix(host, "opencode.ai")) { + return "opencode-native"; + } + if (hostMatchesSuffix(host, "-aiplatform.googleapis.com")) { + return "google-vertex"; + } + if (comparableBaseUrl && MOONSHOT_NATIVE_BASE_URLS.has(comparableBaseUrl)) { + return "moonshot-native"; + } + if (comparableBaseUrl && MODELSTUDIO_NATIVE_BASE_URLS.has(comparableBaseUrl)) { + return "modelstudio-native"; + } + if (isLocalEndpointHost(host)) { + return "local"; + } + return "custom"; +} + +function isOpenAIResponsesApi(api: string | undefined): boolean { + return api !== undefined && OPENAI_RESPONSES_APIS.has(api); +} + +function readCompatPayloadBoolean( + compat: unknown, + key: "supportsPromptCacheKey" | "supportsStore", +): boolean | undefined { + if (!compat || typeof compat !== "object") { + return undefined; + } + const value = (compat as Record)[key]; + return typeof value === "boolean" ? value : undefined; +} + +function resolveOpenAIResponsesPayloadCapabilities( + model: OpenAIResponsesPayloadModel, +): OpenAIResponsesPayloadCapabilities { + const provider = normalizeLowercaseString(model.provider); + const api = normalizeLowercaseString(model.api); + const endpointClass = resolveBundledOpenAIResponsesEndpointClass(model.baseUrl); + const isResponsesApi = isOpenAIResponsesApi(api); + const usesConfiguredBaseUrl = endpointClass !== "default"; + const usesKnownNativeOpenAIEndpoint = + endpointClass === "openai-public" || + endpointClass === "openai-codex" || + endpointClass === "azure-openai"; + const usesKnownNativeOpenAIRoute = + endpointClass === "default" ? provider === "openai" : usesKnownNativeOpenAIEndpoint; + const usesExplicitProxyLikeEndpoint = usesConfiguredBaseUrl && !usesKnownNativeOpenAIEndpoint; + const promptCacheKeySupport = readCompatPayloadBoolean(model.compat, "supportsPromptCacheKey"); + const shouldStripResponsesPromptCache = + promptCacheKeySupport === true + ? false + : promptCacheKeySupport === false + ? isResponsesApi + : isResponsesApi && usesExplicitProxyLikeEndpoint; + const supportsResponsesStoreField = + readCompatPayloadBoolean(model.compat, "supportsStore") !== false && isResponsesApi; + + return { + allowsOpenAIServiceTier: + (provider === "openai" && api === "openai-responses" && endpointClass === "openai-public") || + (provider === "openai-codex" && + (api === "openai-codex-responses" || api === "openai-responses") && + endpointClass === "openai-codex"), + allowsResponsesStore: + supportsResponsesStoreField && + provider !== undefined && + OPENAI_RESPONSES_PROVIDERS.has(provider) && + usesKnownNativeOpenAIEndpoint, + shouldStripResponsesPromptCache, + supportsResponsesStoreField, + usesKnownNativeOpenAIRoute, + }; +} + function parsePositiveInteger(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value) && value > 0) { return Math.floor(value); @@ -50,14 +280,6 @@ function resolveOpenAIResponsesCompactThreshold(model: { contextWindow?: unknown return 80_000; } -function readCompatBoolean(compat: unknown, key: "supportsStore"): boolean | undefined { - if (!compat || typeof compat !== "object") { - return undefined; - } - const value = (compat as Record)[key]; - return typeof value === "boolean" ? value : undefined; -} - function shouldEnableOpenAIResponsesServerCompaction( explicitStore: boolean | undefined, provider: unknown, @@ -98,18 +320,7 @@ export function resolveOpenAIResponsesPayloadPolicy( model: OpenAIResponsesPayloadModel, options: OpenAIResponsesPayloadPolicyOptions = {}, ): OpenAIResponsesPayloadPolicy { - const compat = - model.compat && typeof model.compat === "object" - ? (model.compat as { supportsStore?: boolean }) - : undefined; - const capabilities = resolveProviderRequestPolicyConfig({ - provider: readStringValue(model.provider), - api: readStringValue(model.api), - baseUrl: readStringValue(model.baseUrl), - compat, - capability: "llm", - transport: "stream", - }).capabilities; + const capabilities = resolveOpenAIResponsesPayloadCapabilities(model); const storeMode = options.storeMode ?? "provider-policy"; const explicitStore = storeMode === "preserve" @@ -121,7 +332,7 @@ export function resolveOpenAIResponsesPayloadPolicy( : capabilities.allowsResponsesStore ? true : undefined; - const isResponsesApi = isOpenAIResponsesApi(readStringValue(model.api)); + const isResponsesApi = isOpenAIResponsesApi(normalizeLowercaseString(model.api)); const shouldStripDisabledReasoningPayload = isResponsesApi && (!capabilities.usesKnownNativeOpenAIRoute || !supportsOpenAIReasoningEffort(model, "none")); @@ -137,7 +348,7 @@ export function resolveOpenAIResponsesPayloadPolicy( options.enablePromptCacheStripping === true && capabilities.shouldStripResponsesPromptCache, shouldStripStore: explicitStore !== true && - readCompatBoolean(model.compat, "supportsStore") === false && + readCompatPayloadBoolean(model.compat, "supportsStore") === false && isResponsesApi, useServerCompaction: options.enableServerCompaction === true &&