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
63 changes: 51 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,70 @@
# 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 matching requests when `/fast` is enabled.

## What it does
## What It Does

- 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`
- By default, injects `service_tier: "priority"` into requests whose URL contains `/backend-api/codex/responses`
- Supports additional third-party URL prefixes through plugin options in `opencode.json`
- Leaves all other requests untouched
- Supports configuring startup `enabled` state in plugin options
- Keeps backward compatibility with `~/.config/opencode/opencodex-fast.jsonc`

## Commands

```text
/fast Toggle fast mode globally
/fast Toggle fast mode
/fast on Enable fast mode
/fast off Disable fast mode
/fast status Show current global fast-mode state
/fast status Show current fast-mode state
```

## Installation

Add to your OpenCode config:
Add the plugin to your OpenCode config:

```jsonc
// opencode.jsonc
```json
{
"plugin": ["opencodex-fast@latest"],
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencodex-fast@latest"]
}
```

## Configuration

You can pass plugin options directly on the `opencodex-fast` plugin entry in `opencode.json`:

```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"@example-org/one-plugin",
[
"opencodex-fast@latest",
{
"enabled": true,
"extraUrlPrefixes": [
"https://third-party.example.com/v1",
"https://proxy.example.com/backend-api/codex/responses"
]
}
],
"@example-org/another-plugin"
]
}
```

### Options

- `enabled`: starts OpenCode with fast mode already enabled so you do not need to run `/fast` manually
- `extraUrlPrefixes`: additional URL prefixes to match for priority injection, such as a provider `baseUrl` or a more specific endpoint URL; these are added on top of the built-in Codex matcher, not used instead of it

### Behavior Notes

- The built-in Codex matcher `/backend-api/codex/responses` is always kept; `extraUrlPrefixes` only adds more matches
- Use a provider `baseUrl` for broader matching, or a more specific endpoint URL prefix for tighter matching
- If plugin config sets `enabled`, that value takes precedence on startup
- If plugin config does not set `enabled`, the plugin falls back to `~/.config/opencode/opencodex-fast.jsonc`
- `/fast` still updates the current session and writes the state file for backward compatibility
- When plugin config sets `enabled`, restarting OpenCode restores the configured value even if `/fast` changed it during the previous session
91 changes: 76 additions & 15 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import type { Plugin } from "@opencode-ai/plugin";
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
import type { Config, Part } from "@opencode-ai/sdk";

const DEFAULT_CODEX_URL_MATCHER = "/backend-api/codex/responses";
const FAST_ON_MESSAGE = "Fast mode is now ON.";
const FAST_OFF_MESSAGE = "Fast mode is now OFF.";
const FAST_HANDLED_ERROR = "__FAST_HANDLED__";
Expand All @@ -18,7 +20,14 @@ const STATE_PATH = join(
"opencodex-fast.jsonc",
);

type FastPluginOptions = {
enabled?: unknown;
extraUrlPrefixes?: unknown;
};

let fastEnabled = false;
let configuredEnabled: boolean | undefined;
let extraUrlPrefixes: string[] = [];

function ensureStateDir(): void {
mkdirSync(dirname(STATE_PATH), { recursive: true });
Expand All @@ -30,8 +39,33 @@ function resolveUrl(input: any): string {
return input?.url ?? "";
}

function isCodexUrl(url: string): boolean {
return url.includes("/backend-api/codex/responses");
function normalizeUrlPrefixList(input: unknown): string[] {
if (!Array.isArray(input)) return [];

return input
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean);
}

function trimTrailingSlashes(value: string): string {
return value.replace(/\/+$/, "");
}

function matchesUrlPrefix(url: string, prefix: string): boolean {
const normalizedUrl = trimTrailingSlashes(url);
const normalizedPrefix = trimTrailingSlashes(prefix);
return (
normalizedUrl === normalizedPrefix ||
normalizedUrl.startsWith(`${normalizedPrefix}?`) ||
normalizedUrl.startsWith(`${normalizedPrefix}#`) ||
normalizedUrl.startsWith(`${normalizedPrefix}/`)
);
}

function matchesConfiguredUrl(url: string): boolean {
if (url.includes(DEFAULT_CODEX_URL_MATCHER)) return true;
return extraUrlPrefixes.some((prefix) => matchesUrlPrefix(url, prefix));
}

function parseBody(body: unknown): Record<string, unknown> | null {
Expand Down Expand Up @@ -71,9 +105,33 @@ function readState(): boolean {
}
}

function resolveConfiguredEnabled(options: FastPluginOptions | undefined):
| boolean
| undefined {
return typeof options?.enabled === "boolean" ? options.enabled : undefined;
}

function resolveInitialEnabled(options: FastPluginOptions | undefined): boolean {
const enabled = resolveConfiguredEnabled(options);
if (enabled !== undefined) return enabled;
return readState();
}

function resolveExtraUrlPrefixes(
options: FastPluginOptions | undefined,
): string[] {
return Array.from(new Set(normalizeUrlPrefixList(options?.extraUrlPrefixes)));
}

function appendConfigNote(message: string): string {
if (configuredEnabled === undefined) return message;
if (fastEnabled === configuredEnabled) return message;
return `${message} Restart will restore the plugin-configured ${configuredEnabled ? "ON" : "OFF"} state.`;
}

function maybeInjectPriority(init: any, input: any): any {
const url = resolveUrl(input);
if (!isCodexUrl(url)) return init;
if (!matchesConfiguredUrl(url)) return init;
if (!fastEnabled) return init;

const body = parseBody(init?.body);
Expand Down Expand Up @@ -115,32 +173,35 @@ function getFastMessage(modeArg?: string): string {
if (normalized === "on") {
fastEnabled = true;
writeState(true);
return FAST_ON_MESSAGE;
return appendConfigNote(FAST_ON_MESSAGE);
}

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

if (normalized === "status") {
return fastEnabled ? FAST_ON_MESSAGE : FAST_OFF_MESSAGE;
return appendConfigNote(fastEnabled ? FAST_ON_MESSAGE : FAST_OFF_MESSAGE);
}

if (fastEnabled) {
fastEnabled = false;
writeState(false);
return FAST_OFF_MESSAGE;
return appendConfigNote(FAST_OFF_MESSAGE);
}

fastEnabled = true;
writeState(true);
return FAST_ON_MESSAGE;
return appendConfigNote(FAST_ON_MESSAGE);
}

const plugin: Plugin = async (ctx) => {
fastEnabled = readState();
const plugin = (async (ctx: PluginInput, options?: unknown) => {
const fastOptions = options as FastPluginOptions | undefined;
configuredEnabled = resolveConfiguredEnabled(fastOptions);
fastEnabled = resolveInitialEnabled(fastOptions);
extraUrlPrefixes = resolveExtraUrlPrefixes(fastOptions);
const originalFetch = globalThis.fetch;

globalThis.fetch = async (input: any, init?: any) => {
Expand All @@ -149,17 +210,17 @@ const plugin: Plugin = async (ctx) => {
};

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

"command.execute.before": async (
input: { command: string; sessionID: string; arguments: string },
_output: { parts: any[] },
_output: { parts: Part[] },
) => {
if (input.command !== "fast") {
return;
Expand All @@ -170,6 +231,6 @@ const plugin: Plugin = async (ctx) => {
throw new Error(FAST_HANDLED_ERROR);
},
};
};
}) as Plugin;

export default plugin;
1 change: 1 addition & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"devDependencies": {
"@opencode-ai/plugin": "^1.1.49",
"@opencode-ai/sdk": "^1.3.3",
"@types/node": "^25.1.0",
"typescript": "^5.9.3"
}
Expand Down