diff --git a/README.md b/README.md index 52993a4..1a105e2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/index.ts b/index.ts index 19c30df..7cf0f0e 100644 --- a/index.ts +++ b/index.ts @@ -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__"; @@ -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 }); @@ -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 | null { @@ -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); @@ -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) => { @@ -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; @@ -170,6 +231,6 @@ const plugin: Plugin = async (ctx) => { throw new Error(FAST_HANDLED_ERROR); }, }; -}; +}) as Plugin; export default plugin; diff --git a/package-lock.json b/package-lock.json index 6742618..6fb24de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.1", "devDependencies": { "@opencode-ai/plugin": "^1.1.49", + "@opencode-ai/sdk": "^1.3.3", "@types/node": "^25.1.0", "typescript": "^5.9.3" }, diff --git a/package.json b/package.json index 8bb86f9..815acbe 100644 --- a/package.json +++ b/package.json @@ -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" }