Skip to content

Commit cd0e624

Browse files
refactor(media): remove Codex image generation (#6742) (#6746)
* refactor(media): remove Codex image generation * fix(media): preserve HyperFrames video default * test(media): keep Vela coverage image-only * fix(media): persist Home image model selection (cherry picked from commit a723012) Co-authored-by: Caprika <56862773+alchemistklk@users.noreply.github.com>
1 parent 4d57f6f commit cd0e624

28 files changed

Lines changed: 121 additions & 1844 deletions

apps/daemon/src/media/config.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3939
import os from 'node:os';
4040
import path from 'node:path';
4141
import { MEDIA_PROVIDERS } from './models.js';
42-
import { agentCliEnvForAgent, appConfigDir, readAppConfig } from '../app-config.js';
4342
import { expandHomePrefix } from '../home-expansion.js';
44-
import { spawnEnvForAgent } from '../runtimes/env.js';
4543
import { resolveXAIBearer } from '../integrations/xai-credentials.js';
4644
import { isSandboxModeEnabled } from '../sandbox-mode.js';
4745

@@ -51,7 +49,6 @@ type ProviderMap = Record<string, ProviderEntry>;
5149
type ModelAliasMap = Record<string, string>;
5250
type JsonRecord = Record<string, unknown>;
5351
type OAuthCredential = { apiKey: string; source: string };
54-
export type CodexSubscriptionStatus = { available: boolean };
5552

5653
// Single env var carries the full alias map as JSON so we don't have
5754
// to dynamically lift `OD_MEDIA_MODEL_ALIAS_<id>=value` into a record
@@ -292,41 +289,6 @@ async function readJsonIfPresent(file: string): Promise<JsonRecord | null> {
292289
}
293290
}
294291

295-
export async function resolveCodexImagegenEnv(projectRoot?: string): Promise<NodeJS.ProcessEnv> {
296-
if (projectRoot) {
297-
const dataDir = appConfigDir(projectRoot);
298-
try {
299-
const appConfig = await readAppConfig(dataDir);
300-
const configuredEnv = agentCliEnvForAgent(appConfig.agentCliEnv, 'codex');
301-
return spawnEnvForAgent('codex', process.env, configuredEnv);
302-
} catch {
303-
return spawnEnvForAgent('codex', process.env);
304-
}
305-
}
306-
return spawnEnvForAgent('codex', process.env);
307-
}
308-
309-
function codexHomeFromEnv(env: NodeJS.ProcessEnv): string {
310-
const home = env.CODEX_HOME?.trim() || path.join(os.homedir(), '.codex');
311-
const resolvedHome = home.startsWith('~/')
312-
? path.join(os.homedir(), home.slice(2))
313-
: home;
314-
return path.resolve(resolvedHome);
315-
}
316-
317-
export async function resolveCodexSubscriptionStatus(
318-
projectRoot?: string,
319-
): Promise<CodexSubscriptionStatus> {
320-
if (isSandboxModeEnabled(process.env)) return { available: false };
321-
const env = await resolveCodexImagegenEnv(projectRoot);
322-
const codexAuth = await readJsonIfPresent(
323-
path.join(codexHomeFromEnv(env), 'auth.json'),
324-
);
325-
const authMode = readNestedString(codexAuth, ['auth_mode']);
326-
const accessToken = readNestedString(codexAuth, ['tokens', 'access_token']);
327-
return { available: authMode === 'chatgpt' || Boolean(accessToken) };
328-
}
329-
330292
function apiKeyFromCodexAuth(data: unknown): string {
331293
return readNestedString(data, ['OPENAI_API_KEY']);
332294
}

apps/daemon/src/media/index.ts

Lines changed: 2 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
// plus text-to-speech via /v1/audio/speech,
2121
// with auto-detection for Azure OpenAI
2222
// deployments based on the configured base URL
23-
// * provider 'codex' → local Codex CLI subscription imagegen
2423
// * provider 'volcengine' → Volcengine Ark async tasks API for
2524
// Doubao Seedance 2.0 (video) and Seedream
2625
// (image)
@@ -51,7 +50,7 @@
5150
// so the CLI can exit non-zero and the agent can't silently narrate the
5251
// placeholder as the final result.
5352

54-
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
53+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
5554
import { execFile as execFileCb, spawn } from 'node:child_process';
5655
import os from 'node:os';
5756
import path from 'node:path';
@@ -69,10 +68,7 @@ import {
6968
modelsForSurface,
7069
} from './models.js';
7170
import { assertAndFetchExternalAsset } from '../connectionTest.js';
72-
import { normalizeCodexConfigFile } from '../codex-config-normalize.js';
7371
import {
74-
resolveCodexImagegenEnv,
75-
resolveCodexSubscriptionStatus,
7672
resolveModelAlias,
7773
resolveProviderConfig,
7874
} from './config.js';
@@ -81,7 +77,6 @@ import {
8177
type ImageGenerationRequestSummary,
8278
} from './image-generation-retry.js';
8379
import { renderVelaImage, renderVelaVideo } from './vela.js';
84-
import { codexNeedsDangerFullAccessSandbox } from '../runtimes/defs/codex.js';
8580
import {
8681
ensureProject,
8782
kindFor,
@@ -180,8 +175,6 @@ const NANOBANANA_DEFAULT_MODEL = 'gemini-3.1-flash-image-preview';
180175
const NANOBANANA_DEFAULT_IMAGE_SIZE = '1K';
181176
const IMAGEROUTER_DEFAULT_BASE_URL = 'https://api.imagerouter.io/v1/openai';
182177
const CUSTOM_IMAGE_MODEL_ID = 'custom-image';
183-
const CODEX_IMAGE_ORCHESTRATOR_MODEL = 'gpt-5.5';
184-
185178
const DEFAULT_OUTPUT_BY_SURFACE = {
186179
image: 'image.png',
187180
video: 'video.mp4',
@@ -570,17 +563,6 @@ export async function generateMedia(args: {
570563
ctx,
571564
customImageCredentials,
572565
);
573-
const codexSubscriptionModel =
574-
!customImageOverride
575-
&& surface === 'image'
576-
&& def.provider === 'openai'
577-
&& ctx.wireModel === ctx.model
578-
? codexSubscriptionEquivalent(ctx.model)
579-
: null;
580-
const useCodexSubscription =
581-
codexSubscriptionModel
582-
? (await resolveCodexSubscriptionStatus(projectRoot)).available
583-
: false;
584566
try {
585567
if (
586568
def.provider === 'openai'
@@ -592,18 +574,6 @@ export async function generateMedia(args: {
592574
bytes = result.bytes;
593575
providerNote = result.providerNote;
594576
suggestedExt = result.suggestedExt;
595-
} else if (codexSubscriptionModel && useCodexSubscription) {
596-
providerId = 'codex';
597-
const result = await renderCodexImage({
598-
...ctx,
599-
model: codexSubscriptionModel.id,
600-
wireModel: codexSubscriptionModel.id,
601-
modelDef: codexSubscriptionModel,
602-
provider: findProvider('codex'),
603-
});
604-
bytes = result.bytes;
605-
providerNote = result.providerNote;
606-
suggestedExt = result.suggestedExt;
607577
} else if (def.provider === 'vela' && surface === 'image') {
608578
const result = await renderVelaImage(ctx);
609579
bytes = result.bytes;
@@ -622,11 +592,6 @@ export async function generateMedia(args: {
622592
bytes = result.bytes;
623593
providerNote = result.providerNote;
624594
suggestedExt = result.suggestedExt;
625-
} else if (def.provider === 'codex' && surface === 'image') {
626-
const result = await renderCodexImage(ctx);
627-
bytes = result.bytes;
628-
providerNote = result.providerNote;
629-
suggestedExt = result.suggestedExt;
630595
} else if (
631596
def.provider === 'openai'
632597
&& surface === 'audio'
@@ -928,9 +893,7 @@ function withMediaRequestInit(
928893
}
929894

930895
const OPENAI_IMAGE_NO_CREDENTIAL_MESSAGE =
931-
'no OpenAI credential - configure an API key in Settings or set OPENAI_API_KEY. ' +
932-
"If you're signed into Codex with a ChatGPT subscription, use the codex-gpt-image-2 model instead; " +
933-
'it renders through your local Codex login and needs no OpenAI API key.';
896+
'no OpenAI credential - configure an API key in Settings or set OPENAI_API_KEY.';
934897

935898
async function renderOpenAIImage(ctx: MediaContext, credentials: ProviderConfig): Promise<RenderResult> {
936899
if (!credentials.apiKey) {
@@ -1017,180 +980,6 @@ async function renderOpenAIImage(ctx: MediaContext, credentials: ProviderConfig)
1017980
};
1018981
}
1019982

1020-
function codexGeneratedImagesRoot(env: NodeJS.ProcessEnv = process.env): string {
1021-
const home = env.CODEX_HOME?.trim() || path.join(os.homedir(), '.codex');
1022-
const resolvedHome = home.startsWith('~/') ? path.join(os.homedir(), home.slice(2)) : home;
1023-
return path.resolve(resolvedHome, 'generated_images');
1024-
}
1025-
1026-
function codexImageModelLabel(model: string): string {
1027-
return model.startsWith('codex-') ? model.slice('codex-'.length) : model;
1028-
}
1029-
1030-
function codexImagePrompt(ctx: MediaContext): string {
1031-
const prompt = ctx.prompt || 'A high-quality reference image.';
1032-
const aspect = ctx.aspect ? `\nAspect ratio: ${ctx.aspect}.` : '';
1033-
const prefix = ctx.imageRefs.length > 0
1034-
? '$imagegen Edit the attached reference image:'
1035-
: '$imagegen';
1036-
return `${prefix} ${prompt}${aspect}`;
1037-
}
1038-
1039-
function codexImagegenArgs(ctx: MediaContext, generatedRoot: string, env: NodeJS.ProcessEnv): string[] {
1040-
const sandbox = codexNeedsDangerFullAccessSandbox()
1041-
? ['--sandbox', 'danger-full-access']
1042-
: ['--sandbox', 'workspace-write', '-c', 'sandbox_workspace_write.network_access=true'];
1043-
const model = env.OD_CODEX_IMAGEGEN_MODEL?.trim() || CODEX_IMAGE_ORCHESTRATOR_MODEL;
1044-
const args = [
1045-
'exec',
1046-
'--json',
1047-
'--skip-git-repo-check',
1048-
...sandbox,
1049-
'-C',
1050-
ctx.projectRoot,
1051-
'--add-dir',
1052-
generatedRoot,
1053-
'--model',
1054-
model,
1055-
];
1056-
if (env.OD_CODEX_DISABLE_PLUGINS === '1') args.push('--disable', 'plugins');
1057-
for (const ref of ctx.imageRefs) args.push('-i', ref.abs);
1058-
return args;
1059-
}
1060-
1061-
function parseCodexThreadId(stdout: string): string {
1062-
for (const line of stdout.split(/\r?\n/)) {
1063-
if (!line.trim()) continue;
1064-
try {
1065-
const obj = JSON.parse(line) as { thread_id?: unknown; type?: unknown };
1066-
if (obj.type === 'thread.started' && typeof obj.thread_id === 'string') {
1067-
return obj.thread_id;
1068-
}
1069-
} catch {
1070-
// Non-JSON progress belongs in stdout on some CLI builds; ignore it.
1071-
}
1072-
}
1073-
throw new Error('codex imagegen did not emit a thread.started thread_id');
1074-
}
1075-
1076-
function summarizeCodexImagegenStdout(stdout: string): string {
1077-
const messages: string[] = [];
1078-
for (const line of stdout.split(/\r?\n/)) {
1079-
const trimmed = line.trim();
1080-
if (!trimmed) continue;
1081-
try {
1082-
const obj = JSON.parse(trimmed) as { item?: { text?: unknown }; text?: unknown };
1083-
const text = typeof obj.item?.text === 'string'
1084-
? obj.item.text
1085-
: typeof obj.text === 'string'
1086-
? obj.text
1087-
: '';
1088-
if (text.trim()) messages.push(text.trim());
1089-
} catch {
1090-
messages.push(trimmed);
1091-
}
1092-
}
1093-
return truncate(messages.join('\n'), 500);
1094-
}
1095-
1096-
function codexImagegenMissingOutputError(threadDir: string, stdout: string): Error {
1097-
const summary = summarizeCodexImagegenStdout(stdout);
1098-
const suffix = summary ? ` Codex stdout summary: ${summary}` : '';
1099-
if (/\bpreview[- ]only\b|without saving|without writing|does not write|no file|bez przenoszenia/i.test(summary)) {
1100-
return new Error(
1101-
`Codex imagegen completed in preview-only mode and did not write an image file under ${threadDir}. Use an API-backed image provider or a Codex CLI build that writes generated_images output.${suffix}`,
1102-
);
1103-
}
1104-
return new Error(
1105-
`Codex imagegen completed but did not write an ig_* or call_* image under ${threadDir}. Use an API-backed image provider or a Codex CLI build that writes generated_images output.${suffix}`,
1106-
);
1107-
}
1108-
1109-
async function readCodexGeneratedImage(
1110-
generatedRoot: string,
1111-
threadId: string,
1112-
stdout: string,
1113-
): Promise<Buffer> {
1114-
const threadDir = path.join(generatedRoot, threadId);
1115-
let entries: string[];
1116-
try {
1117-
entries = await readdir(threadDir);
1118-
} catch (err) {
1119-
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
1120-
throw codexImagegenMissingOutputError(threadDir, stdout);
1121-
}
1122-
throw err;
1123-
}
1124-
const supportedImagePattern = /\.(?:png|jpe?g|webp)$/i;
1125-
const match = entries
1126-
.filter((name) => /^ig_/i.test(name) && supportedImagePattern.test(name))
1127-
.sort()[0]
1128-
?? entries
1129-
.filter((name) => /^call_/i.test(name) && supportedImagePattern.test(name))
1130-
.sort()[0];
1131-
if (!match) {
1132-
throw codexImagegenMissingOutputError(threadDir, stdout);
1133-
}
1134-
const imagePath = path.join(threadDir, match);
1135-
const bytes = await readFile(imagePath);
1136-
if (process.env.OD_CODEX_KEEP_GENERATED_IMAGES !== '1') {
1137-
await rm(threadDir, { recursive: true, force: true });
1138-
}
1139-
return bytes;
1140-
}
1141-
1142-
async function runCodexImagegen(
1143-
ctx: MediaContext,
1144-
generatedRoot: string,
1145-
env: NodeJS.ProcessEnv,
1146-
): Promise<{ stderr: string; stdout: string }> {
1147-
const codexBin = env.CODEX_BIN?.trim() || 'codex';
1148-
const child = spawn(codexBin, codexImagegenArgs(ctx, generatedRoot, env), {
1149-
cwd: ctx.projectRoot,
1150-
env,
1151-
stdio: ['pipe', 'pipe', 'pipe'],
1152-
});
1153-
const stdout: Buffer[] = [];
1154-
const stderr: Buffer[] = [];
1155-
const timeoutMs = Number(process.env.OD_CODEX_IMAGEGEN_TIMEOUT_MS || 300_000);
1156-
return await new Promise((resolve, reject) => {
1157-
const timer = setTimeout(() => {
1158-
child.kill('SIGTERM');
1159-
reject(new Error(`codex imagegen timed out after ${timeoutMs}ms`));
1160-
}, timeoutMs);
1161-
child.stdout.on('data', (chunk) => stdout.push(Buffer.from(chunk)));
1162-
child.stderr.on('data', (chunk) => stderr.push(Buffer.from(chunk)));
1163-
child.on('error', (err) => {
1164-
clearTimeout(timer);
1165-
reject(err);
1166-
});
1167-
child.on('close', (code) => {
1168-
clearTimeout(timer);
1169-
const out = Buffer.concat(stdout).toString('utf8');
1170-
const err = Buffer.concat(stderr).toString('utf8');
1171-
if (code !== 0) reject(new Error(`codex imagegen exited ${code}: ${truncate(err || out, 1000)}`));
1172-
else resolve({ stdout: out, stderr: err });
1173-
});
1174-
child.stdin.end(codexImagePrompt(ctx));
1175-
});
1176-
}
1177-
1178-
async function renderCodexImage(ctx: MediaContext): Promise<RenderResult> {
1179-
const env = await resolveCodexImagegenEnv(ctx.projectRoot);
1180-
await normalizeCodexConfigFile(env);
1181-
const generatedRoot = codexGeneratedImagesRoot(env);
1182-
await mkdir(generatedRoot, { recursive: true });
1183-
const { stdout } = await runCodexImagegen(ctx, generatedRoot, env);
1184-
const threadId = parseCodexThreadId(stdout);
1185-
const bytes = await readCodexGeneratedImage(generatedRoot, threadId, stdout);
1186-
const imageModel = codexImageModelLabel(ctx.model);
1187-
return {
1188-
bytes,
1189-
providerNote: `codex/${imageModel} via ${env.OD_CODEX_IMAGEGEN_MODEL?.trim() || CODEX_IMAGE_ORCHESTRATOR_MODEL} · ${ctx.aspect} · ${bytes.length} bytes`,
1190-
suggestedExt: sniffImageExt(bytes),
1191-
};
1192-
}
1193-
1194983
async function renderImageRouterImage(ctx: MediaContext, credentials: ProviderConfig): Promise<RenderResult> {
1195984
if (!credentials.apiKey) {
1196985
throw new Error(
@@ -1333,11 +1122,6 @@ function customImageOverridesOpenAIModel(
13331122
return model === ctx.model || model === ctx.wireModel;
13341123
}
13351124

1336-
function codexSubscriptionEquivalent(modelId: string): MediaModel | null {
1337-
const candidate = findMediaModel(`codex-${modelId}`);
1338-
return candidate?.provider === 'codex' ? candidate : null;
1339-
}
1340-
13411125
async function parseOpenAICompatibleJson(resp: Response, providerTag: string): Promise<any> {
13421126
const text = await resp.text();
13431127
if (!resp.ok) {

apps/daemon/src/media/models.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export type MediaModel = {
3232

3333
export const MEDIA_PROVIDERS: MediaProvider[] = [
3434
{ id: 'openai', label: 'OpenAI', hint: 'gpt-image-2 / dall-e-3', integrated: true, defaultBaseUrl: 'https://api.openai.com/v1' },
35-
{ id: 'codex', label: 'Codex Subscription', hint: 'gpt-image-2 via local Codex CLI login', integrated: true, credentialsRequired: false, docsUrl: 'https://developers.openai.com/codex' },
3635
{ id: 'vela', label: 'Open Design Cloud', hint: 'Managed image and video generation through Vela', integrated: true, credentialsRequired: false, settingsVisible: false },
3736
{ id: 'volcengine', label: 'Volcengine Ark (Doubao)', hint: 'Seedance 2.0 / Seedream', integrated: true, defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3' },
3837
{ id: 'grok', label: 'xAI Grok Imagine', hint: 'grok-imagine — image + video with native audio', integrated: true, defaultBaseUrl: 'https://api.x.ai/v1' },
@@ -86,19 +85,17 @@ export const MEDIA_PROVIDERS: MediaProvider[] = [
8685
];
8786

8887
export const IMAGE_MODELS: MediaModel[] = [
89-
{ id: 'vela/gpt-image-2', label: 'gpt-image-2 (Cloud)', hint: 'Open Design Cloud · managed image generation and editing', provider: 'vela', caps: ['t2i', 'i2i'] },
88+
{ id: 'vela/gpt-image-2', label: 'gpt-image-2 (Cloud)', hint: 'Open Design Cloud · managed image generation and editing', provider: 'vela', caps: ['t2i', 'i2i'], default: true },
9089
{ id: 'vela/nano-banana-2', label: 'nano-banana-2 (Cloud)', hint: 'Open Design Cloud · managed image generation and editing', provider: 'vela', caps: ['t2i', 'i2i'] },
9190
{ id: 'vela/nano-banana-2-lite', label: 'nano-banana-2-lite (Cloud)', hint: 'Open Design Cloud · fast managed image generation and editing', provider: 'vela', caps: ['t2i', 'i2i'] },
9291
{ id: 'vela/seedream-5.0', label: 'seedream-5.0 (Cloud)', hint: 'Open Design Cloud · managed image generation and editing', provider: 'vela', caps: ['t2i', 'i2i'] },
9392
{ id: 'vela/seedream-5.0-pro', label: 'seedream-5.0-pro (Cloud)', hint: 'Open Design Cloud · high-quality managed image generation and editing', provider: 'vela', caps: ['t2i', 'i2i'] },
94-
{ id: 'gpt-image-2', label: 'gpt-image-2', hint: 'OpenAI · 4K, native multimodal', provider: 'openai', caps: ['t2i', 'i2i', 'inpaint'], default: true },
93+
{ id: 'gpt-image-2', label: 'gpt-image-2', hint: 'OpenAI · 4K, native multimodal', provider: 'openai', caps: ['t2i', 'i2i', 'inpaint'] },
9594
{ id: 'gpt-image-1.5', label: 'gpt-image-1.5', hint: 'OpenAI · 4× faster than gpt-image-1', provider: 'openai', caps: ['t2i', 'i2i', 'inpaint'] },
9695
{ id: 'gpt-image-1', label: 'gpt-image-1', hint: 'OpenAI · ChatGPT native', provider: 'openai', caps: ['t2i', 'i2i', 'inpaint'] },
9796
{ id: 'gpt-image-1-mini', label: 'gpt-image-1-mini', hint: 'OpenAI · low-cost variant', provider: 'openai', caps: ['t2i', 'i2i'] },
9897
{ id: 'dall-e-3', label: 'dall-e-3', hint: 'OpenAI · classic', provider: 'openai', caps: ['t2i'] },
9998
{ id: 'dall-e-2', label: 'dall-e-2', hint: 'OpenAI · legacy', provider: 'openai', caps: ['t2i'] },
100-
{ id: 'codex-gpt-image-2', label: 'gpt-image-2 (Codex)', hint: 'Codex Subscription · local CLI imagegen', provider: 'codex', caps: ['t2i', 'i2i'] },
101-
10299
{ id: 'doubao-seedream-3-0-t2i-250415', label: 'seedream-3.0', hint: 'ByteDance · Doubao image', provider: 'volcengine', caps: ['t2i'] },
103100
{ id: 'doubao-seededit-3-0-i2i-250628', label: 'seededit-3.0', hint: 'ByteDance · image edit', provider: 'volcengine', caps: ['i2i'] },
104101

@@ -234,6 +231,8 @@ const MEDIA_MODEL_ALIASES: Readonly<Record<string, string>> = {
234231
'nano-banana': 'vela/nano-banana-2',
235232
'nano-banana-2': 'vela/nano-banana-2',
236233
'nano-banana-2-lite': 'vela/nano-banana-2-lite',
234+
// Preserve existing project metadata while removing the Codex renderer.
235+
'codex-gpt-image-2': 'vela/gpt-image-2',
237236
};
238237

239238
export function canonicalMediaModelId(id: string): string {

0 commit comments

Comments
 (0)