426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
/**
|
|
* 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<IAgentRuntime, CodexBackend>();
|
|
|
|
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<string, unknown>,
|
|
}
|
|
: 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<string> {
|
|
while (!done || queue.length > 0) {
|
|
const next = queue.shift();
|
|
if (next !== undefined) {
|
|
yield next;
|
|
continue;
|
|
}
|
|
await new Promise<void>((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<string | TextResultWithNativeTools | TextStreamResult> {
|
|
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<Plugin["modelMetadata"]>;
|
|
|
|
/** @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<string, unknown> } }).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<void> {
|
|
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;
|