Files
openclaw/extensions/openai/provider-policy-api.ts
T
Peter Steinberger b080dd1e76 refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
2026-08-11 23:26:37 -07:00

621 lines
22 KiB
TypeScript

// 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,
ProviderModelRouteCandidate,
ProviderModelRouteResolution,
ProviderModelRouteSource,
ProviderNormalizeModelCatalogIdContext,
ProviderResponseModelEquivalenceContext,
ProviderResolveModelRoutesContext,
} from "openclaw/plugin-sdk/provider-model-types";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
classifyOpenAIBaseUrl,
isOpenAICodexBaseUrl,
OPENAI_API_BASE_URL,
OPENAI_CODEX_RESPONSES_BASE_URL,
} from "./base-url.js";
import {
isOpenAIDualRouteModelId,
isOpenAIPlatformOnlyRouteModelId,
isOpenAISubscriptionOnlyRouteModelId,
normalizeOpenAIModelRouteId,
OPENAI_GPT_56_MODEL_ID,
OPENAI_GPT_56_SOL_MODEL_ID,
} from "./model-route-contract.js";
import { resolveUnifiedOpenAIThinkingProfile } from "./thinking-policy.js";
const OPENAI_RESPONSES_API = "openai-responses";
const OPENAI_COMPLETIONS_API = "openai-completions";
const OPENAI_CHATGPT_RESPONSES_API = "openai-chatgpt-responses";
const OPENAI_PROVIDER_ID = "openai";
const OPENAI_AGENT_RUNTIME_ID = "openclaw";
const CODEX_AGENT_RUNTIME_ID = "codex";
const OPENCLAW_RUNTIME_COMPATIBLE_IDS = [OPENAI_AGENT_RUNTIME_ID] as const;
const CODEX_RUNTIME_COMPATIBLE_IDS = [OPENAI_AGENT_RUNTIME_ID, CODEX_AGENT_RUNTIME_ID] as const;
type OpenAIResolveSingleModelRouteContext = Omit<
ProviderResolveModelRoutesContext,
"observedRoutes"
> & {
observed?: ProviderModelRouteSource;
};
function normalizeOptionalRouteApi(value: ModelApi | null | undefined): ModelApi | undefined {
return normalizeOptionalString(value) as ModelApi | undefined;
}
/** Canonical logical id for OpenAI catalog projection. */
export function normalizeModelCatalogId(params: ProviderNormalizeModelCatalogIdContext) {
return params.provider.trim().toLowerCase() === OPENAI_PROVIDER_ID
? normalizeOpenAIModelRouteId(params.modelId)
: null;
}
export function isResponseModelEquivalent(params: ProviderResponseModelEquivalenceContext) {
return (
params.provider.trim().toLowerCase() === OPENAI_PROVIDER_ID &&
params.requestedModelId === OPENAI_GPT_56_MODEL_ID &&
params.responseModelId === OPENAI_GPT_56_SOL_MODEL_ID
);
}
/** Resolves authored OpenAI provider config without activating the runtime plugin. */
export function resolveAuthoredOpenAIProviderConfig(params: {
provider: string;
config?: { models?: { providers?: Record<string, ModelProviderConfig | undefined> } };
}): ModelProviderConfig | undefined {
if (params.provider.trim().toLowerCase() !== OPENAI_PROVIDER_ID) {
return undefined;
}
const providers = Object.entries(params.config?.models?.providers ?? {});
const requestedProvider = params.provider.trim();
const providerKey =
providers.find(([providerId]) => providerId.trim() === requestedProvider)?.[0].trim() ??
providers
.find(([providerId]) => providerId.trim().toLowerCase() === OPENAI_PROVIDER_ID)?.[0]
.trim();
let providerConfig: ModelProviderConfig | undefined;
for (const [providerId, candidate] of providers) {
if (providerId.trim() !== providerKey || !candidate) {
continue;
}
providerConfig = providerConfig
? {
...providerConfig,
...candidate,
models: candidate.models ?? providerConfig.models,
}
: candidate;
}
return providerConfig;
}
/**
* 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_PROVIDER_ID) {
return undefined;
}
const configuredProvider = resolveAuthoredOpenAIProviderConfig(ctx);
const configuredApi = normalizeOptionalRouteApi(configuredProvider?.api);
const modelId = ctx.model.id;
const canonicalModelId = normalizeOpenAIModelRouteId(modelId);
const canonicalRouteId = normalizeOpenAIModelRouteId(ctx.modelId);
if (
(configuredApi !== undefined && configuredApi !== OPENAI_RESPONSES_API) ||
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") {
if (value.trim()) {
return value.trim();
}
continue;
}
if (value !== undefined && value !== null) {
return value;
}
}
return undefined;
}
function concreteBaseUrl(value: unknown, fallback: string): string {
return normalizeOptionalString(value) ?? fallback;
}
function resolveOpenAIEnvironmentBaseUrl(
context: Pick<ProviderResolveModelRoutesContext, "env">,
): string | undefined {
return (context.env ?? process.env).OPENAI_BASE_URL;
}
function isHttpBaseUrl(baseUrl: unknown): boolean {
if (typeof baseUrl !== "string") {
return false;
}
try {
return new URL(baseUrl.trim()).protocol === "http:";
} catch {
return false;
}
}
function codexCanReproduceRoute(
candidate: ProviderModelRouteCandidate,
sourceBaseUrl: unknown = candidate.baseUrl,
): boolean {
// Official HTTP ChatGPT input normalizes to the native HTTPS candidate. Retain the source
// protocol here so normalization cannot silently make an unreproducible route Codex-compatible.
if (isHttpBaseUrl(sourceBaseUrl) || candidate.requestTransportOverrides === "present") {
return false;
}
const endpointKind = classifyOpenAIBaseUrl(candidate.baseUrl);
return (
(candidate.api === OPENAI_RESPONSES_API && endpointKind === "platform") ||
(candidate.api === OPENAI_CHATGPT_RESPONSES_API && endpointKind === "chatgpt")
);
}
function withRuntimePolicy(
candidate: ProviderModelRouteCandidate,
sourceBaseUrl: unknown = candidate.baseUrl,
): ProviderModelRouteCandidate {
return {
...candidate,
runtimePolicy: {
compatibleIds: codexCanReproduceRoute(candidate, sourceBaseUrl)
? CODEX_RUNTIME_COMPATIBLE_IDS
: OPENCLAW_RUNTIME_COMPATIBLE_IDS,
},
};
}
function defaultRuntimeIdForRoute(
candidate: ProviderModelRouteCandidate,
sourceBaseUrl: unknown = candidate.baseUrl,
): string {
return codexCanReproduceRoute(candidate, sourceBaseUrl)
? CODEX_AGENT_RUNTIME_ID
: OPENAI_AGENT_RUNTIME_ID;
}
function route(
candidate: ProviderModelRouteCandidate,
sourceBaseUrl?: unknown,
): ProviderModelRouteResolution & { kind: "routes" } {
const compatibleCandidate = withRuntimePolicy(candidate, sourceBaseUrl);
return {
kind: "routes",
routes: [compatibleCandidate],
defaultRuntimeId: defaultRuntimeIdForRoute(compatibleCandidate, sourceBaseUrl),
};
}
/**
* Resolves OpenAI transport policy in provider-default order.
*
* Candidate order is not credential order. Callers must honor a locked profile,
* provider auth, then auth.order before choosing a compatible candidate. Unknown
* models without route facts remain indeterminate until a catalog row is observed.
*/
function resolveSingleObservedModelRoute(
context: OpenAIResolveSingleModelRouteContext,
): ProviderModelRouteResolution {
if (context.provider.trim().toLowerCase() !== "openai") {
return {
kind: "incompatible",
code: "openai-route-provider-mismatch",
message: `OpenAI route policy cannot resolve provider ${context.provider || "(empty)"}.`,
};
}
const modelApi = normalizeOptionalRouteApi(context.configuredModel?.api);
const requestTransportOverrides = context.requestTransportOverrides ?? "none";
const providerApi = normalizeOptionalRouteApi(context.configuredProvider?.api);
const modelBaseUrl = firstRouteBaseUrl(context.configuredModel?.baseUrl);
const providerBaseUrl = firstRouteBaseUrl(context.configuredProvider?.baseUrl);
const environmentBaseUrl = firstRouteBaseUrl(resolveOpenAIEnvironmentBaseUrl(context));
const observedApi = normalizeOptionalRouteApi(context.observed?.api);
const observedBaseUrl = firstRouteBaseUrl(context.observed?.baseUrl);
const hasObservedRoute = observedApi !== undefined || observedBaseUrl !== undefined;
let effectiveApi: ModelApi | undefined;
let effectiveBaseUrl: unknown;
let configuredRoute = false;
let customDefaultApi: ModelApi = OPENAI_COMPLETIONS_API;
// Model facts override provider facts field-by-field, which override the environment.
// Observed rows are atomic fallback only; custom bases may inherit a lower
// authored adapter without combining contradictory official transports.
if (modelApi !== undefined || modelBaseUrl !== undefined) {
configuredRoute = true;
effectiveApi = modelApi ?? providerApi;
effectiveBaseUrl = modelBaseUrl;
if (modelBaseUrl === undefined) {
const lowerBaseUrl = providerBaseUrl ?? environmentBaseUrl;
const lowerEndpointKind = classifyOpenAIBaseUrl(lowerBaseUrl);
effectiveBaseUrl =
lowerEndpointKind === "custom" || lowerEndpointKind === "invalid"
? lowerBaseUrl
: undefined;
}
} else if (providerApi !== undefined || providerBaseUrl !== undefined) {
configuredRoute = true;
effectiveApi = providerApi;
effectiveBaseUrl = providerBaseUrl;
if (providerBaseUrl === undefined) {
const environmentEndpointKind = classifyOpenAIBaseUrl(environmentBaseUrl);
if (environmentEndpointKind === "custom" || environmentEndpointKind === "invalid") {
effectiveBaseUrl = environmentBaseUrl;
}
}
} else if (environmentBaseUrl !== undefined) {
configuredRoute = true;
effectiveBaseUrl = environmentBaseUrl;
customDefaultApi = OPENAI_RESPONSES_API;
} else {
effectiveApi = observedApi;
effectiveBaseUrl = observedBaseUrl;
}
const endpointKind = classifyOpenAIBaseUrl(effectiveBaseUrl);
if (endpointKind === "invalid") {
return {
kind: "incompatible",
code: "invalid-openai-base-url",
message: "OpenAI model route baseUrl must be a non-empty URL string.",
};
}
const chatGPTApi = effectiveApi?.toLowerCase() === OPENAI_CHATGPT_RESPONSES_API;
const authoredChatGPTApi =
modelApi?.toLowerCase() === OPENAI_CHATGPT_RESPONSES_API ||
providerApi?.toLowerCase() === OPENAI_CHATGPT_RESPONSES_API;
// A custom endpoint owns its protocol contract. Subscription egress always
// requires authored ChatGPT intent; observed Platform adapters remain safe
// API-key fallbacks for otherwise unspecified custom routes.
if (endpointKind === "custom") {
if (chatGPTApi && !authoredChatGPTApi) {
return {
kind: "incompatible",
code: "custom-chatgpt-relay-requires-configuration",
message: "Custom ChatGPT relays require an explicitly configured ChatGPT adapter.",
};
}
// An independently authored custom endpoint may reuse only observed
// Platform adapters. Requiring authored ChatGPT intent prevents a stale
// catalog row from redirecting a subscription bearer to that endpoint.
const observedPlatformApi =
observedApi === OPENAI_RESPONSES_API || observedApi === OPENAI_COMPLETIONS_API
? observedApi
: undefined;
const customApi = effectiveApi ?? observedPlatformApi ?? customDefaultApi;
if (
customApi !== OPENAI_RESPONSES_API &&
customApi !== OPENAI_COMPLETIONS_API &&
customApi !== OPENAI_CHATGPT_RESPONSES_API
) {
return {
kind: "incompatible",
code: "unsupported-custom-openai-api",
message: `${customApi} is not an OpenAI-compatible model adapter.`,
};
}
const customAuthRequirement =
customApi.toLowerCase() === OPENAI_CHATGPT_RESPONSES_API ? "subscription" : "api-key";
return route(
{
api: customApi,
baseUrl: concreteBaseUrl(effectiveBaseUrl, OPENAI_API_BASE_URL),
authRequirement: customAuthRequirement,
requestTransportOverrides,
},
effectiveBaseUrl,
);
}
if (
(endpointKind === "platform" && chatGPTApi) ||
(endpointKind === "chatgpt" && effectiveApi !== undefined && !chatGPTApi)
) {
return {
kind: "incompatible",
code: "conflicting-official-openai-route",
message: "OpenAI model API and baseUrl select different official transports.",
};
}
if (
effectiveApi !== undefined &&
effectiveApi !== OPENAI_RESPONSES_API &&
effectiveApi !== OPENAI_COMPLETIONS_API &&
effectiveApi !== OPENAI_CHATGPT_RESPONSES_API
) {
return {
kind: "incompatible",
code: "unsupported-official-openai-api",
message: `${effectiveApi} is not an OpenAI Platform model adapter.`,
};
}
const modelId = normalizeOpenAIModelRouteId(context.modelId);
const sourceBaseUrl = effectiveBaseUrl;
// An authored Completions adapter is a concrete transport contract, not an
// alias for Responses. Codex does not execute that adapter, so preserve it
// and let the OpenClaw runtime own the request.
const platformApi =
configuredRoute && effectiveApi === OPENAI_COMPLETIONS_API
? OPENAI_COMPLETIONS_API
: OPENAI_RESPONSES_API;
const platformRoute = withRuntimePolicy(
{
api: platformApi,
baseUrl:
classifyOpenAIBaseUrl(sourceBaseUrl) === "platform" && isHttpBaseUrl(sourceBaseUrl)
? concreteBaseUrl(sourceBaseUrl, OPENAI_API_BASE_URL)
: OPENAI_API_BASE_URL,
authRequirement: "api-key",
requestTransportOverrides,
},
sourceBaseUrl,
);
const chatGPTRoute = withRuntimePolicy(
{
api: OPENAI_CHATGPT_RESPONSES_API,
baseUrl: OPENAI_CODEX_RESPONSES_BASE_URL,
authRequirement: "subscription",
requestTransportOverrides,
},
sourceBaseUrl,
);
const platformOnly = isOpenAIPlatformOnlyRouteModelId(modelId);
const subscriptionOnly = isOpenAISubscriptionOnlyRouteModelId(modelId);
const dualRoute = isOpenAIDualRouteModelId(modelId);
// Observed catalog transport is not authored route intent. Known model
// contracts stay stable regardless of which official sibling row was seen.
if (!configuredRoute) {
if (subscriptionOnly) {
return route(chatGPTRoute, sourceBaseUrl);
}
if (platformOnly) {
return route(platformRoute, sourceBaseUrl);
}
if (dualRoute) {
return {
kind: "routes",
defaultRuntimeId: defaultRuntimeIdForRoute(platformRoute, sourceBaseUrl),
routes: [platformRoute, chatGPTRoute],
};
}
}
if (endpointKind === "chatgpt" || chatGPTApi) {
if (platformOnly) {
return {
kind: "incompatible",
code: "platform-only-model-on-chatgpt",
message: `${modelId} is available only through OpenAI Platform API-key authentication.`,
};
}
return route(chatGPTRoute, sourceBaseUrl);
}
if (subscriptionOnly) {
return {
kind: "incompatible",
code: "subscription-only-model-on-platform",
message: `${modelId} is available only through ChatGPT subscription authentication.`,
};
}
if (!configuredRoute && !hasObservedRoute) {
return {
kind: "indeterminate",
defaultRuntimeId:
requestTransportOverrides === "present" ? OPENAI_AGENT_RUNTIME_ID : CODEX_AGENT_RUNTIME_ID,
};
}
return route(platformRoute, sourceBaseUrl);
}
function hasAuthoredRouteFacts(context: ProviderResolveModelRoutesContext): boolean {
return (
normalizeOptionalRouteApi(context.configuredModel?.api) !== undefined ||
firstRouteBaseUrl(context.configuredModel?.baseUrl) !== undefined ||
normalizeOptionalRouteApi(context.configuredProvider?.api) !== undefined ||
firstRouteBaseUrl(context.configuredProvider?.baseUrl) !== undefined ||
firstRouteBaseUrl(resolveOpenAIEnvironmentBaseUrl(context)) !== undefined
);
}
function authoredRouteNeedsObservedPlatformApi(
context: ProviderResolveModelRoutesContext,
): boolean {
// Observations may fill only the missing protocol for an authored custom
// endpoint. Complete authored routes must stay isolated from catalog rows.
if (
normalizeOptionalRouteApi(context.configuredModel?.api) !== undefined ||
normalizeOptionalRouteApi(context.configuredProvider?.api) !== undefined
) {
return false;
}
const authoredBaseUrl = firstRouteBaseUrl(
context.configuredModel?.baseUrl,
context.configuredProvider?.baseUrl,
resolveOpenAIEnvironmentBaseUrl(context),
);
return classifyOpenAIBaseUrl(authoredBaseUrl) === "custom";
}
function canonicalRouteCandidateBaseUrl(baseUrl: string): string {
// Catalog rows may spell one endpoint differently. A canonical grouping key
// prevents observation order from creating a false route ambiguity.
try {
const url = new URL(baseUrl);
url.pathname = url.pathname.replace(/\/+$/u, "") || "/";
return url.toString();
} catch {
return baseUrl;
}
}
function routeCandidateKey(candidate: ProviderModelRouteCandidate): string {
return [
candidate.api,
canonicalRouteCandidateBaseUrl(candidate.baseUrl),
candidate.authRequirement,
candidate.requestTransportOverrides,
...(candidate.runtimePolicy?.compatibleIds ?? []),
].join("\u0000");
}
function compareRouteCandidates(
a: ProviderModelRouteCandidate,
b: ProviderModelRouteCandidate,
): number {
const authOrder = (candidate: ProviderModelRouteCandidate) =>
candidate.authRequirement === "api-key" ? 0 : 1;
return (
authOrder(a) - authOrder(b) || a.api.localeCompare(b.api) || a.baseUrl.localeCompare(b.baseUrl)
);
}
function ambiguousObservedRouteGroup(
message: string,
): Extract<ProviderModelRouteResolution, { kind: "incompatible" }> {
return { kind: "incompatible", code: "ambiguous-openai-route-group", message };
}
function resolveAuthoredObservedFallback(observedRoutes: readonly ProviderModelRouteSource[]):
| { kind: "observed"; route?: ProviderModelRouteSource }
| {
kind: "incompatible";
resolution: Extract<ProviderModelRouteResolution, { kind: "incompatible" }>;
} {
const platformApis = new Set<ModelApi>();
for (const observed of observedRoutes) {
const api = normalizeOptionalRouteApi(observed.api);
if (!api || api === OPENAI_CHATGPT_RESPONSES_API) {
continue;
}
if (api !== OPENAI_RESPONSES_API && api !== OPENAI_COMPLETIONS_API) {
return {
kind: "incompatible",
resolution: {
kind: "incompatible",
code: "unsupported-custom-openai-api",
message: `${api} is not an OpenAI-compatible model adapter.`,
},
};
}
platformApis.add(api);
}
if (platformApis.size > 1) {
return {
kind: "incompatible",
resolution: ambiguousObservedRouteGroup(
"Observed OpenAI routes disagree on the Platform adapter for an authored endpoint.",
),
};
}
const api = [...platformApis][0];
return { kind: "observed", ...(api ? { route: { api } } : {}) };
}
/** Resolves every physical row for one logical OpenAI model in provider order. */
export function resolveModelRoutes(
context: ProviderResolveModelRoutesContext,
): ProviderModelRouteResolution {
const observedRoutes = (context.observedRoutes ?? []).filter(
(observed) => observed.api != null || observed.baseUrl != null,
);
if (hasAuthoredRouteFacts(context)) {
if (authoredRouteNeedsObservedPlatformApi(context)) {
const fallback = resolveAuthoredObservedFallback(observedRoutes);
if (fallback.kind === "incompatible") {
return fallback.resolution;
}
return resolveSingleObservedModelRoute({ ...context, observed: fallback.route });
}
return resolveSingleObservedModelRoute(context);
}
if (observedRoutes.length <= 1) {
return resolveSingleObservedModelRoute({ ...context, observed: observedRoutes[0] });
}
const resolutions = observedRoutes.map((observed) =>
resolveSingleObservedModelRoute({ ...context, observed }),
);
const incompatible = resolutions
.filter((resolution) => resolution.kind === "incompatible")
.toSorted((a, b) => a.code.localeCompare(b.code) || a.message.localeCompare(b.message))[0];
if (incompatible) {
return incompatible;
}
const routesByKey = new Map<string, ProviderModelRouteCandidate>();
for (const resolution of resolutions) {
if (resolution.kind !== "routes") {
continue;
}
for (const candidate of resolution.routes) {
const key = routeCandidateKey(candidate);
const existing = routesByKey.get(key);
if (!existing || candidate.baseUrl.localeCompare(existing.baseUrl) < 0) {
routesByKey.set(key, candidate);
}
}
}
const routes = [...routesByKey.values()].toSorted(compareRouteCandidates);
const authRequirements = new Set(routes.map((candidate) => candidate.authRequirement));
if (routes.length > authRequirements.size) {
return ambiguousObservedRouteGroup(
"Observed OpenAI routes contain multiple endpoints for the same authentication class.",
);
}
const firstRoute = routes[0];
if (!firstRoute) {
return resolveSingleObservedModelRoute(context);
}
return {
kind: "routes",
routes: routes as [ProviderModelRouteCandidate, ...ProviderModelRouteCandidate[]],
defaultRuntimeId: resolutions.some(
(resolution) => resolution.kind === "routes" && resolution.defaultRuntimeId === "openclaw",
)
? OPENAI_AGENT_RUNTIME_ID
: defaultRuntimeIdForRoute(firstRoute),
};
}
export function normalizeConfig(params: { provider: string; providerConfig: ModelProviderConfig }) {
return params.providerConfig;
}
export function resolveThinkingProfile(params: ProviderDefaultThinkingPolicyContext) {
switch (params.provider.trim().toLowerCase()) {
case "openai":
return resolveUnifiedOpenAIThinkingProfile(
params.modelId,
params.agentRuntime,
params.compat,
params.api,
);
default:
return null;
}
}