Skip to content

Commit 62856d9

Browse files
numman-aliclaude
andcommitted
feat: v4.0.0 - Model-specific prompt engineering matching Codex CLI
BREAKING CHANGE: Major prompt engineering overhaul This release brings full parity with Codex CLI's prompt selection: Model-Specific Prompts: - gpt-5.1-codex-max* → gpt-5.1-codex-max_prompt.md (117 lines, frontend design) - gpt-5.1-codex*, codex-* → gpt_5_codex_prompt.md (105 lines, focused coding) - gpt-5.1* → gpt_5_1_prompt.md (368 lines, full behavioral guidance) Legacy GPT-5.0 → GPT-5.1 Migration: - gpt-5-codex → gpt-5.1-codex - gpt-5 → gpt-5.1 - gpt-5-mini, gpt-5-nano → gpt-5.1 - codex-mini-latest → gpt-5.1-codex-mini New Features: - ModelFamily type for prompt selection ("codex-max" | "codex" | "gpt-5.1") - getModelFamily() function for model family detection - Lazy instruction loading per model family - Separate caching per model family - Model family logged in request logs Fixes: - OpenCode prompt cache URL (main → dev branch) - Multi-model session log detection in test script Test Coverage: - 191 unit tests (16 new for model family detection) - 13 integration tests with family verification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent c5afe9a commit 62856d9

File tree

15 files changed

+368
-158
lines changed

15 files changed

+368
-158
lines changed

AGENTS.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides coding guidance for AI agents (including Claude Code, Codex,
44

55
## Overview
66

7-
This is an **opencode plugin** that enables OAuth authentication with OpenAI's ChatGPT Plus/Pro Codex backend. It allows users to access `gpt-5.1-codex`, `gpt-5.1-codex-max`, `gpt-5.1-codex-mini`, `gpt-5-codex`, `gpt-5-codex-mini`, `gpt-5.1`, and `gpt-5` models through their ChatGPT subscription instead of using OpenAI Platform API credits.
7+
This is an **opencode plugin** that enables OAuth authentication with OpenAI's ChatGPT Plus/Pro Codex backend. It allows users to access `gpt-5.1-codex`, `gpt-5.1-codex-max`, `gpt-5.1-codex-mini`, and `gpt-5.1` models through their ChatGPT subscription instead of using OpenAI Platform API credits. Legacy GPT-5.0 models are automatically normalized to their GPT-5.1 equivalents.
88

99
**Key architecture principle**: 7-step fetch flow that intercepts opencode's OpenAI SDK requests, transforms them for the ChatGPT backend API, and handles OAuth token management.
1010

@@ -97,19 +97,28 @@ The main entry point orchestrates a **7-step fetch flow**:
9797
- Model-specific options override global
9898
- Plugin defaults: `reasoningEffort: "medium"`, `reasoningSummary: "auto"`, `textVerbosity: "medium"`
9999

100-
**4. Model Normalization**:
100+
**4. Model Normalization** (GPT-5.0 → GPT-5.1 migration):
101101
- All `gpt-5.1-codex-max*` variants → `gpt-5.1-codex-max`
102102
- All `gpt-5.1-codex*` variants → `gpt-5.1-codex`
103103
- All `gpt-5.1-codex-mini*` variants → `gpt-5.1-codex-mini`
104-
- All `gpt-5-codex` variants → `gpt-5-codex`
105-
- All `gpt-5-codex-mini*` or `codex-mini-latest` variants → `codex-mini-latest`
106104
- All `gpt-5.1` variants → `gpt-5.1`
107-
- All `gpt-5` variants → `gpt-5`
105+
- **Legacy mappings** (GPT-5.0 being phased out):
106+
- `gpt-5-codex*` variants → `gpt-5.1-codex`
107+
- `gpt-5-codex-mini*` or `codex-mini-latest``gpt-5.1-codex-mini`
108+
- `gpt-5*` variants (including `gpt-5-mini`, `gpt-5-nano`) → `gpt-5.1`
108109
- `minimal` effort auto-normalized to `low` for Codex families and clamped to `medium` (or `high` when requested) for Codex Mini
109110

110-
**5. Codex Instructions Caching**:
111+
**5. Model-Specific Prompt Selection**:
112+
- Different prompts for different model families (matching Codex CLI):
113+
- `gpt-5.1-codex-max*``gpt-5.1-codex-max_prompt.md` (117 lines, frontend design guidelines)
114+
- `gpt-5.1-codex*`, `codex-*``gpt_5_codex_prompt.md` (105 lines, coding focus)
115+
- `gpt-5.1*``gpt_5_1_prompt.md` (368 lines, full behavioral guidance)
116+
- `getModelFamily()` determines prompt selection based on normalized model
117+
118+
**6. Codex Instructions Caching**:
111119
- Fetches from latest release tag (not main branch)
112-
- ETag-based HTTP conditional requests
120+
- ETag-based HTTP conditional requests per model family
121+
- Separate cache files per family: `codex-max-instructions.md`, `codex-instructions.md`, `gpt-5.1-instructions.md`
113122
- Cache invalidation when release tag changes
114123
- Falls back to bundled version if GitHub unavailable
115124

@@ -140,7 +149,7 @@ OAuth implementation follows OpenAI Codex CLI patterns:
140149

141150
### Testing Strategy
142151

143-
- **123 comprehensive tests** covering all modules
152+
- **191 comprehensive tests** covering all modules
144153
- Test files mirror source structure (`test/auth.test.ts``lib/auth/auth.ts`)
145154
- Mock-heavy testing (no actual network calls or file I/O in tests)
146155
- Focus on edge cases: token expiration, model normalization, input filtering, CODEX_MODE toggling

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@
22

33
All notable changes to this project are documented here. Dates use the ISO format (YYYY-MM-DD).
44

5+
## [4.0.0] - 2025-11-25
6+
7+
**Major release**: Complete prompt engineering overhaul matching official Codex CLI behavior.
8+
9+
### Added
10+
- **Model-specific system prompts**: Plugin now fetches the correct Codex prompt based on model family, matching Codex CLI's `model_family.rs` logic:
11+
- `gpt-5.1-codex-max*``gpt-5.1-codex-max_prompt.md` (117 lines, includes frontend design guidelines)
12+
- `gpt-5.1-codex*`, `gpt-5.1-codex-mini*``gpt_5_codex_prompt.md` (105 lines, focused coding prompt)
13+
- `gpt-5.1*``gpt_5_1_prompt.md` (368 lines, full behavioral guidance)
14+
- New `ModelFamily` type (`"codex-max" | "codex" | "gpt-5.1"`) for prompt selection.
15+
- New `getModelFamily()` function to determine prompt selection based on normalized model name.
16+
- Model family now logged in request logs for debugging (`modelFamily` field in after-transform logs).
17+
- 16 new unit tests for model family detection (now **191 total unit tests**).
18+
- Integration tests now verify correct model family selection (13 integration tests with family verification).
19+
20+
### Changed
21+
- **Legacy GPT-5.0 models now map to GPT-5.1**: All legacy `gpt-5` model variants automatically normalize to their `gpt-5.1` equivalents as GPT-5.0 is being phased out by OpenAI:
22+
- `gpt-5-codex``gpt-5.1-codex`
23+
- `gpt-5``gpt-5.1`
24+
- `gpt-5-mini`, `gpt-5-nano``gpt-5.1`
25+
- `codex-mini-latest``gpt-5.1-codex-mini`
26+
- **Lazy instruction loading**: Instructions are now fetched per-request based on model family (not pre-loaded at initialization).
27+
- **Separate caching per model family**: Each model family has its own cached prompt file:
28+
- `codex-max-instructions.md` + `codex-max-instructions-meta.json`
29+
- `codex-instructions.md` + `codex-instructions-meta.json`
30+
- `gpt-5.1-instructions.md` + `gpt-5.1-instructions-meta.json`
31+
32+
### Fixed
33+
- Fixed OpenCode prompt cache URL to fetch from `dev` branch instead of non-existent `main` branch.
34+
- Fixed model configuration test script to correctly identify model logs in multi-model sessions (opencode uses a small model like `gpt-5-nano` for title generation alongside the user's selected model).
35+
36+
### Technical Details
37+
This release brings full parity with Codex CLI's prompt engineering:
38+
- **Codex family** (105 lines): Concise, tool-focused prompt for coding tasks
39+
- **Codex Max family** (117 lines): Adds frontend design guidelines for UI work
40+
- **GPT-5.1 general** (368 lines): Comprehensive behavioral guidance, personality, planning
41+
542
## [3.3.0] - 2025-11-19
643
### Added
744
- GPT 5.1 Codex Max support: normalization, per-model defaults, and new presets (`gpt-5.1-codex-max`, `gpt-5.1-codex-max-xhigh`) with extended reasoning options (including `none`/`xhigh`) while keeping the 272k context / 128k output limits.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Follow me on [X @nummanthinks](https://x.com/nummanthinks) for future updates an
4444
-**Automatic tool remapping** - Codex tools → opencode tools
4545
-**Configurable reasoning** - Control effort, summary verbosity, and text output
4646
-**Usage-aware errors** - Shows clear guidance when ChatGPT subscription limits are reached
47-
-**Type-safe & tested** - Strict TypeScript with 160+ unit tests + 14 integration tests
47+
-**Type-safe & tested** - Strict TypeScript with 191 unit tests + 13 integration tests
4848
-**Modular architecture** - Easy to maintain and extend
4949

5050
## Installation

config/full-opencode.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://opencode.ai/config.json",
33
"plugin": [
4-
"opencode-openai-codex-auth"
4+
"file:///Users/numman/Repos/opencode-codex-plugin-fresh/dist"
55
],
66
"provider": {
77
"openai": {
@@ -226,4 +226,4 @@
226226
}
227227
}
228228
}
229-
}
229+
}

index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import {
4646
PROVIDER_ID,
4747
} from "./lib/constants.js";
4848
import { logRequest } from "./lib/logger.js";
49-
import { getCodexInstructions } from "./lib/prompts/codex.js";
5049
import {
5150
createCodexHeaders,
5251
extractRequestUrl,
@@ -122,9 +121,6 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
122121
const pluginConfig = loadPluginConfig();
123122
const codexMode = getCodexMode(pluginConfig);
124123

125-
// Fetch Codex system instructions (cached with ETag for efficiency)
126-
const CODEX_INSTRUCTIONS = await getCodexInstructions();
127-
128124
// Return SDK configuration
129125
return {
130126
apiKey: DUMMY_API_KEY,
@@ -164,11 +160,11 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
164160
const originalUrl = extractRequestUrl(input);
165161
const url = rewriteUrlForCodex(originalUrl);
166162

167-
// Step 3: Transform request body with Codex instructions
163+
// Step 3: Transform request body with model-specific Codex instructions
164+
// Instructions are fetched per model family (codex-max, codex, gpt-5.1)
168165
const transformation = await transformRequestForCodex(
169166
init,
170167
url,
171-
CODEX_INSTRUCTIONS,
172168
userConfig,
173169
codexMode,
174170
);

lib/prompts/codex.ts

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,69 @@
1-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2-
import { join, dirname } from "node:path";
3-
import { fileURLToPath } from "node:url";
1+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
42
import { homedir } from "node:os";
5-
import type { GitHubRelease, CacheMetadata } from "../types.js";
3+
import { dirname, join } from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
import type { CacheMetadata, GitHubRelease } from "../types.js";
66

77
// Codex instructions constants
8-
const GITHUB_API_RELEASES = "https://api.github.com/repos/openai/codex/releases/latest";
8+
const GITHUB_API_RELEASES =
9+
"https://api.github.com/repos/openai/codex/releases/latest";
910
const CACHE_DIR = join(homedir(), ".opencode", "cache");
10-
const CACHE_FILE = join(CACHE_DIR, "codex-instructions.md");
11-
const CACHE_METADATA_FILE = join(CACHE_DIR, "codex-instructions-meta.json");
1211

1312
const __filename = fileURLToPath(import.meta.url);
1413
const __dirname = dirname(__filename);
1514

15+
/**
16+
* Model family type for prompt selection
17+
* Maps to different system prompts in the Codex CLI
18+
*/
19+
export type ModelFamily = "codex-max" | "codex" | "gpt-5.1";
20+
21+
/**
22+
* Prompt file mapping for each model family
23+
* Based on codex-rs/core/src/model_family.rs logic
24+
*/
25+
const PROMPT_FILES: Record<ModelFamily, string> = {
26+
"codex-max": "gpt-5.1-codex-max_prompt.md",
27+
codex: "gpt_5_codex_prompt.md",
28+
"gpt-5.1": "gpt_5_1_prompt.md",
29+
};
30+
31+
/**
32+
* Cache file mapping for each model family
33+
*/
34+
const CACHE_FILES: Record<ModelFamily, string> = {
35+
"codex-max": "codex-max-instructions.md",
36+
codex: "codex-instructions.md",
37+
"gpt-5.1": "gpt-5.1-instructions.md",
38+
};
39+
40+
/**
41+
* Determine the model family based on the normalized model name
42+
* @param normalizedModel - The normalized model name (e.g., "gpt-5.1-codex-max", "gpt-5.1-codex", "gpt-5.1")
43+
* @returns The model family for prompt selection
44+
*/
45+
export function getModelFamily(normalizedModel: string): ModelFamily {
46+
// Order matters - check more specific patterns first
47+
if (normalizedModel.includes("codex-max")) {
48+
return "codex-max";
49+
}
50+
if (
51+
normalizedModel.includes("codex") ||
52+
normalizedModel.startsWith("codex-")
53+
) {
54+
return "codex";
55+
}
56+
return "gpt-5.1";
57+
}
58+
1659
/**
1760
* Get the latest release tag from GitHub
1861
* @returns Release tag name (e.g., "rust-v0.43.0")
1962
*/
2063
async function getLatestReleaseTag(): Promise<string> {
2164
const response = await fetch(GITHUB_API_RELEASES);
22-
if (!response.ok) throw new Error(`Failed to fetch latest release: ${response.status}`);
65+
if (!response.ok)
66+
throw new Error(`Failed to fetch latest release: ${response.status}`);
2367
const data = (await response.json()) as GitHubRelease;
2468
return data.tag_name;
2569
}
@@ -30,31 +74,49 @@ async function getLatestReleaseTag(): Promise<string> {
3074
* Always fetches from the latest release tag, not main branch
3175
*
3276
* Rate limit protection: Only checks GitHub if cache is older than 15 minutes
33-
* @returns Codex instructions
77+
*
78+
* @param normalizedModel - The normalized model name (optional, defaults to "gpt-5.1-codex" for backwards compatibility)
79+
* @returns Codex instructions for the specified model family
3480
*/
35-
export async function getCodexInstructions(): Promise<string> {
81+
export async function getCodexInstructions(
82+
normalizedModel = "gpt-5.1-codex",
83+
): Promise<string> {
84+
const modelFamily = getModelFamily(normalizedModel);
85+
const promptFile = PROMPT_FILES[modelFamily];
86+
const cacheFile = join(CACHE_DIR, CACHE_FILES[modelFamily]);
87+
const cacheMetaFile = join(
88+
CACHE_DIR,
89+
`${CACHE_FILES[modelFamily].replace(".md", "-meta.json")}`,
90+
);
91+
3692
try {
3793
// Load cached metadata (includes ETag, tag, and lastChecked timestamp)
3894
let cachedETag: string | null = null;
3995
let cachedTag: string | null = null;
4096
let cachedTimestamp: number | null = null;
4197

42-
if (existsSync(CACHE_METADATA_FILE)) {
43-
const metadata = JSON.parse(readFileSync(CACHE_METADATA_FILE, "utf8")) as CacheMetadata;
98+
if (existsSync(cacheMetaFile)) {
99+
const metadata = JSON.parse(
100+
readFileSync(cacheMetaFile, "utf8"),
101+
) as CacheMetadata;
44102
cachedETag = metadata.etag;
45103
cachedTag = metadata.tag;
46104
cachedTimestamp = metadata.lastChecked;
47105
}
48106

49107
// Rate limit protection: If cache is less than 15 minutes old, use it
50108
const CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
51-
if (cachedTimestamp && (Date.now() - cachedTimestamp) < CACHE_TTL_MS && existsSync(CACHE_FILE)) {
52-
return readFileSync(CACHE_FILE, "utf8");
109+
if (
110+
cachedTimestamp &&
111+
Date.now() - cachedTimestamp < CACHE_TTL_MS &&
112+
existsSync(cacheFile)
113+
) {
114+
return readFileSync(cacheFile, "utf8");
53115
}
54116

55117
// Get the latest release tag (only if cache is stale or missing)
56118
const latestTag = await getLatestReleaseTag();
57-
const CODEX_INSTRUCTIONS_URL = `https://raw.githubusercontent.com/openai/codex/${latestTag}/codex-rs/core/gpt_5_codex_prompt.md`;
119+
const CODEX_INSTRUCTIONS_URL = `https://raw.githubusercontent.com/openai/codex/${latestTag}/codex-rs/core/${promptFile}`;
58120

59121
// If tag changed, we need to fetch new instructions
60122
if (cachedTag !== latestTag) {
@@ -71,8 +133,8 @@ export async function getCodexInstructions(): Promise<string> {
71133

72134
// 304 Not Modified - our cached version is still current
73135
if (response.status === 304) {
74-
if (existsSync(CACHE_FILE)) {
75-
return readFileSync(CACHE_FILE, "utf8");
136+
if (existsSync(cacheFile)) {
137+
return readFileSync(cacheFile, "utf8");
76138
}
77139
// Cache file missing but GitHub says not modified - fall through to re-fetch
78140
}
@@ -88,9 +150,9 @@ export async function getCodexInstructions(): Promise<string> {
88150
}
89151

90152
// Cache the instructions with ETag and tag (verbatim from GitHub)
91-
writeFileSync(CACHE_FILE, instructions, "utf8");
153+
writeFileSync(cacheFile, instructions, "utf8");
92154
writeFileSync(
93-
CACHE_METADATA_FILE,
155+
cacheMetaFile,
94156
JSON.stringify({
95157
etag: newETag,
96158
tag: latestTag,
@@ -107,18 +169,22 @@ export async function getCodexInstructions(): Promise<string> {
107169
} catch (error) {
108170
const err = error as Error;
109171
console.error(
110-
"[openai-codex-plugin] Failed to fetch instructions from GitHub:",
172+
`[openai-codex-plugin] Failed to fetch ${modelFamily} instructions from GitHub:`,
111173
err.message,
112174
);
113175

114176
// Try to use cached version even if stale
115-
if (existsSync(CACHE_FILE)) {
116-
console.error("[openai-codex-plugin] Using cached instructions");
117-
return readFileSync(CACHE_FILE, "utf8");
177+
if (existsSync(cacheFile)) {
178+
console.error(
179+
`[openai-codex-plugin] Using cached ${modelFamily} instructions`,
180+
);
181+
return readFileSync(cacheFile, "utf8");
118182
}
119183

120-
// Fall back to bundled version
121-
console.error("[openai-codex-plugin] Falling back to bundled instructions");
184+
// Fall back to bundled version (use codex-instructions.md as default)
185+
console.error(
186+
`[openai-codex-plugin] Falling back to bundled instructions for ${modelFamily}`,
187+
);
122188
return readFileSync(join(__dirname, "codex-instructions.md"), "utf8");
123189
}
124190
}

lib/prompts/opencode-codex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { homedir } from "node:os";
1010
import { mkdir, readFile, writeFile } from "node:fs/promises";
1111

1212
const OPENCODE_CODEX_URL =
13-
"https://raw.githubusercontent.com/sst/opencode/main/packages/opencode/src/session/prompt/codex.txt";
13+
"https://raw.githubusercontent.com/sst/opencode/dev/packages/opencode/src/session/prompt/codex.txt";
1414
const CACHE_DIR = join(homedir(), ".opencode", "cache");
1515
const CACHE_FILE = join(CACHE_DIR, "opencode-codex.txt");
1616
const CACHE_META_FILE = join(CACHE_DIR, "opencode-codex-meta.json");

0 commit comments

Comments
 (0)