Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

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;