Skip to content
Open
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
43 changes: 24 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
# opencodex-fast

An OpenCode plugin that adds `"service_tier": "priority"` to Codex requests when `/fast` is enabled globally.
An OpenCode plugin that adds `"service_tier": "priority"` to eligible Codex requests for a Fast-enabled chat session.

## What it does
## Per-session Fast mode

- Adds a `/fast` command to OpenCode
- When enabled, injects `service_tier: "priority"` into requests sent to `https://chatgpt.com/backend-api/codex/responses`
- Mirrors Codex Fast mode, which is documented as 1.5x faster at 2x credit cost
- Leaves all non-Codex requests untouched
- Persists a single global `enabled` flag in `~/.config/opencode/opencodex-fast.jsonc`
- `/fast`, `/fast on`, `/fast off`, and `/fast status` apply only to the current session.
- Session metadata is the source of truth, so the same session stays synchronized in multiple TUI windows. Different sessions remain isolated.
- The server entry marks only Fast-enabled sessions. The request wrapper removes that private marker before forwarding every request and injects priority only for the Codex endpoint (`/backend-api/codex/responses`), leaving OpenAI API-key requests untouched.
- The legacy global `~/.config/opencode/opencodex-fast.jsonc` file is ignored and never modified.

## Commands
## Terminal TUI

```text
/fast Toggle fast mode globally
/fast on Enable fast mode
/fast off Disable fast mode
/fast status Show current global fast-mode state
```
Install the TUI entry as well as the server entry to get indicators and `Ctrl+Y`:

- In a session, `Ctrl+Y` toggles Fast for that session. The indicator is driven by reactive session metadata.
- On the home screen, `Ctrl+Y` arms Fast only in that TUI window. Its indicator shows the local arm/reservation state. The next newly created session in that window inherits Fast before its first request; the arm is one-shot and nonpersistent.
- Home-screen arming does not affect other TUI windows or existing sessions.

## Installation

Add to your OpenCode config:
For OpenCode Desktop or server-only use, add the package only to the OpenCode configuration:

```jsonc
// opencode.jsonc
{
"plugin": ["opencodex-fast@latest"],
}
// opencode.jsonc (server entry; Desktop and terminal)
{ "plugin": ["opencodex-fast@latest"] }
```

For the complete terminal experience (session indicators and `Ctrl+Y`), add it to both `opencode.jsonc` and the TUI configuration:

```jsonc
// tui.jsonc (terminal TUI entry)
{ "plugin": ["opencodex-fast@latest"] }
```

OpenCode 1.18.1+ is required for the TUI integration.
24 changes: 24 additions & 0 deletions fast-session-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const FAST_METADATA_KEY = "opencodex-fast.enabled";

export type SessionMetadata = Record<string, unknown>;

export function normalizeMetadata(metadata: unknown): SessionMetadata {
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
return {};
}
return { ...metadata };
}

export function isFastEnabled(metadata: unknown): boolean {
return normalizeMetadata(metadata)[FAST_METADATA_KEY] === true;
}

export function withFastEnabled(metadata: unknown, enabled: boolean): SessionMetadata {
const next = normalizeMetadata(metadata);
if (enabled) {
next[FAST_METADATA_KEY] = true;
} else {
delete next[FAST_METADATA_KEY];
}
return next;
}
228 changes: 98 additions & 130 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,172 +1,140 @@
import {
existsSync,
mkdirSync,
readFileSync,
renameSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import type { Plugin } from "@opencode-ai/plugin";

const FAST_ON_MESSAGE = "Fast mode is now ON.";
const FAST_OFF_MESSAGE = "Fast mode is now OFF.";
const FAST_HANDLED_ERROR = "__FAST_HANDLED__";
const STATE_PATH = join(
process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
"opencode",
"opencodex-fast.jsonc",
);
import { isFastEnabled, normalizeMetadata, withFastEnabled } from "./fast-session-state.js";

let fastEnabled = false;
const FAST_ON_MESSAGE = "Fast mode is now ON for this session.";
const FAST_OFF_MESSAGE = "Fast mode is now OFF for this session.";
const FAST_HANDLED_ERROR = "__FAST_HANDLED__";
const FAST_HEADER = "x-opencodex-fast";

function ensureStateDir(): void {
mkdirSync(dirname(STATE_PATH), { recursive: true });
}
type LegacySessionClient = {
get: (input: { path: { id: string } }) => Promise<{ data?: unknown; error?: unknown }>;
update: (input: { path: { id: string }; body: { metadata: Record<string, unknown> } }) => Promise<{ error?: unknown }>;
prompt: (input: unknown) => Promise<unknown>;
};

function resolveUrl(input: any): string {
function resolveUrl(input: unknown): string {
if (typeof input === "string") return input;
if (input instanceof URL) return input.href;
return input?.url ?? "";
return input && typeof input === "object" && "url" in input && typeof input.url === "string"
? input.url
: "";
}

function isCodexUrl(url: string): boolean {
return url.includes("/backend-api/codex/responses");
}

function parseBody(body: unknown): Record<string, unknown> | null {
if (typeof body !== "string") return null;

try {
const parsed = JSON.parse(body);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return null;
}
return parsed as Record<string, unknown>;
const parsed = new URL(url);
return parsed.protocol === "https:"
&& parsed.hostname === "chatgpt.com"
&& parsed.pathname.endsWith("/backend-api/codex/responses");
} catch {
return null;
return false;
}
}

function writeState(enabled: boolean): void {
ensureStateDir();
const tempPath = `${STATE_PATH}.tmp`;
const content = `${JSON.stringify({ enabled }, null, 2)}\n`;
writeFileSync(tempPath, content, "utf8");
renameSync(tempPath, STATE_PATH);
}

function readState(): boolean {
function parseBody(body: unknown): Record<string, unknown> | undefined {
if (typeof body !== "string") return undefined;
try {
if (!existsSync(STATE_PATH)) {
writeState(false);
return false;
}

const raw = readFileSync(STATE_PATH, "utf8");
const parsed = JSON.parse(raw) as { enabled?: unknown };
return parsed.enabled === true;
const parsed: unknown = JSON.parse(body);
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: undefined;
} catch {
return false;
return undefined;
}
}

function maybeInjectPriority(init: any, input: any): any {
const url = resolveUrl(input);
if (!isCodexUrl(url)) return init;
if (!fastEnabled) return init;
/** Removes the private session marker and injects priority only for Codex OAuth requests. */
export function prepareFastRequest(input: unknown, init?: RequestInit): RequestInit | undefined {
const inputHeaders = input && typeof input === "object" && "headers" in input
? (input as { headers?: RequestInit["headers"] }).headers
: undefined;
const headers = new Headers(init?.headers ?? inputHeaders);
const enabled = headers.get(FAST_HEADER) === "true";
headers.delete(FAST_HEADER);
const next: RequestInit = { ...init, headers };
if (!enabled || !isCodexUrl(resolveUrl(input))) return next;

const body = parseBody(init?.body);
if (!body) return init;
if (body.service_tier === "priority") return init;

return {
...init,
body: JSON.stringify({
...body,
service_tier: "priority",
}),
};
if (!body || body.service_tier === "priority") return next;
return { ...next, body: JSON.stringify({ ...body, service_tier: "priority" }) };
}

async function sendIgnoredMessage(
client: any,
sessionID: string,
text: string,
): Promise<void> {
await client.session.prompt({
path: { id: sessionID },
body: {
noReply: true,
parts: [
{
type: "text",
text,
ignored: true,
},
],
},
});
function legacySessionClient(client: unknown): LegacySessionClient {
return (client as { session: LegacySessionClient }).session;
}

function getFastMessage(modeArg?: string): string {
const normalized = modeArg?.toLowerCase();

if (normalized === "on") {
fastEnabled = true;
writeState(true);
return FAST_ON_MESSAGE;
}

if (normalized === "off") {
fastEnabled = false;
writeState(false);
return FAST_OFF_MESSAGE;
}

if (normalized === "status") {
return fastEnabled ? FAST_ON_MESSAGE : FAST_OFF_MESSAGE;
}
function metadataFromResponse(data: unknown): Record<string, unknown> {
if (!data || typeof data !== "object") return {};
return normalizeMetadata((data as { metadata?: unknown }).metadata);
}

if (fastEnabled) {
fastEnabled = false;
writeState(false);
return FAST_OFF_MESSAGE;
}
async function readSessionMetadata(client: unknown, sessionID: string): Promise<Record<string, unknown>> {
const result = await legacySessionClient(client).get({ path: { id: sessionID } });
if (result.error || !result.data) throw new Error("Could not read session metadata.");
return metadataFromResponse(result.data);
}

fastEnabled = true;
writeState(true);
return FAST_ON_MESSAGE;
async function sendIgnoredMessage(client: unknown, sessionID: string, text: string): Promise<void> {
await legacySessionClient(client).prompt({
path: { id: sessionID },
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
});
}

const plugin: Plugin = async (ctx) => {
fastEnabled = readState();
const originalFetch = globalThis.fetch;

globalThis.fetch = async (input: any, init?: any) => {
const nextInit = maybeInjectPriority(init, input);
return originalFetch(input, nextInit);
const queues = new Map<string, Promise<void>>();
globalThis.fetch = async (input, init) => originalFetch(input, prepareFastRequest(input, init));

const queueSessionWrite = (
sessionID: string,
operation: (metadata: Record<string, unknown>) => boolean,
): Promise<boolean> => {
const previous = queues.get(sessionID) ?? Promise.resolve();
let enabled = false;
const next = previous.catch(() => undefined).then(async () => {
const metadata = await readSessionMetadata(ctx.client, sessionID);
enabled = operation(metadata);
const result = await legacySessionClient(ctx.client).update({
path: { id: sessionID },
body: { metadata: withFastEnabled(metadata, enabled) },
});
if (result.error) throw new Error("Could not update session Fast mode.");
});
queues.set(sessionID, next);
return next.then(() => enabled).finally(() => {
if (queues.get(sessionID) === next) queues.delete(sessionID);
});
};

return {
config: async (opencodeConfig) => {
opencodeConfig.command ??= {};
opencodeConfig.command["fast"] = {
template: "[on|off|status]",
description: "Toggle Codex priority service tier injection",
};
opencodeConfig.command.fast = { template: "[on|off|status]", description: "Toggle Codex priority service tier for this session" };
},

"command.execute.before": async (
input: { command: string; sessionID: string; arguments: string },
_output: { parts: any[] },
) => {
if (input.command !== "fast") {
return;
"chat.headers": async (input, output) => {
try {
if (isFastEnabled(await readSessionMetadata(ctx.client, input.sessionID))) {
output.headers[FAST_HEADER] = "true";
}
} catch {
// Fail closed: without session metadata, never mark the request Fast.
}

const message = getFastMessage(input.arguments.trim() || undefined);
await sendIgnoredMessage(ctx.client, input.sessionID, message);
},
"command.execute.before": async (input) => {
if (input.command !== "fast") return;
const mode = input.arguments.trim().toLowerCase();
if (mode === "status") {
const enabled = isFastEnabled(await readSessionMetadata(ctx.client, input.sessionID));
await sendIgnoredMessage(ctx.client, input.sessionID, enabled ? FAST_ON_MESSAGE : FAST_OFF_MESSAGE);
throw new Error(FAST_HANDLED_ERROR);
}
const enabled = await queueSessionWrite(
input.sessionID,
(metadata) => mode === "on" ? true : mode === "off" ? false : !isFastEnabled(metadata),
);
await sendIgnoredMessage(ctx.client, input.sessionID, enabled ? FAST_ON_MESSAGE : FAST_OFF_MESSAGE);
throw new Error(FAST_HANDLED_ERROR);
},
};
Expand Down
Loading