fix: expose codex provider catalog

This commit is contained in:
Peter Steinberger
2026-04-23 05:34:44 +01:00
parent 0585e181f8
commit 20b05f220e
22 changed files with 624 additions and 171 deletions

View File

@@ -3,6 +3,8 @@
"name": "Codex",
"description": "Codex app-server harness and Codex-managed GPT model catalog.",
"providers": ["codex"],
"providerDiscoveryEntry": "./provider-discovery.ts",
"syntheticAuthRefs": ["codex"],
"nonSecretAuthMarkers": ["codex-app-server"],
"activation": {
"onAgentHarnesses": ["codex"]

View File

@@ -0,0 +1,83 @@
import type {
ModelDefinitionConfig,
ModelProviderConfig,
} from "openclaw/plugin-sdk/provider-model-shared";
import type { CodexAppServerModel } from "./src/app-server/models.js";
export const CODEX_PROVIDER_ID = "codex";
export const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
export const CODEX_APP_SERVER_AUTH_MARKER = "codex-app-server";
const DEFAULT_CONTEXT_WINDOW = 272_000;
const DEFAULT_MAX_TOKENS = 128_000;
export const FALLBACK_CODEX_MODELS = [
{
id: "gpt-5.4",
model: "gpt-5.4",
displayName: "gpt-5.4",
description: "Latest frontier agentic coding model.",
isDefault: true,
inputModalities: ["text", "image"],
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
},
{
id: "gpt-5.4-mini",
model: "gpt-5.4-mini",
displayName: "GPT-5.4-Mini",
description: "Smaller frontier agentic coding model.",
inputModalities: ["text", "image"],
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
},
{
id: "gpt-5.2",
model: "gpt-5.2",
displayName: "gpt-5.2",
inputModalities: ["text", "image"],
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
},
] satisfies CodexAppServerModel[];
export function buildCodexModelDefinition(model: {
id: string;
model: string;
displayName?: string;
inputModalities: string[];
supportedReasoningEfforts: string[];
}): ModelDefinitionConfig {
const id = model.id.trim() || model.model.trim();
return {
id,
name: model.displayName?.trim() || id,
api: "openai-codex-responses",
reasoning: model.supportedReasoningEfforts.length > 0 || shouldDefaultToReasoningModel(id),
input: model.inputModalities.includes("image") ? ["text", "image"] : ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
compat: {
supportsReasoningEffort: model.supportedReasoningEfforts.length > 0,
supportsUsageInStreaming: true,
},
};
}
export function buildCodexProviderConfig(models: CodexAppServerModel[]): ModelProviderConfig {
return {
baseUrl: CODEX_BASE_URL,
apiKey: CODEX_APP_SERVER_AUTH_MARKER,
auth: "token",
api: "openai-codex-responses",
models: models.map(buildCodexModelDefinition),
};
}
function shouldDefaultToReasoningModel(modelId: string): boolean {
const lower = modelId.toLowerCase();
return (
lower.startsWith("gpt-5") ||
lower.startsWith("o1") ||
lower.startsWith("o3") ||
lower.startsWith("o4")
);
}

View File

@@ -0,0 +1,45 @@
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import {
buildCodexProviderConfig,
CODEX_APP_SERVER_AUTH_MARKER,
CODEX_PROVIDER_ID,
FALLBACK_CODEX_MODELS,
} from "./provider-catalog.js";
function resolveCodexPluginConfig(ctx: ProviderCatalogContext): unknown {
return (ctx.config.plugins?.entries as Record<string, { config?: unknown } | undefined>)?.codex
?.config;
}
async function runCodexCatalog(ctx: ProviderCatalogContext) {
const { buildCodexProviderCatalog } = await import("./provider.js");
return await buildCodexProviderCatalog({
env: ctx.env,
pluginConfig: resolveCodexPluginConfig(ctx),
});
}
export const codexProviderDiscovery: ProviderPlugin = {
id: CODEX_PROVIDER_ID,
label: "Codex",
docsPath: "/providers/models",
auth: [],
catalog: {
order: "late",
run: runCodexCatalog,
},
staticCatalog: {
order: "late",
run: async () => ({
provider: buildCodexProviderConfig(FALLBACK_CODEX_MODELS),
}),
},
resolveSyntheticAuth: () => ({
apiKey: CODEX_APP_SERVER_AUTH_MARKER,
source: "codex-app-server",
mode: "token",
}),
};
export default codexProviderDiscovery;

View File

@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { CODEX_GPT5_BEHAVIOR_CONTRACT } from "./prompt-overlay.js";
import { codexProviderDiscovery } from "./provider-discovery.js";
import { buildCodexProvider, buildCodexProviderCatalog } from "./provider.js";
import { CodexAppServerClient } from "./src/app-server/client.js";
import {
@@ -178,6 +179,25 @@ describe("codex provider", () => {
});
});
it("exposes a lightweight provider-discovery entry for model list/status", async () => {
expect(codexProviderDiscovery.id).toBe("codex");
expect(codexProviderDiscovery.resolveSyntheticAuth?.({ provider: "codex" })).toEqual({
apiKey: "codex-app-server",
source: "codex-app-server",
mode: "token",
});
const result = await codexProviderDiscovery.staticCatalog?.run({
config: {},
env: {},
agentDir: "/tmp/openclaw-agent",
} as never);
expect(
result && "provider" in result ? result.provider.models.map((model) => model.id) : [],
).toEqual(["gpt-5.4", "gpt-5.4-mini", "gpt-5.2"]);
});
it("adds the GPT-5 prompt overlay to Codex provider runs", () => {
const provider = buildCodexProvider();

View File

@@ -1,26 +1,28 @@
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
import {
normalizeModelCompat,
type ModelDefinitionConfig,
type ModelProviderConfig,
type ProviderPlugin,
} from "openclaw/plugin-sdk/provider-model-shared";
import {
listCodexAppServerModels,
type CodexAppServerModel,
type CodexAppServerModelListResult,
} from "./harness.js";
import { resolveCodexSystemPromptContribution } from "./prompt-overlay.js";
import {
buildCodexModelDefinition,
buildCodexProviderConfig,
CODEX_APP_SERVER_AUTH_MARKER,
CODEX_BASE_URL,
CODEX_PROVIDER_ID,
FALLBACK_CODEX_MODELS,
} from "./provider-catalog.js";
import {
type CodexAppServerStartOptions,
readCodexPluginConfig,
resolveCodexAppServerRuntimeOptions,
} from "./src/app-server/config.js";
import type {
CodexAppServerModel,
CodexAppServerModelListResult,
} from "./src/app-server/models.js";
const PROVIDER_ID = "codex";
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
const DEFAULT_CONTEXT_WINDOW = 272_000;
const DEFAULT_MAX_TOKENS = 128_000;
const DEFAULT_DISCOVERY_TIMEOUT_MS = 2500;
const LIVE_DISCOVERY_ENV = "OPENCLAW_CODEX_DISCOVERY_LIVE";
@@ -42,36 +44,9 @@ type BuildCatalogOptions = {
listModels?: CodexModelLister;
};
const FALLBACK_CODEX_MODELS = [
{
id: "gpt-5.4",
model: "gpt-5.4",
displayName: "gpt-5.4",
description: "Latest frontier agentic coding model.",
isDefault: true,
inputModalities: ["text", "image"],
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
},
{
id: "gpt-5.4-mini",
model: "gpt-5.4-mini",
displayName: "GPT-5.4-Mini",
description: "Smaller frontier agentic coding model.",
inputModalities: ["text", "image"],
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
},
{
id: "gpt-5.2",
model: "gpt-5.2",
displayName: "gpt-5.2",
inputModalities: ["text", "image"],
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
},
] satisfies CodexAppServerModel[];
export function buildCodexProvider(options: BuildCodexProviderOptions = {}): ProviderPlugin {
return {
id: PROVIDER_ID,
id: CODEX_PROVIDER_ID,
label: "Codex",
docsPath: "/providers/models",
auth: [],
@@ -84,9 +59,15 @@ export function buildCodexProvider(options: BuildCodexProviderOptions = {}): Pro
listModels: options.listModels,
}),
},
staticCatalog: {
order: "late",
run: async () => ({
provider: buildCodexProviderConfig(FALLBACK_CODEX_MODELS),
}),
},
resolveDynamicModel: (ctx) => resolveCodexDynamicModel(ctx.modelId),
resolveSyntheticAuth: () => ({
apiKey: "codex-app-server",
apiKey: CODEX_APP_SERVER_AUTH_MARKER,
source: "codex-app-server",
mode: "token",
}),
@@ -115,22 +96,13 @@ export async function buildCodexProviderCatalog(
let discovered: CodexAppServerModel[] = [];
if (config.discovery?.enabled !== false && !shouldSkipLiveDiscovery(options.env)) {
discovered = await listModelsBestEffort({
listModels: options.listModels ?? listCodexAppServerModels,
listModels: options.listModels ?? listCodexAppServerModelsLazy,
timeoutMs,
startOptions: appServer.start,
});
}
const models = (discovered.length > 0 ? discovered : FALLBACK_CODEX_MODELS).map(
codexModelToDefinition,
);
return {
provider: {
baseUrl: CODEX_BASE_URL,
apiKey: "codex-app-server",
auth: "token",
api: "openai-codex-responses",
models,
},
provider: buildCodexProviderConfig(discovered.length > 0 ? discovered : FALLBACK_CODEX_MODELS),
};
}
@@ -140,45 +112,17 @@ function resolveCodexDynamicModel(modelId: string): ProviderRuntimeModel | undef
return undefined;
}
return normalizeModelCompat({
...buildModelDefinition({
...buildCodexModelDefinition({
id,
model: id,
inputModalities: ["text", "image"],
supportedReasoningEfforts: shouldDefaultToReasoningModel(id) ? ["medium"] : [],
}),
provider: PROVIDER_ID,
provider: CODEX_PROVIDER_ID,
baseUrl: CODEX_BASE_URL,
} as ProviderRuntimeModel);
}
function codexModelToDefinition(model: CodexAppServerModel): ModelDefinitionConfig {
return buildModelDefinition(model);
}
function buildModelDefinition(model: {
id: string;
model: string;
displayName?: string;
inputModalities: string[];
supportedReasoningEfforts: string[];
}): ModelDefinitionConfig {
const id = model.id.trim() || model.model.trim();
return {
id,
name: model.displayName?.trim() || id,
api: "openai-codex-responses",
reasoning: model.supportedReasoningEfforts.length > 0 || shouldDefaultToReasoningModel(id),
input: model.inputModalities.includes("image") ? ["text", "image"] : ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
compat: {
supportsReasoningEffort: model.supportedReasoningEfforts.length > 0,
supportsUsageInStreaming: true,
},
};
}
async function listModelsBestEffort(params: {
listModels: CodexModelLister;
timeoutMs: number;
@@ -197,6 +141,16 @@ async function listModelsBestEffort(params: {
}
}
async function listCodexAppServerModelsLazy(options: {
timeoutMs: number;
limit?: number;
startOptions?: CodexAppServerStartOptions;
sharedClient?: boolean;
}): Promise<CodexAppServerModelListResult> {
const { listCodexAppServerModels } = await import("./src/app-server/models.js");
return listCodexAppServerModels(options);
}
function normalizeTimeoutMs(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) && value > 0
? value