mirror of
https://github.com/hanzoai/opencode-openai-codex-auth.git
synced 2026-09-24 17:01:02 +00:00
Release v1.0.2 - Smart caching and stability improvements
## π― Major Improvements ### Smart ETag-Based Caching - Replaced 24-hour TTL cache with HTTP ETag-based conditional requests - Only downloads instructions when content actually changes (304 Not Modified responses) - Significantly reduces GitHub API calls while staying up-to-date ### Release Tag Tracking for Stability - Now fetches Codex instructions from latest GitHub release tag instead of main branch - Ensures compatibility with ChatGPT Codex API (main branch may have unreleased features) - Prevents "Instructions are not valid" errors from bleeding-edge changes ## π Bug Fixes ### Model Normalization - Fixed default model fallback: unsupported models now default to `gpt-5` (not `gpt-5-codex`) - Preserves user's choice between `gpt-5` and `gpt-5-codex` when explicitly specified - Only codex model variants normalize to `gpt-5-codex` ### Error Prevention - Added `body.text` initialization check to prevent TypeError on `body.text.verbosity` - Improved error handling in request transformation ### Code Quality - Standardized all console.error prefixes to `[openai-codex-plugin]` - Updated documentation to reflect ETag caching implementation - Added npm version and downloads badges to README ## π Documentation - Updated README with accurate caching behavior description - Added npm package badges for version tracking - Clarified release-based fetching strategy Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# OpenAI ChatGPT OAuth Plugin for opencode
|
||||
|
||||
[](https://www.npmjs.com/package/opencode-openai-codex-auth)
|
||||
[](https://www.npmjs.com/package/opencode-openai-codex-auth)
|
||||
|
||||
This plugin enables opencode to use OpenAI's Codex backend via ChatGPT Plus/Pro OAuth authentication, allowing you to use your ChatGPT subscription instead of OpenAI Platform API credits.
|
||||
|
||||
## Features
|
||||
@@ -7,7 +10,7 @@ This plugin enables opencode to use OpenAI's Codex backend via ChatGPT Plus/Pro
|
||||
- β
ChatGPT Plus/Pro OAuth authentication
|
||||
- β
**Zero external dependencies** - Lightweight with only @openauthjs/openauth
|
||||
- β
**Auto-refreshing tokens** - Handles token expiration automatically
|
||||
- β
**Auto-updating Codex instructions** - Fetches latest from OpenAI's Codex repo (cached 24h)
|
||||
- β
**Smart auto-updating Codex instructions** - Tracks latest stable release with ETag caching
|
||||
- β
Full tool support (write, edit, bash, grep, etc.)
|
||||
- β
Automatic tool remapping (Codex tools β opencode tools)
|
||||
- β
High reasoning effort with detailed thinking blocks
|
||||
@@ -87,9 +90,12 @@ The plugin automatically configures:
|
||||
The plugin:
|
||||
|
||||
1. **Authentication**: Uses ChatGPT OAuth flow with PKCE for secure authentication
|
||||
2. **Token Management**: Auto-refreshes tokens using `ai-sdk-provider-chatgpt-oauth`
|
||||
3. **Codex Instructions**: Automatically fetches latest instructions from [openai/codex](https://github.com/openai/codex) repository
|
||||
- Cached locally in `~/.opencode/cache/` for 24 hours
|
||||
2. **Token Management**: Native token refresh implementation (no external dependencies)
|
||||
3. **Codex Instructions**: Automatically fetches from the latest stable release of [openai/codex](https://github.com/openai/codex)
|
||||
- Tracks latest release tag (not main branch) for stability
|
||||
- Uses ETag-based caching for efficient updates (only downloads when content changes)
|
||||
- Cached locally in `~/.opencode/cache/`
|
||||
- Auto-updates when OpenAI publishes new releases
|
||||
- Falls back to bundled version if GitHub is unavailable
|
||||
4. **Request Transformation**: Routes requests to `https://chatgpt.com/backend-api/codex/responses`
|
||||
5. **Model Normalization**: Maps all model names to `gpt-5-codex` (the Codex backend model)
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function OpenAIAuthPlugin({ client }) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Fetch Codex instructions (cached for 24h)
|
||||
// Fetch Codex instructions (cached with ETag)
|
||||
const CODEX_INSTRUCTIONS = await getCodexInstructions();
|
||||
|
||||
// Return options that will be passed to the OpenAI SDK
|
||||
@@ -100,21 +100,17 @@ export async function OpenAIAuthPlugin({ client }) {
|
||||
try {
|
||||
const body = JSON.parse(init.body);
|
||||
|
||||
// Normalize model name - Codex only supports specific model IDs
|
||||
// Map all variants to their base models
|
||||
// Normalize model name - Codex only supports gpt-5 and gpt-5-codex
|
||||
if (body.model) {
|
||||
if (body.model.includes("codex")) {
|
||||
// Any codex variant β gpt-5-codex
|
||||
body.model = "gpt-5-codex";
|
||||
} else if (
|
||||
body.model.includes("gpt-5") ||
|
||||
body.model.includes("gpt-nano")
|
||||
) {
|
||||
// gpt-5 variants β gpt-5-codex for best tool support
|
||||
body.model = "gpt-5-codex";
|
||||
} else if (body.model.includes("gpt-5")) {
|
||||
// gpt-5 variants β gpt-5 (keep user's choice)
|
||||
body.model = "gpt-5";
|
||||
} else {
|
||||
// Default fallback
|
||||
body.model = "gpt-5-codex";
|
||||
// Default fallback for unsupported models
|
||||
body.model = "gpt-5";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +149,9 @@ export async function OpenAIAuthPlugin({ client }) {
|
||||
}
|
||||
body.reasoning.effort = "high";
|
||||
body.reasoning.summary = "detailed";
|
||||
if (!body.text) {
|
||||
body.text = {};
|
||||
}
|
||||
body.text.verbosity = "medium";
|
||||
|
||||
// Remove unsupported parameters
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@ export async function exchangeAuthorizationCode(
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
console.error("[openai-codex-auth] code->token failed:", res.status, text);
|
||||
console.error("[openai-codex-plugin] code->token failed:", res.status, text);
|
||||
return { type: "failed" };
|
||||
}
|
||||
const json = await res.json();
|
||||
@@ -81,7 +81,7 @@ export async function exchangeAuthorizationCode(
|
||||
!json?.refresh_token ||
|
||||
typeof json?.expires_in !== "number"
|
||||
) {
|
||||
console.error("[openai-codex-auth] token response missing fields:", json);
|
||||
console.error("[openai-codex-plugin] token response missing fields:", json);
|
||||
return { type: "failed" };
|
||||
}
|
||||
return {
|
||||
|
||||
+67
-25
@@ -4,61 +4,103 @@ import { fileURLToPath } from "node:url";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
// Codex instructions constants
|
||||
const CODEX_INSTRUCTIONS_URL =
|
||||
"https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/gpt_5_codex_prompt.md";
|
||||
const GITHUB_API_RELEASES = "https://api.github.com/repos/openai/codex/releases/latest";
|
||||
const CACHE_DIR = join(homedir(), ".opencode", "cache");
|
||||
const CACHE_FILE = join(CACHE_DIR, "codex-instructions.md");
|
||||
const CACHE_METADATA_FILE = join(CACHE_DIR, "codex-instructions-meta.json");
|
||||
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
/**
|
||||
* Fetch Codex instructions from GitHub and cache them
|
||||
* Get the latest release tag from GitHub
|
||||
* @returns {Promise<string>} Release tag name (e.g., "rust-v0.43.0")
|
||||
*/
|
||||
async function getLatestReleaseTag() {
|
||||
const response = await fetch(GITHUB_API_RELEASES);
|
||||
if (!response.ok) throw new Error(`Failed to fetch latest release: ${response.status}`);
|
||||
const data = await response.json();
|
||||
return data.tag_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Codex instructions from GitHub with ETag-based caching
|
||||
* Uses HTTP conditional requests to efficiently check for updates
|
||||
* Always fetches from the latest release tag, not main branch
|
||||
* @returns {Promise<string>} Codex instructions
|
||||
*/
|
||||
export async function getCodexInstructions() {
|
||||
try {
|
||||
// Check if cache exists and is fresh
|
||||
// Get the latest release tag
|
||||
const latestTag = await getLatestReleaseTag();
|
||||
const CODEX_INSTRUCTIONS_URL = `https://raw.githubusercontent.com/openai/codex/${latestTag}/codex-rs/core/gpt_5_codex_prompt.md`;
|
||||
|
||||
// Load cached metadata (includes ETag and tag)
|
||||
let cachedETag = null;
|
||||
let cachedTag = null;
|
||||
if (existsSync(CACHE_METADATA_FILE)) {
|
||||
const metadata = JSON.parse(readFileSync(CACHE_METADATA_FILE, "utf8"));
|
||||
const age = Date.now() - metadata.timestamp;
|
||||
cachedETag = metadata.etag;
|
||||
cachedTag = metadata.tag;
|
||||
}
|
||||
|
||||
if (age < CACHE_TTL && existsSync(CACHE_FILE)) {
|
||||
// If tag changed, we need to fetch new instructions
|
||||
if (cachedTag !== latestTag) {
|
||||
cachedETag = null; // Force re-fetch
|
||||
}
|
||||
|
||||
// Make conditional request with If-None-Match header
|
||||
const headers = {};
|
||||
if (cachedETag) {
|
||||
headers["If-None-Match"] = cachedETag;
|
||||
}
|
||||
|
||||
const response = await fetch(CODEX_INSTRUCTIONS_URL, { headers });
|
||||
|
||||
// 304 Not Modified - our cached version is still current
|
||||
if (response.status === 304) {
|
||||
if (existsSync(CACHE_FILE)) {
|
||||
return readFileSync(CACHE_FILE, "utf8");
|
||||
}
|
||||
// Cache file missing but GitHub says not modified - fall through to re-fetch
|
||||
}
|
||||
|
||||
// Fetch fresh instructions from GitHub
|
||||
const response = await fetch(CODEX_INSTRUCTIONS_URL);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
// 200 OK - new content or first fetch
|
||||
if (response.ok) {
|
||||
const instructions = await response.text();
|
||||
const newETag = response.headers.get("etag");
|
||||
|
||||
const instructions = await response.text();
|
||||
// Create cache directory if it doesn't exist
|
||||
if (!existsSync(CACHE_DIR)) {
|
||||
mkdirSync(CACHE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Create cache directory if it doesn't exist
|
||||
if (!existsSync(CACHE_DIR)) {
|
||||
mkdirSync(CACHE_DIR, { recursive: true });
|
||||
// Cache the instructions with ETag and tag (verbatim from GitHub)
|
||||
writeFileSync(CACHE_FILE, instructions, "utf8");
|
||||
writeFileSync(
|
||||
CACHE_METADATA_FILE,
|
||||
JSON.stringify({
|
||||
etag: newETag,
|
||||
tag: latestTag,
|
||||
lastChecked: Date.now(),
|
||||
url: CODEX_INSTRUCTIONS_URL,
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
// Cache the instructions
|
||||
writeFileSync(CACHE_FILE, instructions, "utf8");
|
||||
writeFileSync(
|
||||
CACHE_METADATA_FILE,
|
||||
JSON.stringify({ timestamp: Date.now(), url: CODEX_INSTRUCTIONS_URL }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
return instructions;
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[openai-codex-plugin] Failed to fetch instructions from GitHub:",
|
||||
error.message,
|
||||
);
|
||||
|
||||
// Try to use cached version even if expired
|
||||
// Try to use cached version even if stale
|
||||
if (existsSync(CACHE_FILE)) {
|
||||
console.error("[openai-codex-plugin] Using stale cached instructions");
|
||||
console.error("[openai-codex-plugin] Using cached instructions");
|
||||
return readFileSync(CACHE_FILE, "utf8");
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ export function startLocalOAuthServer({ state }) {
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error(
|
||||
"[openai-codex-auth] Failed to bind http://127.0.0.1:1455 (",
|
||||
"[openai-codex-plugin] Failed to bind http://127.0.0.1:1455 (",
|
||||
err?.code,
|
||||
") Falling back to manual paste.",
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "opencode-openai-codex-auth",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "OpenAI ChatGPT (Codex backend) OAuth auth plugin for opencode - use your ChatGPT Plus/Pro subscription instead of API credits",
|
||||
"main": "./index.mjs",
|
||||
"type": "module",
|
||||
|
||||
Reference in New Issue
Block a user