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
17 changes: 17 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

## [Unreleased]

### Added

- Added `/fast` Codex fast mode toggles for chat and workflow-stage sessions, applying OpenAI priority service tier to supported `openai/*` and `openai-codex/*` models only; active supported models now show a visible `fast` indicator after the model name ([#1134](https://github.com/flora131/atomic/issues/1134)).

### Changed

- Refined the `/fast` selector into a conventional toggle UI with on/off states, clearer scope descriptions, and space/enter toggle support ([#1134](https://github.com/flora131/atomic/issues/1134)).
- Compressed the `/fast` selector copy, row layout, and per-change status message so the summary, toggles, scopes, and keyboard hints stay readable without duplicate off/standard-tier messaging ([#1134](https://github.com/flora131/atomic/issues/1134)).

### Fixed

- Preserved custom registered provider streamers when Codex fast mode is enabled for native OpenAI response APIs ([#1134](https://github.com/flora131/atomic/issues/1134)).
- Fixed `/fast` changes so the banner/footer and current session update immediately, and inherited chat fast-mode state now reaches subagent child sessions without waiting for a restart ([#1134](https://github.com/flora131/atomic/issues/1134)).
- Fixed `/fast` persistence so existing project-level fast-mode overrides are updated alongside global settings for the changed scope without clobbering untouched global chat or workflow fast-mode preferences ([#1134](https://github.com/flora131/atomic/issues/1134)).
- Made Codex fast-mode request helpers require an explicit enabled flag and treat `service_tier: undefined` as unset when preparing OpenAI payloads ([#1134](https://github.com/flora131/atomic/issues/1134)).
- Fixed attached workflow-stage chat footers to resolve the `fast` model indicator against workflow fast-mode settings instead of chat settings ([#1134](https://github.com/flora131/atomic/issues/1134)).

## [0.8.21] - 2026-05-30

### Changed
Expand Down
4 changes: 4 additions & 0 deletions packages/coding-agent/docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ Use `/logout` to clear credentials. Tokens are stored in `~/.atomic/agent/auth.j
- Requires ChatGPT Plus or Pro subscription
- Officially endorsed by OpenAI: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)

### Codex Fast Mode

Run `/fast` in interactive mode to enable OpenAI priority service tier separately for normal chat and workflow-stage sessions. The command is shown only when the current model scope includes a supported `openai/*` or `openai-codex/*` model. When enabled for the active supported model, the UI appends `fast` after the model name in the chat footer and workflow stage model labels. Fast mode intentionally does not apply to `github-copilot/*`, Azure OpenAI, OpenRouter, or custom OpenAI-compatible providers.

### Claude Pro/Max

Anthropic subscription auth is active for Claude Pro/Max accounts. Third-party harness usage draws from [extra usage](https://claude.ai/settings/usage) and is billed per token, not against Claude plan limits.
Expand Down
18 changes: 18 additions & 0 deletions packages/coding-agent/docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ Edit directly or use `/settings` for common options. Atomic reads legacy `~/.pi/
}
```

### Codex Fast Mode

Use `/fast` in interactive mode to edit these settings. Atomic applies fast mode only to supported `openai/*` and `openai-codex/*` providers, not `github-copilot/*` or other OpenAI-compatible providers. When fast mode is active for the current supported model, Atomic shows `fast` after the model name in the chat footer and workflow stage model labels.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `codexFastMode.chat` | boolean | `false` | Use OpenAI priority service tier for supported normal chat requests |
| `codexFastMode.workflow` | boolean | `false` | Use OpenAI priority service tier for supported workflow-stage requests |

```json
{
"codexFastMode": {
"chat": true,
"workflow": false
}
}
```

### UI & Display

| Setting | Type | Default | Description |
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Type `/` in the editor to open command completion. Extensions can register custo
| `/login`, `/logout` | Manage OAuth or API-key credentials |
| `/model` | Switch models |
| `/scoped-models` | Enable/disable models for CTRL+P cycling |
| `/fast` | Toggle Codex fast mode for chat and workflow stages when `openai/*` or `openai-codex/*` models are available |
| `/settings` | Thinking level, theme, message delivery, transport |
| `/resume` | Pick from previous sessions |
| `/new` | Start a new session |
Expand Down
50 changes: 50 additions & 0 deletions packages/coding-agent/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,58 @@ export const ENV_SHARE_VIEWER_URL = `${ENV_PREFIX}_SHARE_VIEWER_URL`;
export const ENV_CLEAR_ON_SHRINK = `${ENV_PREFIX}_CLEAR_ON_SHRINK`;
export const ENV_HARDWARE_CURSOR = `${ENV_PREFIX}_HARDWARE_CURSOR`;
export const ENV_TIMING = `${ENV_PREFIX}_TIMING`;
export const ENV_CODEX_FAST_MODE = `${ENV_PREFIX}_CODEX_FAST_MODE`;
export const WORKFLOW_STAGE_SUBAGENT_GUARD_ENV = `${ENV_PREFIX}_WORKFLOW_STAGE_SUBAGENT_GUARD`;

export interface CodexFastModeEnvironmentSettings {
chat?: boolean;
workflow?: boolean;
}

function parseCodexFastModeEnvBoolean(value: string | undefined): boolean | undefined {
switch (value?.trim().toLowerCase()) {
case "1":
case "true":
case "enabled":
case "on":
return true;
case "0":
case "false":
case "disabled":
case "off":
return false;
default:
return undefined;
}
}

export function serializeCodexFastModeEnvironmentSettings(settings: Required<CodexFastModeEnvironmentSettings>): string {
return `chat=${settings.chat ? "1" : "0"};workflow=${settings.workflow ? "1" : "0"}`;
}

export function parseCodexFastModeEnvironmentSettings(value: string | undefined): CodexFastModeEnvironmentSettings | undefined {
if (!value) return undefined;
const settings: CodexFastModeEnvironmentSettings = {};
for (const part of value.split(/[;,]/)) {
const separatorIndex = part.indexOf("=");
if (separatorIndex === -1) continue;
const key = part.slice(0, separatorIndex).trim();
const parsedValue = parseCodexFastModeEnvBoolean(part.slice(separatorIndex + 1));
if (parsedValue === undefined) continue;
if (key === "chat") settings.chat = parsedValue;
if (key === "workflow") settings.workflow = parsedValue;
}
return settings.chat !== undefined || settings.workflow !== undefined ? settings : undefined;
}

export function getCodexFastModeEnvironmentSettings(): CodexFastModeEnvironmentSettings | undefined {
return parseCodexFastModeEnvironmentSettings(getEnvValue(ENV_CODEX_FAST_MODE));
}

export function setCodexFastModeEnvironmentSettings(settings: Required<CodexFastModeEnvironmentSettings>): void {
setEnvValue(ENV_CODEX_FAST_MODE, serializeCodexFastModeEnvironmentSettings(settings));
}

export function getEnvNames(name: string): string[] {
if (ENV_PREFIX === LEGACY_ENV_PREFIX || !name.startsWith(`${ENV_PREFIX}_`)) return [name];
return [name, `${LEGACY_ENV_PREFIX}_${name.slice(ENV_PREFIX.length + 1)}`];
Expand Down
5 changes: 5 additions & 0 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,11 @@ export class AgentSession {
});
}

/** Orchestration context for this session, when owned by a workflow/subagent runtime. */
get orchestrationContext(): OrchestrationContext | undefined {
return this._orchestrationContext;
}

/** Model registry for API key resolution and model discovery */
get modelRegistry(): ModelRegistry {
return this._modelRegistry;
Expand Down
233 changes: 233 additions & 0 deletions packages/coding-agent/src/core/codex-fast-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import {
clampThinkingLevel,
type Api,
type AssistantMessageEventStream,
type Context,
type Model,
type OpenAICodexResponsesOptions,
streamOpenAICodexResponses,
streamOpenAIResponses,
streamSimple,
type OpenAIResponsesOptions,
type SimpleStreamOptions,
type StreamOptions,
type ThinkingLevel,
} from "@earendil-works/pi-ai";
import type { OrchestrationContext } from "./extensions/index.ts";

export const CODEX_FAST_MODE_SERVICE_TIER = "priority" as const;

export interface CodexFastModeResolvedSettings {
chat: boolean;
workflow: boolean;
}

export type CodexFastModeScope = "chat" | "workflow";

export interface CodexFastModeStreamOptions extends SimpleStreamOptions {
serviceTier?: typeof CODEX_FAST_MODE_SERVICE_TIER;
}

export interface CodexFastModeStreamers {
streamSimple: (
model: Model<Api>,
context: Context,
options?: SimpleStreamOptions,
) => AssistantMessageEventStream;
streamOpenAIResponses: (
model: Model<"openai-responses">,
context: Context,
options?: OpenAIResponsesOptions,
) => AssistantMessageEventStream;
streamOpenAICodexResponses: (
model: Model<"openai-codex-responses">,
context: Context,
options?: OpenAICodexResponsesOptions,
) => AssistantMessageEventStream;
}

const DEFAULT_CODEX_FAST_MODE_STREAMERS: CodexFastModeStreamers = {
streamSimple,
streamOpenAIResponses,
streamOpenAICodexResponses,
};

export function isCodexFastModeSupportedProvider(provider: string): boolean {
return provider === "openai" || provider === "openai-codex";
}

export function isCodexFastModeCandidateModelId(modelId: string | undefined): boolean {
const provider = modelId?.split("/", 1)[0];
return provider !== undefined && isCodexFastModeSupportedProvider(provider);
}

export function isCodexFastModeSupportedModel(model: Pick<Model<Api>, "provider">): boolean {
return isCodexFastModeSupportedProvider(model.provider);
}

export function hasSupportedCodexFastModeModel(models: readonly Pick<Model<Api>, "provider">[]): boolean {
return models.some(isCodexFastModeSupportedModel);
}

export function isWorkflowStageOrchestrationContext(context: OrchestrationContext | undefined): boolean {
return context?.kind === "workflow-stage";
}

export function getCodexFastModeScope(context: OrchestrationContext | undefined): CodexFastModeScope {
return isWorkflowStageOrchestrationContext(context) ? "workflow" : "chat";
}

export function isCodexFastModeEnabledForScope(
settings: CodexFastModeResolvedSettings,
scope: CodexFastModeScope,
): boolean {
return settings[scope];
}

export function isCodexFastModeEnabledForSession(
settings: CodexFastModeResolvedSettings,
context: OrchestrationContext | undefined,
): boolean {
return isCodexFastModeEnabledForScope(settings, getCodexFastModeScope(context));
}

export function shouldApplyCodexFastModeForScope(
model: Pick<Model<Api>, "provider">,
settings: CodexFastModeResolvedSettings,
scope: CodexFastModeScope,
): boolean {
return isCodexFastModeSupportedModel(model) && isCodexFastModeEnabledForScope(settings, scope);
}

export function shouldApplyCodexFastMode(
model: Pick<Model<Api>, "provider">,
settings: CodexFastModeResolvedSettings,
context: OrchestrationContext | undefined,
): boolean {
return shouldApplyCodexFastModeForScope(model, settings, getCodexFastModeScope(context));
}

export function withCodexFastModeStreamOptions(
options: SimpleStreamOptions | undefined,
enabled: boolean,
): CodexFastModeStreamOptions | undefined {
if (!enabled) {
return options;
}

return {
...(options ?? {}),
serviceTier: CODEX_FAST_MODE_SERVICE_TIER,
};
}

export function isCodexFastModeNativeApi(api: Api): api is "openai-responses" | "openai-codex-responses" {
return api === "openai-responses" || api === "openai-codex-responses";
}

export function shouldUseNativeCodexFastMode(
model: Pick<Model<Api>, "api" | "provider">,
options: CodexFastModeStreamOptions | undefined,
): boolean {
return (
isCodexFastModeSupportedModel(model) &&
isCodexFastModeNativeApi(model.api) &&
options?.serviceTier === CODEX_FAST_MODE_SERVICE_TIER
);
}

function buildCodexFastModeBaseProviderOptions(
options: CodexFastModeStreamOptions | undefined,
): StreamOptions {
return {
temperature: options?.temperature,
maxTokens: options?.maxTokens,
signal: options?.signal,
apiKey: options?.apiKey,
transport: options?.transport,
cacheRetention: options?.cacheRetention,
sessionId: options?.sessionId,
onPayload: options?.onPayload,
onResponse: options?.onResponse,
headers: options?.headers,
timeoutMs: options?.timeoutMs,
websocketConnectTimeoutMs: options?.websocketConnectTimeoutMs,
maxRetries: options?.maxRetries,
maxRetryDelayMs: options?.maxRetryDelayMs,
metadata: options?.metadata,
};
}

export function mapCodexFastModeReasoningEffort(
model: Model<Api>,
reasoning: ThinkingLevel | undefined,
): ThinkingLevel | undefined {
const clampedReasoning = reasoning ? clampThinkingLevel(model, reasoning) : undefined;
return clampedReasoning === "off" ? undefined : clampedReasoning;
}

export function buildOpenAIResponsesCodexFastModeOptions(
model: Model<Api>,
options: CodexFastModeStreamOptions | undefined,
): OpenAIResponsesOptions {
return {
...buildCodexFastModeBaseProviderOptions(options),
reasoningEffort: mapCodexFastModeReasoningEffort(model, options?.reasoning),
serviceTier: options?.serviceTier,
};
}

export function buildOpenAICodexResponsesCodexFastModeOptions(
model: Model<Api>,
options: CodexFastModeStreamOptions | undefined,
): OpenAICodexResponsesOptions {
return {
...buildCodexFastModeBaseProviderOptions(options),
reasoningEffort: mapCodexFastModeReasoningEffort(model, options?.reasoning),
serviceTier: options?.serviceTier,
};
}

export function streamWithCodexFastMode(
model: Model<Api>,
context: Context,
options: CodexFastModeStreamOptions | undefined,
streamers: CodexFastModeStreamers = DEFAULT_CODEX_FAST_MODE_STREAMERS,
): AssistantMessageEventStream {
if (shouldUseNativeCodexFastMode(model, options)) {
if (model.api === "openai-responses") {
return streamers.streamOpenAIResponses(
model as Model<"openai-responses">,
context,
buildOpenAIResponsesCodexFastModeOptions(model, options),
);
}

return streamers.streamOpenAICodexResponses(
model as Model<"openai-codex-responses">,
context,
buildOpenAICodexResponsesCodexFastModeOptions(model, options),
);
}

return streamers.streamSimple(model, context, options);
}

function isObjectPayload(payload: unknown): payload is Record<string, unknown> {
return typeof payload === "object" && payload !== null && !Array.isArray(payload);
}

export function withCodexFastModePayload(payload: unknown, enabled: boolean): unknown {
if (!enabled || !isObjectPayload(payload) || payload.service_tier !== undefined) {
return payload;
}

return {
...payload,
service_tier: CODEX_FAST_MODE_SERVICE_TIER,
};
}

export function formatCodexFastModeModelLabel(modelName: string, enabled: boolean): string {
return enabled ? `${modelName} fast` : modelName;
}
12 changes: 12 additions & 0 deletions packages/coding-agent/src/core/model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,18 @@ export class ModelRegistry {
this.upsertRegisteredProvider(providerName, config);
}

/**
* Check whether extensions have registered custom streamSimple dispatch for an API.
*/
hasRegisteredStreamSimpleForApi(api: Api): boolean {
for (const config of this.registeredProviders.values()) {
if (config.api === api && config.streamSimple) {
return true;
}
}
return false;
}

/**
* Unregister a previously registered provider.
*
Expand Down
Loading
Loading