Files
openclaw/extensions/codex/src/app-server/thread-model-selection.ts
Peter Steinberger 3c8269ca52 refactor(codex)!: fold the codex text provider into openai with a doctor migration
The live codex text provider was a redundant projection of the openai
catalog (exclusive provider ownership; the openai plugin's ChatGPT OAuth
discovery already serves gpt-5.6-* route-aware). Folding it:

- extensions/codex no longer registers a text provider, catalog entry, or
  synthetic text auth; provider.ts/provider-catalog.ts/provider-discovery.ts
  and the route-blind model-name heuristics are deleted; the narrow
  post-harness reasoning fallback moves to an app-server-owned module
- openai thinking policy keys on explicit selected-route provenance
  (api === openai-chatgpt-responses) instead of value-shape inference
- models.list gains an optional additive agentRuntime field (configured
  intent); session agentHarnessId remains the execution proof
- doctor --fix migrates the shipped codex/* config shape end to end:
  every model slot, provider-config merge with blocker-aware conflict
  handling, sessions, cron payloads (two-phase: runtime policy persists
  before cron refs rewrite), transcripts; migrated refs carry model-scoped
  agentRuntime.id=codex preserving the shipped wizard semantics; auto
  runtime policies normalize to codex with sibling fields preserved;
  blocked provider conflicts retain the whole legacy namespace fail-closed
  with an actionable warning
- the stale openai:default profile cleanup (#91352) was deliberately
  deferred to a follow-up after review showed it needs per-agent identity
  proofs; doctor keeps warning about unusable profiles

Fixes #105561
Fixes #84637
Fixes #90420
2026-07-16 03:06:43 +01:00

153 lines
5.5 KiB
TypeScript

import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
import {
resolveCodexAppServerReasoningEffort,
type CodexReasoningEffort,
} from "./reasoning-effort.js";
import {
isCodexAppServerNativeAuthProfile,
type CodexAppServerAuthProfileLookup,
type CodexAppServerThreadBinding,
} from "./session-binding.js";
export const CODEX_NATIVE_PERSONALITY_NONE = "none";
export function resolveCodexBindingModelProviderFallback(params: {
provider?: string;
currentModel: string | undefined;
bindingModel: string | undefined;
bindingModelProvider: string | undefined;
}): string | undefined {
const provider = params.provider?.trim().toLowerCase();
if (provider && provider !== "codex") {
return undefined;
}
const currentModel = params.currentModel?.trim();
const bindingModel = params.bindingModel?.trim();
if (
currentModel &&
bindingModel &&
currentModel === bindingModel &&
params.bindingModelProvider
) {
return params.bindingModelProvider;
}
return hasProviderQualifiedModelRef(currentModel) ? undefined : params.bindingModelProvider;
}
export function resolveCodexAppServerThreadModelSelection(params: {
provider: string;
model: string;
binding?: Pick<
CodexAppServerThreadBinding,
"threadId" | "authProfileId" | "model" | "modelProvider"
>;
authProfileId?: string;
authProfileStore?: CodexAppServerAuthProfileLookup["authProfileStore"];
agentDir?: string;
config?: CodexAppServerAuthProfileLookup["config"];
}): { model: string; modelProvider?: string } {
const authProfileId = params.authProfileId ?? params.binding?.authProfileId;
const explicitModelProvider = resolveCodexAppServerModelProvider({
provider: params.provider,
authProfileId,
authProfileStore: params.authProfileStore,
agentDir: params.agentDir,
config: params.config,
});
const bindingModelProvider = params.binding?.threadId
? resolveCodexBindingModelProviderFallback({
provider: params.provider,
currentModel: params.model,
bindingModel: params.binding.model,
bindingModelProvider: params.binding.modelProvider,
})
: undefined;
return resolveCodexAppServerRequestModelSelection({
model: params.model,
modelProvider: explicitModelProvider ?? bindingModelProvider,
authProfileId,
authProfileStore: params.authProfileStore,
agentDir: params.agentDir,
config: params.config,
});
}
export function resolveCodexAppServerRequestModelSelection(params: {
model: string;
modelProvider?: string | null;
authProfileId?: string;
authProfileStore?: CodexAppServerAuthProfileLookup["authProfileStore"];
agentDir?: string;
config?: CodexAppServerAuthProfileLookup["config"];
}): { model: string; modelProvider?: string } {
const model = params.model.trim();
const modelProvider = params.modelProvider?.trim();
if (modelProvider) {
return { model, modelProvider };
}
// Codex app-server expects provider-qualified refs as separate fields. Keep
// explicit providers intact so provider-owned slashy model ids are not split.
const slashIndex = model.indexOf("/");
if (slashIndex <= 0 || slashIndex >= model.length - 1) {
return { model };
}
const inferredProvider = model.slice(0, slashIndex);
const inferredModelProvider = resolveCodexAppServerModelProvider({
provider: inferredProvider,
authProfileId: params.authProfileId,
authProfileStore: params.authProfileStore,
agentDir: params.agentDir,
config: params.config,
});
return {
model: model.slice(slashIndex + 1).trim(),
...(inferredModelProvider ? { modelProvider: inferredModelProvider } : {}),
};
}
function hasProviderQualifiedModelRef(model: string | undefined): boolean {
const trimmed = model?.trim();
const slashIndex = trimmed?.indexOf("/") ?? -1;
return slashIndex > 0 && slashIndex < (trimmed?.length ?? 0) - 1;
}
export function resolveCodexAppServerModelProvider(params: {
provider: string;
authProfileId?: string;
authProfileStore?: CodexAppServerAuthProfileLookup["authProfileStore"];
agentDir?: string;
config?: CodexAppServerAuthProfileLookup["config"];
}): string | undefined {
const normalized = params.provider.trim();
const normalizedLower = normalized.toLowerCase();
if (!normalized || normalizedLower === "codex") {
// `codex` is OpenClaw's virtual provider; let Codex app-server keep its
// native provider/auth selection instead of forcing the legacy OpenAI path.
return undefined;
}
if (isCodexAppServerNativeAuthProfile(params) && normalizedLower === "openai") {
// When OpenClaw is forwarding ChatGPT/Codex OAuth, `openai` is Codex's
// native provider id, not a public OpenAI API-key choice. Omit the override
// so app-server keeps its configured provider/auth pair for this session.
return undefined;
}
return normalizedLower === "openai" ? "openai" : normalized;
}
// Modern Codex models reject the legacy CLI `minimal` default. Prefer
// app-server metadata, then use the app-server-owned fallback effort contract
// for Pro models whose minimum supported effort is `medium`.
// Other modern models translate `minimal` to `low`. (#71946)
// Exported for unit-test coverage of the model-aware translation path.
export function resolveReasoningEffort(
thinkLevel: EmbeddedRunAttemptParams["thinkLevel"] | "ultra",
modelId: string,
supportedReasoningEfforts?: readonly string[],
): CodexReasoningEffort | null {
return resolveCodexAppServerReasoningEffort({
thinkLevel,
modelId,
supportedReasoningEfforts,
});
}