/** * Plugin entry for the ChatGPT Codex model provider: registers the TEXT_*, * RESPONSE_HANDLER, and ACTION_PLANNER model handlers that route generation * through a user's ChatGPT subscription via the codex CLI OAuth cache. Every * handler delegates to a single per-runtime CodexBackend (held in a WeakMap so * calls on one runtime serialize through the backend's FIFO queue). * * Auto-enables only when an auth profile selects provider "codex-cli"; there is * no env-key trigger. Tool- or message-bearing calls return native tool calls * rather than a plain string, and streaming calls attach toolCalls so the * planner still sees tool-only responses. */ import type { GenerateTextParams, IAgentRuntime, Plugin, TextStreamResult } from "@elizaos/core"; import { logger, ModelType } from "@elizaos/core"; import { CodexBackend, type CodexGenerateParams, type CodexGenerateResult, } from "./src/codex-backend"; const TEXT_NANO_MODEL_TYPE = (ModelType.TEXT_NANO ?? "TEXT_NANO") as string; const TEXT_MEDIUM_MODEL_TYPE = (ModelType.TEXT_MEDIUM ?? "TEXT_MEDIUM") as string; const TEXT_MEGA_MODEL_TYPE = (ModelType.TEXT_MEGA ?? "TEXT_MEGA") as string; const RESPONSE_HANDLER_MODEL_TYPE = (ModelType.RESPONSE_HANDLER ?? "RESPONSE_HANDLER") as string; const ACTION_PLANNER_MODEL_TYPE = (ModelType.ACTION_PLANNER ?? "ACTION_PLANNER") as string; const CODEX_MODEL_SETTING = "CODEX_MODEL"; const CODEX_SUPPORTED_MODELS = [ "gpt-5", "gpt-5-codex", "gpt-5.4", "gpt-5.5", "gpt-5.5-pro", ] as const; type RuntimeWithSettings = IAgentRuntime & { getSetting?: (key: string) => string | number | boolean | undefined | null; }; type TextResultWithNativeTools = { text: string; toolCalls: CodexGenerateResult["toolCalls"]; finishReason?: string; usage?: CodexGenerateResult["usage"]; }; function readEnv(name: string): string | undefined { if (typeof process === "undefined") return undefined; return process.env[name]; } function getSetting(runtime: IAgentRuntime, key: string): string | undefined { const value = (runtime as RuntimeWithSettings).getSetting?.(key); return value === undefined || value === null ? readEnv(key) : String(value); } function getCodexModel(runtime: IAgentRuntime): string { return getSetting(runtime, CODEX_MODEL_SETTING) ?? "gpt-5.5"; } function getRequestedCodexModel(runtime: IAgentRuntime, params: GenerateTextParams): string { const requestedModel = (params as GenerateTextParams & { model?: unknown }).model; return typeof requestedModel === "string" && requestedModel.trim().length > 0 ? requestedModel.trim() : getCodexModel(runtime); } const backendByRuntime = new WeakMap(); function createBackend(runtime: IAgentRuntime): CodexBackend { const existing = backendByRuntime.get(runtime); if (existing) return existing; const jitterRaw = getSetting(runtime, "CODEX_JITTER_MS_MAX"); const jitterMaxMs = jitterRaw === undefined ? undefined : Number.parseInt(jitterRaw, 10); const backend = new CodexBackend({ authPath: getSetting(runtime, "CODEX_AUTH_PATH"), baseUrl: getSetting(runtime, "CODEX_BASE_URL"), model: getCodexModel(runtime), originator: getSetting(runtime, "CODEX_ORIGINATOR"), jitterMaxMs: Number.isFinite(jitterMaxMs) ? jitterMaxMs : undefined, }); backendByRuntime.set(runtime, backend); return backend; } function toTextReturn( params: GenerateTextParams, result: CodexGenerateResult ): string | TextResultWithNativeTools { if (params.tools?.length || params.messages?.length || result.toolCalls.length > 0) { return { text: result.text, toolCalls: result.toolCalls, finishReason: result.finishReason, usage: result.usage, }; } return result.text; } function buildCodexGenerateParams( runtime: IAgentRuntime, params: GenerateTextParams ): CodexGenerateParams { // Honor `responseSchema` natively. OpenAI-compatible Codex models accept // `response_format: { type: "json_schema", schema }` for guaranteed JSON // output; if the caller already passed a custom `responseFormat`, leave it // alone. const paramsWithSchema = params as GenerateTextParams & { responseSchema?: unknown; system?: string; }; const responseFormat = params.responseFormat ?? (paramsWithSchema.responseSchema ? { type: "json_schema" as const, schema: paramsWithSchema.responseSchema as Record, } : undefined); return { prompt: params.prompt ?? "", system: paramsWithSchema.system, messages: params.messages, tools: params.tools, toolChoice: params.toolChoice, model: getRequestedCodexModel(runtime, params), temperature: params.temperature, maxTokens: params.maxTokens, responseFormat, }; } function streamTextWithCodex(runtime: IAgentRuntime, params: GenerateTextParams): TextStreamResult { const queue: string[] = []; let notify: (() => void) | undefined; let done = false; const wake = () => { notify?.(); notify = undefined; }; const resultPromise = createBackend(runtime) .generate({ ...buildCodexGenerateParams(runtime, params), onTextDelta: (delta) => { queue.push(delta); wake(); }, }) .finally(() => { done = true; wake(); }); async function* textStream(): AsyncIterable { while (!done || queue.length > 0) { const next = queue.shift(); if (next !== undefined) { yield next; continue; } await new Promise((resolve) => { notify = resolve; }); } } // The planner streams (the Discord message handler sets onStreamChunk), so // ACTION_PLANNER/RESPONSE_HANDLER go through THIS path. The runtime collapses // the stream and only preserves native tool calls when `toolCalls` is present // on the returned object (it duck-types `"toolCalls" in streamRaw`). Without // it, a tool-only response (empty text + native toolCalls) collapses to a bare // empty string and the planner never sees the tool call — it replans until the // required-tool cap and gives up. Mirror plugin-openai: attach `toolCalls` // whenever the call is native, and stamp the model name for trajectory pricing. const shouldReturnNativeTools = Boolean( params.messages?.length || params.tools?.length || params.toolChoice ); return { textStream: textStream(), text: resultPromise.then((result) => result.text), ...(shouldReturnNativeTools ? { toolCalls: resultPromise.then((result) => result.toolCalls) } : {}), usage: resultPromise.then((result) => result.usage ? { promptTokens: result.usage.inputTokens, completionTokens: result.usage.outputTokens, totalTokens: result.usage.totalTokens, } : undefined ), finishReason: resultPromise.then((result) => result.finishReason), providerMetadata: { modelName: getRequestedCodexModel(runtime, params) }, }; } async function generateTextWithCodex( runtime: IAgentRuntime, params: GenerateTextParams, modelType: string ): Promise { const model = getRequestedCodexModel(runtime, params); logger.debug(`[codex-cli] Using ${modelType} model: ${model}`); if (params.stream) return streamTextWithCodex(runtime, params); const result = await createBackend(runtime).generate(buildCodexGenerateParams(runtime, params)); return toTextReturn(params, result); } const codexModels = { [ModelType.TEXT_SMALL]: (runtime: IAgentRuntime, params: GenerateTextParams) => generateTextWithCodex(runtime, params, ModelType.TEXT_SMALL), [TEXT_NANO_MODEL_TYPE]: (runtime: IAgentRuntime, params: GenerateTextParams) => generateTextWithCodex(runtime, params, TEXT_NANO_MODEL_TYPE), [TEXT_MEDIUM_MODEL_TYPE]: (runtime: IAgentRuntime, params: GenerateTextParams) => generateTextWithCodex(runtime, params, TEXT_MEDIUM_MODEL_TYPE), [ModelType.TEXT_LARGE]: (runtime: IAgentRuntime, params: GenerateTextParams) => generateTextWithCodex(runtime, params, ModelType.TEXT_LARGE), [TEXT_MEGA_MODEL_TYPE]: (runtime: IAgentRuntime, params: GenerateTextParams) => generateTextWithCodex(runtime, params, TEXT_MEGA_MODEL_TYPE), [RESPONSE_HANDLER_MODEL_TYPE]: (runtime: IAgentRuntime, params: GenerateTextParams) => generateTextWithCodex(runtime, params, RESPONSE_HANDLER_MODEL_TYPE), [ACTION_PLANNER_MODEL_TYPE]: (runtime: IAgentRuntime, params: GenerateTextParams) => generateTextWithCodex(runtime, params, ACTION_PLANNER_MODEL_TYPE), } as Plugin["models"]; const codexModelMetadata = Object.fromEntries( Object.keys(codexModels ?? {}).map((modelType) => [ modelType, { displayModelSetting: CODEX_MODEL_SETTING }, ]) ) satisfies NonNullable; /** @internal - exported for shape tests only. */ export const __INTERNAL_buildCodexGenerateParams = buildCodexGenerateParams; export const codexCliPlugin: Plugin = { name: "codex-cli", description: "ChatGPT Codex model provider using the codex CLI OAuth token cache", autoEnable: { // No env-key auto-enable; activated when an auth profile selects codex-cli // as its provider (e.g. via subscription onboarding). shouldEnable: (_env, config) => { const auth = (config as { auth?: { profiles?: Record } }).auth; const profiles = auth?.profiles; if (!profiles || typeof profiles !== "object") return false; return Object.values(profiles).some((profile) => { if (!profile || typeof profile !== "object") return false; return (profile as { provider?: unknown }).provider === "codex-cli"; }); }, }, config: { CODEX_AUTH_PATH: readEnv("CODEX_AUTH_PATH") ?? null, CODEX_BASE_URL: readEnv("CODEX_BASE_URL") ?? null, [CODEX_MODEL_SETTING]: readEnv(CODEX_MODEL_SETTING) ?? null, CODEX_JITTER_MS_MAX: readEnv("CODEX_JITTER_MS_MAX") ?? null, CODEX_ORIGINATOR: readEnv("CODEX_ORIGINATOR") ?? null, }, async init(): Promise { logger.info(`[codex-cli] initialized. Supported models: ${CODEX_SUPPORTED_MODELS.join(", ")}`); }, models: codexModels, modelMetadata: codexModelMetadata, }; export * from "./src/codex-auth"; export * from "./src/sse-parser"; export * from "./src/tool-format-openai"; export { CODEX_SUPPORTED_MODELS, CodexBackend }; export default codexCliPlugin;