Skip to content

Commit 20b05f2

Browse files
committed
fix: expose codex provider catalog
1 parent 0585e18 commit 20b05f2

22 files changed

Lines changed: 624 additions & 171 deletions

extensions/codex/openclaw.plugin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"name": "Codex",
44
"description": "Codex app-server harness and Codex-managed GPT model catalog.",
55
"providers": ["codex"],
6+
"providerDiscoveryEntry": "./provider-discovery.ts",
7+
"syntheticAuthRefs": ["codex"],
68
"nonSecretAuthMarkers": ["codex-app-server"],
79
"activation": {
810
"onAgentHarnesses": ["codex"]
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type {
2+
ModelDefinitionConfig,
3+
ModelProviderConfig,
4+
} from "openclaw/plugin-sdk/provider-model-shared";
5+
import type { CodexAppServerModel } from "./src/app-server/models.js";
6+
7+
export const CODEX_PROVIDER_ID = "codex";
8+
export const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
9+
export const CODEX_APP_SERVER_AUTH_MARKER = "codex-app-server";
10+
11+
const DEFAULT_CONTEXT_WINDOW = 272_000;
12+
const DEFAULT_MAX_TOKENS = 128_000;
13+
14+
export const FALLBACK_CODEX_MODELS = [
15+
{
16+
id: "gpt-5.4",
17+
model: "gpt-5.4",
18+
displayName: "gpt-5.4",
19+
description: "Latest frontier agentic coding model.",
20+
isDefault: true,
21+
inputModalities: ["text", "image"],
22+
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
23+
},
24+
{
25+
id: "gpt-5.4-mini",
26+
model: "gpt-5.4-mini",
27+
displayName: "GPT-5.4-Mini",
28+
description: "Smaller frontier agentic coding model.",
29+
inputModalities: ["text", "image"],
30+
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
31+
},
32+
{
33+
id: "gpt-5.2",
34+
model: "gpt-5.2",
35+
displayName: "gpt-5.2",
36+
inputModalities: ["text", "image"],
37+
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
38+
},
39+
] satisfies CodexAppServerModel[];
40+
41+
export function buildCodexModelDefinition(model: {
42+
id: string;
43+
model: string;
44+
displayName?: string;
45+
inputModalities: string[];
46+
supportedReasoningEfforts: string[];
47+
}): ModelDefinitionConfig {
48+
const id = model.id.trim() || model.model.trim();
49+
return {
50+
id,
51+
name: model.displayName?.trim() || id,
52+
api: "openai-codex-responses",
53+
reasoning: model.supportedReasoningEfforts.length > 0 || shouldDefaultToReasoningModel(id),
54+
input: model.inputModalities.includes("image") ? ["text", "image"] : ["text"],
55+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
56+
contextWindow: DEFAULT_CONTEXT_WINDOW,
57+
maxTokens: DEFAULT_MAX_TOKENS,
58+
compat: {
59+
supportsReasoningEffort: model.supportedReasoningEfforts.length > 0,
60+
supportsUsageInStreaming: true,
61+
},
62+
};
63+
}
64+
65+
export function buildCodexProviderConfig(models: CodexAppServerModel[]): ModelProviderConfig {
66+
return {
67+
baseUrl: CODEX_BASE_URL,
68+
apiKey: CODEX_APP_SERVER_AUTH_MARKER,
69+
auth: "token",
70+
api: "openai-codex-responses",
71+
models: models.map(buildCodexModelDefinition),
72+
};
73+
}
74+
75+
function shouldDefaultToReasoningModel(modelId: string): boolean {
76+
const lower = modelId.toLowerCase();
77+
return (
78+
lower.startsWith("gpt-5") ||
79+
lower.startsWith("o1") ||
80+
lower.startsWith("o3") ||
81+
lower.startsWith("o4")
82+
);
83+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
2+
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
3+
import {
4+
buildCodexProviderConfig,
5+
CODEX_APP_SERVER_AUTH_MARKER,
6+
CODEX_PROVIDER_ID,
7+
FALLBACK_CODEX_MODELS,
8+
} from "./provider-catalog.js";
9+
10+
function resolveCodexPluginConfig(ctx: ProviderCatalogContext): unknown {
11+
return (ctx.config.plugins?.entries as Record<string, { config?: unknown } | undefined>)?.codex
12+
?.config;
13+
}
14+
15+
async function runCodexCatalog(ctx: ProviderCatalogContext) {
16+
const { buildCodexProviderCatalog } = await import("./provider.js");
17+
return await buildCodexProviderCatalog({
18+
env: ctx.env,
19+
pluginConfig: resolveCodexPluginConfig(ctx),
20+
});
21+
}
22+
23+
export const codexProviderDiscovery: ProviderPlugin = {
24+
id: CODEX_PROVIDER_ID,
25+
label: "Codex",
26+
docsPath: "/providers/models",
27+
auth: [],
28+
catalog: {
29+
order: "late",
30+
run: runCodexCatalog,
31+
},
32+
staticCatalog: {
33+
order: "late",
34+
run: async () => ({
35+
provider: buildCodexProviderConfig(FALLBACK_CODEX_MODELS),
36+
}),
37+
},
38+
resolveSyntheticAuth: () => ({
39+
apiKey: CODEX_APP_SERVER_AUTH_MARKER,
40+
source: "codex-app-server",
41+
mode: "token",
42+
}),
43+
};
44+
45+
export default codexProviderDiscovery;

extensions/codex/provider.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { afterEach, describe, expect, it, vi } from "vitest";
22
import { CODEX_GPT5_BEHAVIOR_CONTRACT } from "./prompt-overlay.js";
3+
import { codexProviderDiscovery } from "./provider-discovery.js";
34
import { buildCodexProvider, buildCodexProviderCatalog } from "./provider.js";
45
import { CodexAppServerClient } from "./src/app-server/client.js";
56
import {
@@ -178,6 +179,25 @@ describe("codex provider", () => {
178179
});
179180
});
180181

182+
it("exposes a lightweight provider-discovery entry for model list/status", async () => {
183+
expect(codexProviderDiscovery.id).toBe("codex");
184+
expect(codexProviderDiscovery.resolveSyntheticAuth?.({ provider: "codex" })).toEqual({
185+
apiKey: "codex-app-server",
186+
source: "codex-app-server",
187+
mode: "token",
188+
});
189+
190+
const result = await codexProviderDiscovery.staticCatalog?.run({
191+
config: {},
192+
env: {},
193+
agentDir: "/tmp/openclaw-agent",
194+
} as never);
195+
196+
expect(
197+
result && "provider" in result ? result.provider.models.map((model) => model.id) : [],
198+
).toEqual(["gpt-5.4", "gpt-5.4-mini", "gpt-5.2"]);
199+
});
200+
181201
it("adds the GPT-5 prompt overlay to Codex provider runs", () => {
182202
const provider = buildCodexProvider();
183203

extensions/codex/provider.ts

Lines changed: 34 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
22
import {
33
normalizeModelCompat,
4-
type ModelDefinitionConfig,
54
type ModelProviderConfig,
65
type ProviderPlugin,
76
} from "openclaw/plugin-sdk/provider-model-shared";
8-
import {
9-
listCodexAppServerModels,
10-
type CodexAppServerModel,
11-
type CodexAppServerModelListResult,
12-
} from "./harness.js";
137
import { resolveCodexSystemPromptContribution } from "./prompt-overlay.js";
8+
import {
9+
buildCodexModelDefinition,
10+
buildCodexProviderConfig,
11+
CODEX_APP_SERVER_AUTH_MARKER,
12+
CODEX_BASE_URL,
13+
CODEX_PROVIDER_ID,
14+
FALLBACK_CODEX_MODELS,
15+
} from "./provider-catalog.js";
1416
import {
1517
type CodexAppServerStartOptions,
1618
readCodexPluginConfig,
1719
resolveCodexAppServerRuntimeOptions,
1820
} from "./src/app-server/config.js";
21+
import type {
22+
CodexAppServerModel,
23+
CodexAppServerModelListResult,
24+
} from "./src/app-server/models.js";
1925

20-
const PROVIDER_ID = "codex";
21-
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
22-
const DEFAULT_CONTEXT_WINDOW = 272_000;
23-
const DEFAULT_MAX_TOKENS = 128_000;
2426
const DEFAULT_DISCOVERY_TIMEOUT_MS = 2500;
2527
const LIVE_DISCOVERY_ENV = "OPENCLAW_CODEX_DISCOVERY_LIVE";
2628

@@ -42,36 +44,9 @@ type BuildCatalogOptions = {
4244
listModels?: CodexModelLister;
4345
};
4446

45-
const FALLBACK_CODEX_MODELS = [
46-
{
47-
id: "gpt-5.4",
48-
model: "gpt-5.4",
49-
displayName: "gpt-5.4",
50-
description: "Latest frontier agentic coding model.",
51-
isDefault: true,
52-
inputModalities: ["text", "image"],
53-
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
54-
},
55-
{
56-
id: "gpt-5.4-mini",
57-
model: "gpt-5.4-mini",
58-
displayName: "GPT-5.4-Mini",
59-
description: "Smaller frontier agentic coding model.",
60-
inputModalities: ["text", "image"],
61-
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
62-
},
63-
{
64-
id: "gpt-5.2",
65-
model: "gpt-5.2",
66-
displayName: "gpt-5.2",
67-
inputModalities: ["text", "image"],
68-
supportedReasoningEfforts: ["low", "medium", "high", "xhigh"],
69-
},
70-
] satisfies CodexAppServerModel[];
71-
7247
export function buildCodexProvider(options: BuildCodexProviderOptions = {}): ProviderPlugin {
7348
return {
74-
id: PROVIDER_ID,
49+
id: CODEX_PROVIDER_ID,
7550
label: "Codex",
7651
docsPath: "/providers/models",
7752
auth: [],
@@ -84,9 +59,15 @@ export function buildCodexProvider(options: BuildCodexProviderOptions = {}): Pro
8459
listModels: options.listModels,
8560
}),
8661
},
62+
staticCatalog: {
63+
order: "late",
64+
run: async () => ({
65+
provider: buildCodexProviderConfig(FALLBACK_CODEX_MODELS),
66+
}),
67+
},
8768
resolveDynamicModel: (ctx) => resolveCodexDynamicModel(ctx.modelId),
8869
resolveSyntheticAuth: () => ({
89-
apiKey: "codex-app-server",
70+
apiKey: CODEX_APP_SERVER_AUTH_MARKER,
9071
source: "codex-app-server",
9172
mode: "token",
9273
}),
@@ -115,22 +96,13 @@ export async function buildCodexProviderCatalog(
11596
let discovered: CodexAppServerModel[] = [];
11697
if (config.discovery?.enabled !== false && !shouldSkipLiveDiscovery(options.env)) {
11798
discovered = await listModelsBestEffort({
118-
listModels: options.listModels ?? listCodexAppServerModels,
99+
listModels: options.listModels ?? listCodexAppServerModelsLazy,
119100
timeoutMs,
120101
startOptions: appServer.start,
121102
});
122103
}
123-
const models = (discovered.length > 0 ? discovered : FALLBACK_CODEX_MODELS).map(
124-
codexModelToDefinition,
125-
);
126104
return {
127-
provider: {
128-
baseUrl: CODEX_BASE_URL,
129-
apiKey: "codex-app-server",
130-
auth: "token",
131-
api: "openai-codex-responses",
132-
models,
133-
},
105+
provider: buildCodexProviderConfig(discovered.length > 0 ? discovered : FALLBACK_CODEX_MODELS),
134106
};
135107
}
136108

@@ -140,45 +112,17 @@ function resolveCodexDynamicModel(modelId: string): ProviderRuntimeModel | undef
140112
return undefined;
141113
}
142114
return normalizeModelCompat({
143-
...buildModelDefinition({
115+
...buildCodexModelDefinition({
144116
id,
145117
model: id,
146118
inputModalities: ["text", "image"],
147119
supportedReasoningEfforts: shouldDefaultToReasoningModel(id) ? ["medium"] : [],
148120
}),
149-
provider: PROVIDER_ID,
121+
provider: CODEX_PROVIDER_ID,
150122
baseUrl: CODEX_BASE_URL,
151123
} as ProviderRuntimeModel);
152124
}
153125

154-
function codexModelToDefinition(model: CodexAppServerModel): ModelDefinitionConfig {
155-
return buildModelDefinition(model);
156-
}
157-
158-
function buildModelDefinition(model: {
159-
id: string;
160-
model: string;
161-
displayName?: string;
162-
inputModalities: string[];
163-
supportedReasoningEfforts: string[];
164-
}): ModelDefinitionConfig {
165-
const id = model.id.trim() || model.model.trim();
166-
return {
167-
id,
168-
name: model.displayName?.trim() || id,
169-
api: "openai-codex-responses",
170-
reasoning: model.supportedReasoningEfforts.length > 0 || shouldDefaultToReasoningModel(id),
171-
input: model.inputModalities.includes("image") ? ["text", "image"] : ["text"],
172-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
173-
contextWindow: DEFAULT_CONTEXT_WINDOW,
174-
maxTokens: DEFAULT_MAX_TOKENS,
175-
compat: {
176-
supportsReasoningEffort: model.supportedReasoningEfforts.length > 0,
177-
supportsUsageInStreaming: true,
178-
},
179-
};
180-
}
181-
182126
async function listModelsBestEffort(params: {
183127
listModels: CodexModelLister;
184128
timeoutMs: number;
@@ -197,6 +141,16 @@ async function listModelsBestEffort(params: {
197141
}
198142
}
199143

144+
async function listCodexAppServerModelsLazy(options: {
145+
timeoutMs: number;
146+
limit?: number;
147+
startOptions?: CodexAppServerStartOptions;
148+
sharedClient?: boolean;
149+
}): Promise<CodexAppServerModelListResult> {
150+
const { listCodexAppServerModels } = await import("./src/app-server/models.js");
151+
return listCodexAppServerModels(options);
152+
}
153+
200154
function normalizeTimeoutMs(value: unknown): number {
201155
return typeof value === "number" && Number.isFinite(value) && value > 0
202156
? value

src/cli/command-catalog.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
8383
{
8484
commandPath: ["models", "list"],
8585
exact: true,
86-
policy: { ensureCliPath: false },
86+
policy: { ensureCliPath: false, routeConfigGuard: "always" },
8787
route: { id: "models-list" },
8888
},
8989
{
9090
commandPath: ["models", "status"],
9191
exact: true,
92-
policy: { ensureCliPath: false },
92+
policy: { ensureCliPath: false, routeConfigGuard: "always" },
9393
route: { id: "models-status" },
9494
},
9595
{ commandPath: ["backup"], policy: { bypassConfigGuard: true } },

0 commit comments

Comments
 (0)