Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 120 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"license": "ISC",
"type": "module",
"devDependencies": {
"@openai/codex": "^0.98.0",
"@openai/codex": "^0.99.0",
"@types/node": "^24.10.1",
"mcp-hello-world": "^1.1.2",
"tsx": "^4.20.6",
Expand Down
35 changes: 28 additions & 7 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {CodexEventHandler} from "./CodexEventHandler";
import {CodexApprovalHandler} from "./CodexApprovalHandler";
import {CodexAuthMethods, type CodexAuthRequest} from "./CodexAuthMethod";
import {CodexAcpClient, type SessionMetadata} from "./CodexAcpClient";
import type {Account, Model, RateLimitSnapshot} from "./app-server/v2";
import type {ReasoningEffort} from "./app-server";
import type {Account, Model, ReasoningEffortOption} from "./app-server/v2";
import type {RateLimitsMap} from "./RateLimitsMap";
import type {InputModality, ReasoningEffort} from "./app-server";
import {ModelId} from "./ModelId";
import {AgentMode} from "./AgentMode";
import type {TokenCount} from "./TokenCount";
Expand All @@ -21,12 +22,14 @@ import {logger} from "./Logger";
export interface SessionState {
sessionId: string,
currentModelId: string,
supportedReasoningEfforts: Array<ReasoningEffortOption>,
supportedInputModalities: Array<InputModality>,
agentMode: AgentMode,
currentTurnId: string | null;
lastTokenUsage: TokenCount | null;
totalTokenUsage: TokenCount | null;
modelContextWindow: number | null;
rateLimits: RateLimitSnapshot | null;
rateLimits: RateLimitsMap | null;
account: Account | null;
cwd: string;
sessionMcpServers?: Array<string>;
Expand Down Expand Up @@ -117,9 +120,12 @@ export class CodexAcpServer implements acp.Agent {
const {sessionId, currentModelId, models} = sessionMetadata;
logger.log(`Waiting MCP servers to start...`)
const sessionMcpServers = await pendingMcpServers;
const currentModel = this.findCurrentModel(models, currentModelId);
const sessionState: SessionState = {
sessionId: sessionId,
currentModelId: currentModelId,
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
agentMode: AgentMode.getInitialAgentMode(),
currentTurnId: null,
lastTokenUsage: null,
Expand Down Expand Up @@ -237,6 +243,8 @@ export class CodexAcpServer implements acp.Agent {
}

sessionState.currentModelId = ModelId.fromComponents(model, reasoningEffort).toString();
sessionState.supportedReasoningEfforts = model.supportedReasoningEfforts;
sessionState.supportedInputModalities = model.inputModalities;

return {};
}
Expand All @@ -245,6 +253,11 @@ export class CodexAcpServer implements acp.Agent {
void this.availableCommands.publish(sessionId);
}

private findCurrentModel(models: Model[], currentModelId: string): Model | undefined {
const modelId = ModelId.fromString(currentModelId);
return models.find(m => m.id === modelId.model);
}

private createModelState(availableModels: Model[], selectedModelId: string): SessionModelState {
const allowedModels = availableModels
.flatMap((model) =>
Expand Down Expand Up @@ -292,14 +305,22 @@ export class CodexAcpServer implements acp.Agent {
};
}

const disableSummary = sessionState.account?.type === "apiKey"
const modelId = ModelId.fromString(sessionState.currentModelId);
const modelLacksReasoning = sessionState.supportedReasoningEfforts.length > 0
&& sessionState.supportedReasoningEfforts.every(e => e.reasoningEffort === "none");

const disableSummary = sessionState.account?.type === "apiKey" || modelLacksReasoning;
if (disableSummary) {
logger.log("Disable reasoning.summary because API key is used", {sessionId: params.sessionId});
logger.log("Disable reasoning.summary", {
sessionId: params.sessionId,
reason: sessionState.account?.type === "apiKey" ? "API key" : "model lacks reasoning"
});
}


if (!sessionState.supportedInputModalities.includes("image") && params.prompt.some(b => b.type === "image")) {
throw RequestError.invalidRequest("The current model does not support image input");
}
const agentMode = sessionState.agentMode;
const modelId = ModelId.fromString(sessionState.currentModelId);
const turnCompleted = await this.runWithProcessCheck(
() => this.codexAcpClient.sendPrompt(params, agentMode, modelId, disableSummary));

Expand Down
28 changes: 20 additions & 8 deletions src/CodexCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {ACPSessionConnection} from "./ACPSessionConnection";
import type {CodexAcpClient} from "./CodexAcpClient";
import type {RateLimitSnapshot, SkillsListEntry} from "./app-server/v2";
import type {SessionState} from "./CodexAcpServer";
import type {RateLimitsMap} from "./RateLimitsMap";
import type {TokenCount} from "./TokenCount";
import {logger} from "./Logger";

Expand Down Expand Up @@ -233,7 +234,7 @@ export class CodexCommands {
return "API key configured";
}
if (account.type === "chatgpt") {
return `ChatGPT (${account.email})`;
return `ChatGPT ${account.planType} (${account.email})`;
}
return "unknown";
}
Expand All @@ -259,36 +260,47 @@ export class CodexCommands {
return `${percentLeft}% left (${usedFormatted} used / ${totalFormatted})`;
}

private formatRateLimitLines(rateLimits: RateLimitSnapshot | null): string[] {
if (!rateLimits) {
private formatRateLimitLines(rateLimits: RateLimitsMap | null): string[] {
if (!rateLimits || rateLimits.size === 0) {
return [`**Limits:** data not available yet`];
}

const lines: string[] = [];

for (const [, entry] of rateLimits) {
lines.push(...this.formatSingleRateLimit(entry.limitName, entry.snapshot));
}

return lines.length > 0 ? lines : [`**Limits:** data not available yet`];
}

private formatSingleRateLimit(limitName: string, rateLimits: RateLimitSnapshot): string[] {
const lines: string[] = [];
const prefix = limitName ? `${limitName} ` : "";

if (rateLimits.primary) {
const percentLeft = Math.round(100 - rateLimits.primary.usedPercent);
const resetText = this.formatResetTime(rateLimits.primary.resetsAt);
const label = this.formatWindowLabel(rateLimits.primary.windowDurationMins);
lines.push(`**${label}:** ${percentLeft}% left${resetText}`);
lines.push(`**${prefix}${label}:** ${percentLeft}% left${resetText}`);
}

if (rateLimits.secondary) {
const percentLeft = Math.round(100 - rateLimits.secondary.usedPercent);
const resetText = this.formatResetTime(rateLimits.secondary.resetsAt);
const label = this.formatWindowLabel(rateLimits.secondary.windowDurationMins);
lines.push(`**${label}:** ${percentLeft}% left${resetText}`);
lines.push(`**${prefix}${label}:** ${percentLeft}% left${resetText}`);
}

if (rateLimits.credits) {
if (rateLimits.credits.unlimited) {
lines.push(`**Credits:** unlimited`);
lines.push(`**${prefix}Credits:** unlimited`);
} else if (rateLimits.credits.balance) {
lines.push(`**Credits:** ${rateLimits.credits.balance}`);
lines.push(`**${prefix}Credits:** ${rateLimits.credits.balance}`);
}
}

return lines.length > 0 ? lines : [`**Limits:** data not available yet`];
return lines;
}

private formatWindowLabel(windowDurationMins: number | null): string {
Expand Down
17 changes: 16 additions & 1 deletion src/CodexEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ export class CodexEventHandler {
case "configWarning":
return await this.createConfigWarningEvent(notification.params);
case "thread/compacted":
return {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "*Context compacted to fit the model's context window.*\n\n"
}
};
case "windows/worldWritableWarning":
case "account/login/completed":
case "authStatusChange":
Expand All @@ -131,6 +138,7 @@ export class CodexEventHandler {
case "thread/started":
case "thread/name/updated":
case "item/plan/delta":
case "app/list/updated":
return null;
}
}
Expand Down Expand Up @@ -450,6 +458,13 @@ export class CodexEventHandler {
}

private handleRateLimitsUpdated(params: AccountRateLimitsUpdatedNotification): void {
this.sessionState.rateLimits = params.rateLimits;
if (!this.sessionState.rateLimits) {
this.sessionState.rateLimits = new Map();
}
this.sessionState.rateLimits.set(params.limitId, {
limitId: params.limitId,
limitName: params.limitName,
snapshot: params.rateLimits,
});
}
}
9 changes: 9 additions & 0 deletions src/RateLimitsMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type {RateLimitSnapshot} from "./app-server/v2";

export type RateLimitEntry = {
limitId: string;
limitName: string;
snapshot: RateLimitSnapshot;
};

export type RateLimitsMap = Map<string, RateLimitEntry>;
Loading