Skip to content

Commit 88db136

Browse files
Numman Aliclaude
andcommitted
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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 12348a6 commit 88db136

File tree

6 files changed

+91
-44
lines changed

6 files changed

+91
-44
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# OpenAI ChatGPT OAuth Plugin for opencode
22

3+
[![npm version](https://img.shields.io/npm/v/opencode-openai-codex-auth.svg)](https://www.npmjs.com/package/opencode-openai-codex-auth)
4+
[![npm downloads](https://img.shields.io/npm/dm/opencode-openai-codex-auth.svg)](https://www.npmjs.com/package/opencode-openai-codex-auth)
5+
36
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.
47

58
## Features
69

710
- ✅ ChatGPT Plus/Pro OAuth authentication
811
-**Zero external dependencies** - Lightweight with only @openauthjs/openauth
912
-**Auto-refreshing tokens** - Handles token expiration automatically
10-
-**Auto-updating Codex instructions** - Fetches latest from OpenAI's Codex repo (cached 24h)
13+
-**Smart auto-updating Codex instructions** - Tracks latest stable release with ETag caching
1114
- ✅ Full tool support (write, edit, bash, grep, etc.)
1215
- ✅ Automatic tool remapping (Codex tools → opencode tools)
1316
- ✅ High reasoning effort with detailed thinking blocks
@@ -87,9 +90,12 @@ The plugin automatically configures:
8790
The plugin:
8891

8992
1. **Authentication**: Uses ChatGPT OAuth flow with PKCE for secure authentication
90-
2. **Token Management**: Auto-refreshes tokens using `ai-sdk-provider-chatgpt-oauth`
91-
3. **Codex Instructions**: Automatically fetches latest instructions from [openai/codex](https://github.com/openai/codex) repository
92-
- Cached locally in `~/.opencode/cache/` for 24 hours
93+
2. **Token Management**: Native token refresh implementation (no external dependencies)
94+
3. **Codex Instructions**: Automatically fetches from the latest stable release of [openai/codex](https://github.com/openai/codex)
95+
- Tracks latest release tag (not main branch) for stability
96+
- Uses ETag-based caching for efficient updates (only downloads when content changes)
97+
- Cached locally in `~/.opencode/cache/`
98+
- Auto-updates when OpenAI publishes new releases
9399
- Falls back to bundled version if GitHub is unavailable
94100
4. **Request Transformation**: Routes requests to `https://chatgpt.com/backend-api/codex/responses`
95101
5. **Model Normalization**: Maps all model names to `gpt-5-codex` (the Codex backend model)

index.mjs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function OpenAIAuthPlugin({ client }) {
4040
return {};
4141
}
4242

43-
// Fetch Codex instructions (cached for 24h)
43+
// Fetch Codex instructions (cached with ETag)
4444
const CODEX_INSTRUCTIONS = await getCodexInstructions();
4545

4646
// Return options that will be passed to the OpenAI SDK
@@ -100,21 +100,17 @@ export async function OpenAIAuthPlugin({ client }) {
100100
try {
101101
const body = JSON.parse(init.body);
102102

103-
// Normalize model name - Codex only supports specific model IDs
104-
// Map all variants to their base models
103+
// Normalize model name - Codex only supports gpt-5 and gpt-5-codex
105104
if (body.model) {
106105
if (body.model.includes("codex")) {
107106
// Any codex variant → gpt-5-codex
108107
body.model = "gpt-5-codex";
109-
} else if (
110-
body.model.includes("gpt-5") ||
111-
body.model.includes("gpt-nano")
112-
) {
113-
// gpt-5 variants → gpt-5-codex for best tool support
114-
body.model = "gpt-5-codex";
108+
} else if (body.model.includes("gpt-5")) {
109+
// gpt-5 variants → gpt-5 (keep user's choice)
110+
body.model = "gpt-5";
115111
} else {
116-
// Default fallback
117-
body.model = "gpt-5-codex";
112+
// Default fallback for unsupported models
113+
body.model = "gpt-5";
118114
}
119115
}
120116

@@ -153,6 +149,9 @@ export async function OpenAIAuthPlugin({ client }) {
153149
}
154150
body.reasoning.effort = "high";
155151
body.reasoning.summary = "detailed";
152+
if (!body.text) {
153+
body.text = {};
154+
}
156155
body.text.verbosity = "medium";
157156

158157
// Remove unsupported parameters

lib/auth.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function exchangeAuthorizationCode(
7272
});
7373
if (!res.ok) {
7474
const text = await res.text().catch(() => "");
75-
console.error("[openai-codex-auth] code->token failed:", res.status, text);
75+
console.error("[openai-codex-plugin] code->token failed:", res.status, text);
7676
return { type: "failed" };
7777
}
7878
const json = await res.json();
@@ -81,7 +81,7 @@ export async function exchangeAuthorizationCode(
8181
!json?.refresh_token ||
8282
typeof json?.expires_in !== "number"
8383
) {
84-
console.error("[openai-codex-auth] token response missing fields:", json);
84+
console.error("[openai-codex-plugin] token response missing fields:", json);
8585
return { type: "failed" };
8686
}
8787
return {

lib/codex.mjs

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,103 @@ import { fileURLToPath } from "node:url";
44
import { homedir } from "node:os";
55

66
// Codex instructions constants
7-
const CODEX_INSTRUCTIONS_URL =
8-
"https://raw.githubusercontent.com/openai/codex/main/codex-rs/core/gpt_5_codex_prompt.md";
7+
const GITHUB_API_RELEASES = "https://api.github.com/repos/openai/codex/releases/latest";
98
const CACHE_DIR = join(homedir(), ".opencode", "cache");
109
const CACHE_FILE = join(CACHE_DIR, "codex-instructions.md");
1110
const CACHE_METADATA_FILE = join(CACHE_DIR, "codex-instructions-meta.json");
12-
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
1311

1412
const __filename = fileURLToPath(import.meta.url);
1513
const __dirname = dirname(__filename);
1614

1715
/**
18-
* Fetch Codex instructions from GitHub and cache them
16+
* Get the latest release tag from GitHub
17+
* @returns {Promise<string>} Release tag name (e.g., "rust-v0.43.0")
18+
*/
19+
async function getLatestReleaseTag() {
20+
const response = await fetch(GITHUB_API_RELEASES);
21+
if (!response.ok) throw new Error(`Failed to fetch latest release: ${response.status}`);
22+
const data = await response.json();
23+
return data.tag_name;
24+
}
25+
26+
/**
27+
* Fetch Codex instructions from GitHub with ETag-based caching
28+
* Uses HTTP conditional requests to efficiently check for updates
29+
* Always fetches from the latest release tag, not main branch
1930
* @returns {Promise<string>} Codex instructions
2031
*/
2132
export async function getCodexInstructions() {
2233
try {
23-
// Check if cache exists and is fresh
34+
// Get the latest release tag
35+
const latestTag = await getLatestReleaseTag();
36+
const CODEX_INSTRUCTIONS_URL = `https://raw.githubusercontent.com/openai/codex/${latestTag}/codex-rs/core/gpt_5_codex_prompt.md`;
37+
38+
// Load cached metadata (includes ETag and tag)
39+
let cachedETag = null;
40+
let cachedTag = null;
2441
if (existsSync(CACHE_METADATA_FILE)) {
2542
const metadata = JSON.parse(readFileSync(CACHE_METADATA_FILE, "utf8"));
26-
const age = Date.now() - metadata.timestamp;
43+
cachedETag = metadata.etag;
44+
cachedTag = metadata.tag;
45+
}
46+
47+
// If tag changed, we need to fetch new instructions
48+
if (cachedTag !== latestTag) {
49+
cachedETag = null; // Force re-fetch
50+
}
51+
52+
// Make conditional request with If-None-Match header
53+
const headers = {};
54+
if (cachedETag) {
55+
headers["If-None-Match"] = cachedETag;
56+
}
57+
58+
const response = await fetch(CODEX_INSTRUCTIONS_URL, { headers });
2759

28-
if (age < CACHE_TTL && existsSync(CACHE_FILE)) {
60+
// 304 Not Modified - our cached version is still current
61+
if (response.status === 304) {
62+
if (existsSync(CACHE_FILE)) {
2963
return readFileSync(CACHE_FILE, "utf8");
3064
}
65+
// Cache file missing but GitHub says not modified - fall through to re-fetch
3166
}
3267

33-
// Fetch fresh instructions from GitHub
34-
const response = await fetch(CODEX_INSTRUCTIONS_URL);
35-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
68+
// 200 OK - new content or first fetch
69+
if (response.ok) {
70+
const instructions = await response.text();
71+
const newETag = response.headers.get("etag");
3672

37-
const instructions = await response.text();
73+
// Create cache directory if it doesn't exist
74+
if (!existsSync(CACHE_DIR)) {
75+
mkdirSync(CACHE_DIR, { recursive: true });
76+
}
3877

39-
// Create cache directory if it doesn't exist
40-
if (!existsSync(CACHE_DIR)) {
41-
mkdirSync(CACHE_DIR, { recursive: true });
78+
// Cache the instructions with ETag and tag (verbatim from GitHub)
79+
writeFileSync(CACHE_FILE, instructions, "utf8");
80+
writeFileSync(
81+
CACHE_METADATA_FILE,
82+
JSON.stringify({
83+
etag: newETag,
84+
tag: latestTag,
85+
lastChecked: Date.now(),
86+
url: CODEX_INSTRUCTIONS_URL,
87+
}),
88+
"utf8",
89+
);
90+
91+
return instructions;
4292
}
4393

44-
// Cache the instructions
45-
writeFileSync(CACHE_FILE, instructions, "utf8");
46-
writeFileSync(
47-
CACHE_METADATA_FILE,
48-
JSON.stringify({ timestamp: Date.now(), url: CODEX_INSTRUCTIONS_URL }),
49-
"utf8",
50-
);
51-
52-
return instructions;
94+
throw new Error(`HTTP ${response.status}`);
5395
} catch (error) {
5496
console.error(
5597
"[openai-codex-plugin] Failed to fetch instructions from GitHub:",
5698
error.message,
5799
);
58100

59-
// Try to use cached version even if expired
101+
// Try to use cached version even if stale
60102
if (existsSync(CACHE_FILE)) {
61-
console.error("[openai-codex-plugin] Using stale cached instructions");
103+
console.error("[openai-codex-plugin] Using cached instructions");
62104
return readFileSync(CACHE_FILE, "utf8");
63105
}
64106

lib/server.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function startLocalOAuthServer({ state }) {
5454
})
5555
.on("error", (err) => {
5656
console.error(
57-
"[openai-codex-auth] Failed to bind http://127.0.0.1:1455 (",
57+
"[openai-codex-plugin] Failed to bind http://127.0.0.1:1455 (",
5858
err?.code,
5959
") Falling back to manual paste.",
6060
);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-openai-codex-auth",
3-
"version": "1.0.1",
3+
"version": "1.0.2",
44
"description": "OpenAI ChatGPT (Codex backend) OAuth auth plugin for opencode - use your ChatGPT Plus/Pro subscription instead of API credits",
55
"main": "./index.mjs",
66
"type": "module",

0 commit comments

Comments
 (0)