diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index d2381d3d..af168f8d 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -738,6 +738,7 @@ export class CodexSecurity { }; const codex = this.#dependencies.createCodex({ ...(apiKey === null ? {} : { apiKey }), + ...(this.config.endpoint ? { baseUrl: this.config.endpoint } : {}), env: definedEnvironment( selectedScanEnvironment(environment, "chatgpt"), ), @@ -1880,6 +1881,7 @@ export function scanPreflightCodexConfig(config: JsonObject): JsonObject { "model_reasoning_effort", "model_provider", "service_tier", + "openai_base_url", ]) { const value = source[key]; if (safeString(value, 512)) result[key] = value; diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 1abe8302..d7e3b644 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -157,6 +157,7 @@ const VALUE_OPTIONS = new Set([ "--mode", "--model", "--effort", + "--endpoint", "--output-dir", "--plugin-path", "--python", @@ -203,6 +204,7 @@ interface ScanArguments { mode: ScanMode; model?: string; effort?: ModelReasoningEffort; + endpoint?: string; outputDir?: string; archiveExisting: boolean; pluginPath?: string; @@ -953,6 +955,11 @@ export async function main( `OpenAI model to use (default: ${DEFAULT_SCAN_MODEL_CONFIGURATION.model}).`, ), effort: effortOption(), + endpoint: optionValue("--endpoint") + .optional() + .describe( + "Custom LLM API endpoint URL. Also settable via CODEX_ENDPOINT env var.", + ), outputDir: optionValue("--output-dir") .optional() .describe( @@ -1029,6 +1036,13 @@ export async function main( ], }, }, + { + args: { repository: "." }, + options: { + model: "gpt-5.6-terra", + endpoint: "https://custom.api.example.com/v1", + }, + }, ], output: z.record(z.string(), z.unknown()).optional(), async run({ args, error: incurError, format, options }) { @@ -1052,6 +1066,7 @@ export async function main( mode: options.mode, model: options.model, effort: options.effort, + endpoint: options.endpoint, outputDir: options.outputDir, archiveExisting: options.archiveExisting, pluginPath: options.pluginPath, @@ -1194,6 +1209,11 @@ export async function main( `OpenAI model for each repository (default: ${DEFAULT_SCAN_MODEL_CONFIGURATION.model}).`, ), effort: effortOption(), + endpoint: optionValue("--endpoint") + .optional() + .describe( + "Custom LLM API endpoint URL. Also settable via CODEX_ENDPOINT env var.", + ), maxAttempts: z .number() .int() @@ -1216,6 +1236,14 @@ export async function main( args: {}, options: { model: "gpt-5.6-terra", effort: "high" }, }, + { + args: {}, + options: { + model: "gpt-5.6-terra", + effort: "high", + endpoint: "https://custom.api.example.com/v1", + }, + }, ], hint: "CSV example:\n" + @@ -1247,13 +1275,15 @@ export async function main( if ( argument === "--model" || argument === "--effort" || - argument === "--codex" + argument === "--codex" || + argument === "--endpoint" ) { optionIndex += 2; } else if ( argument.startsWith("--model=") || argument.startsWith("--effort=") || - argument.startsWith("--codex=") + argument.startsWith("--codex=") || + argument.startsWith("--endpoint=") ) { optionIndex += 1; } else { @@ -1262,7 +1292,7 @@ export async function main( } if (argv[0] !== "bulk-scan" || optionIndex !== argv.length) { throw new Error( - "Run 'codex-security bulk-scan [--model MODEL] [--effort EFFORT] [--codex KEY=VALUE]' to discover repositories, or provide a CSV and --output-dir.", + "Run 'codex-security bulk-scan [--model MODEL] [--effort EFFORT] [--endpoint URL] [--codex KEY=VALUE]' to discover repositories, or provide a CSV and --output-dir.", ); } const wizard = await runBulkScanWizard( @@ -1295,6 +1325,8 @@ export async function main( mode: options.mode, maxAttempts: options.maxAttempts, config: { + endpoint: + options.endpoint ?? dependencies.environment["CODEX_ENDPOINT"], pluginPath: options.pluginPath, pythonPath: options.python, codexOverrides: parseCodexOverrides( @@ -2434,6 +2466,8 @@ async function runScan( const repository = arguments_.repository ?? dependencies.currentDirectory(); const target = targetFromArguments(arguments_); const config: CodexSecurityConfig = { + endpoint: + arguments_.endpoint ?? dependencies.environment["CODEX_ENDPOINT"], pluginPath: arguments_.pluginPath, pythonPath: arguments_.pythonPath, codexOverrides: diff --git a/sdk/typescript/src/config.ts b/sdk/typescript/src/config.ts index 2fa18a6e..90254740 100644 --- a/sdk/typescript/src/config.ts +++ b/sdk/typescript/src/config.ts @@ -11,6 +11,7 @@ export interface JsonObject { } export interface CodexSecurityConfig { + endpoint?: string; pluginPath?: string; codexOverrides?: JsonObject; pythonPath?: string; diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 13e73152..116b49eb 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -495,6 +495,21 @@ describe("CodexSecurity orchestration", () => { ).resolves.toBeUndefined(); }); + test("preserves openai_base_url in sanitized preflight config", () => { + const config = scanPreflightCodexConfig({ + openai_base_url: "https://custom.api.example.com/v1", + model: "gpt-5.6-sol", + }); + expect(config["openai_base_url"]).toBe("https://custom.api.example.com/v1"); + }); + + test("rejects openai_base_url containing secrets", () => { + const config = scanPreflightCodexConfig({ + openai_base_url: "https://service-api-key-secret.example.com/v1", + }); + expect(config["openai_base_url"]).toBeUndefined(); + }); + test("selects a real-scan target in the active repository layout", async () => { await expect( stat(join(REPOSITORY_ROOT, INTEGRATION_TARGET)), diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 84b7d7c2..224b820d 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -1714,6 +1714,13 @@ describe("CLI", () => { expect(({} as Record)["polluted"]).toBeUndefined(); }); + test("parses openai_base_url override through --codex", () => { + const result = parseCodexOverrides([ + 'openai_base_url="https://custom.api.example.com/v1"', + ]); + expect(result["openai_base_url"]).toBe("https://custom.api.example.com/v1"); + }); + test("rejects invalid scan and export options before starting the SDK", async () => { const cases: ReadonlyArray<[readonly string[], string]> = [ [["scan", ".", "--path", "src", "--diff", "HEAD"], "mutually exclusive"], diff --git a/sdk/typescript/tests-ts/config.test.ts b/sdk/typescript/tests-ts/config.test.ts index a03839f3..1939b2aa 100644 --- a/sdk/typescript/tests-ts/config.test.ts +++ b/sdk/typescript/tests-ts/config.test.ts @@ -6,6 +6,7 @@ import { parse } from "smol-toml"; import { scanRuntimeCodexConfig } from "../src/api.js"; import { ConfigurationError, + type CodexSecurityConfig, DEFAULT_CODEX_CONFIG, type JsonObject, mergedCodexConfig, @@ -320,6 +321,15 @@ describe("Codex configuration", () => { }); }); + test("passes an optional custom endpoint through configuration", () => { + const config: CodexSecurityConfig = {}; + expect(config.endpoint).toBeUndefined(); + const withEndpoint: CodexSecurityConfig = { + endpoint: "https://custom.api.example.com/v1", + }; + expect(withEndpoint.endpoint).toBe("https://custom.api.example.com/v1"); + }); + test("rejects owned plugin keys and incompatible v2 overrides", async () => { await expect( mergedCodexConfig({ codexOverrides: { features: false } }), diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json index 22cca3b3..8b928ebe 100644 --- a/sdk/typescript/tsconfig.json +++ b/sdk/typescript/tsconfig.json @@ -3,7 +3,6 @@ "exclude": ["dist", "node_modules", "tests-ts/package.test.ts"], "compilerOptions": { "allowJs": false, - "baseUrl": ".", "checkJs": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true,