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:
Numman Ali
2025-10-02 12:13:46 +01:00
co-authored by Hanzo Dev
parent 12348a68af
commit a6e52abac3
6 changed files with 91 additions and 44 deletions
+10 -4
View File
@@ -1,5 +1,8 @@
# OpenAI ChatGPT OAuth Plugin for opencode
[![npm version](https://img.shields.io/npm/v/opencode-openai-codex-auth.svg)](https://www.npmjs.com/package/opencode-openai-codex-auth)
[![npm downloads](https://img.shields.io/npm/dm/opencode-openai-codex-auth.svg)](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)
+10 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",