mirror of
https://github.com/hanzoai/opencode-openai-codex-auth.git
synced 2026-09-24 17:01:02 +00:00
feat: Enhance Codex integration with prompt caching and error handling improvements
This commit is contained in:
@@ -7,3 +7,4 @@ dist/
|
||||
opencode.json
|
||||
.opencode/
|
||||
tmp
|
||||
.codex-cache
|
||||
@@ -0,0 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Added stable prompt caching via host-provided `prompt_cache_key` and Codex conversation headers.
|
||||
- Improved usage-limit error messaging to mirror Codex CLI short-term and weekly windows.
|
||||
- Synced CODEX_MODE bridge prompt content with the latest Codex CLI release.
|
||||
- Updated documentation and README to reflect the canonical `config/full-opencode.json` and new behaviour.
|
||||
@@ -36,11 +36,13 @@ Follow me on [X @nummanthinks](https://x.com/nummanthinks) for future updates an
|
||||
- ✅ **9 pre-configured model variants** - Low/Medium/High reasoning for both gpt-5 and gpt-5-codex
|
||||
- ✅ **Zero external dependencies** - Lightweight with only @openauthjs/openauth
|
||||
- ✅ **Auto-refreshing tokens** - Handles token expiration automatically
|
||||
- ✅ **Prompt caching** - Reuses responses across turns via stable `prompt_cache_key`
|
||||
- ✅ **Smart auto-updating Codex instructions** - Tracks latest stable release with ETag caching
|
||||
- ✅ **Full tool support** - write, edit, bash, grep, glob, and more
|
||||
- ✅ **CODEX_MODE** - Codex-OpenCode bridge prompt with Task tool & MCP awareness (enabled by default)
|
||||
- ✅ **Automatic tool remapping** - Codex tools → opencode tools
|
||||
- ✅ **Configurable reasoning** - Control effort, summary verbosity, and text output
|
||||
- ✅ **Usage-aware errors** - Shows clear guidance when ChatGPT subscription limits are reached
|
||||
- ✅ **Type-safe & tested** - Strict TypeScript with 159 unit tests + 14 integration tests
|
||||
- ✅ **Modular architecture** - Easy to maintain and extend
|
||||
|
||||
@@ -75,6 +77,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
"models": {
|
||||
"gpt-5-codex-low": {
|
||||
"name": "GPT 5 Codex Low (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -87,6 +93,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
},
|
||||
"gpt-5-codex-medium": {
|
||||
"name": "GPT 5 Codex Medium (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "medium",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -99,6 +109,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
},
|
||||
"gpt-5-codex-high": {
|
||||
"name": "GPT 5 Codex High (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "detailed",
|
||||
@@ -111,6 +125,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
},
|
||||
"gpt-5-minimal": {
|
||||
"name": "GPT 5 Minimal (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "minimal",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -123,6 +141,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
},
|
||||
"gpt-5-low": {
|
||||
"name": "GPT 5 Low (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -135,6 +157,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
},
|
||||
"gpt-5-medium": {
|
||||
"name": "GPT 5 Medium (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "medium",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -147,6 +173,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
},
|
||||
"gpt-5-high": {
|
||||
"name": "GPT 5 High (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "detailed",
|
||||
@@ -159,6 +189,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
},
|
||||
"gpt-5-mini": {
|
||||
"name": "GPT 5 Mini (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -171,6 +205,10 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
},
|
||||
"gpt-5-nano": {
|
||||
"name": "GPT 5 Nano (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "minimal",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -197,6 +235,12 @@ For the complete experience with all reasoning variants matching the official Co
|
||||
|
||||
All appear in the opencode model selector as "GPT 5 Codex Low (OAuth)", "GPT 5 High (OAuth)", etc.
|
||||
|
||||
### Prompt caching & usage limits
|
||||
|
||||
Codex backend caching is enabled automatically. When OpenCode supplies a `prompt_cache_key` (its session identifier), the plugin forwards it unchanged so Codex can reuse work between turns. The plugin no longer synthesizes its own cache IDs—if the host omits `prompt_cache_key`, Codex will treat the turn as uncached. The bundled CODEX_MODE bridge prompt is synchronized with the latest Codex CLI release, so opencode and Codex stay in lock-step on tool availability. When your ChatGPT subscription nears a limit, opencode surfaces the plugin's friendly error message with the 5-hour and weekly windows, mirroring the Codex CLI summary.
|
||||
|
||||
> **Auto-compaction note:** OpenCode's context auto-compaction and usage sidebar only populate when the full configuration above is used (the minimal config lacks the per-model metadata OpenCode needs). Stick with `config/full-opencode.json` if you want live token counts and automatic history compaction inside the UI.
|
||||
|
||||
#### Alternative: Minimal Configuration
|
||||
|
||||
For a simpler setup (uses plugin defaults: medium reasoning, auto summaries):
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
"models": {
|
||||
"gpt-5-codex-low": {
|
||||
"name": "GPT 5 Codex Low (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -29,6 +33,10 @@
|
||||
},
|
||||
"gpt-5-codex-medium": {
|
||||
"name": "GPT 5 Codex Medium (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "medium",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -41,6 +49,10 @@
|
||||
},
|
||||
"gpt-5-codex-high": {
|
||||
"name": "GPT 5 Codex High (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "detailed",
|
||||
@@ -53,6 +65,10 @@
|
||||
},
|
||||
"gpt-5-minimal": {
|
||||
"name": "GPT 5 Minimal (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "minimal",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -65,6 +81,10 @@
|
||||
},
|
||||
"gpt-5-low": {
|
||||
"name": "GPT 5 Low (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -77,6 +97,10 @@
|
||||
},
|
||||
"gpt-5-medium": {
|
||||
"name": "GPT 5 Medium (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "medium",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -89,6 +113,10 @@
|
||||
},
|
||||
"gpt-5-high": {
|
||||
"name": "GPT 5 High (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "detailed",
|
||||
@@ -101,6 +129,10 @@
|
||||
},
|
||||
"gpt-5-mini": {
|
||||
"name": "GPT 5 Mini (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -113,6 +145,10 @@
|
||||
},
|
||||
"gpt-5-nano": {
|
||||
"name": "GPT 5 Nano (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "minimal",
|
||||
"reasoningSummary": "auto",
|
||||
|
||||
@@ -20,8 +20,14 @@ Complete reference for configuring the OpenCode OpenAI Codex Auth Plugin.
|
||||
"models": {
|
||||
"gpt-5-codex-low": {
|
||||
"name": "GPT 5 Codex Low (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
"textVerbosity": "medium",
|
||||
"include": ["reasoning.encrypted_content"],
|
||||
"store": false
|
||||
}
|
||||
@@ -342,6 +348,7 @@ Advanced plugin settings in `~/.opencode/openai-codex-auth-config.json`:
|
||||
**What it does:**
|
||||
- `true` (default): Uses Codex-OpenCode bridge prompt (Task tool & MCP aware)
|
||||
- `false`: Uses legacy tool remap message
|
||||
- Bridge prompt content is synced with the latest Codex CLI release (ETag-cached)
|
||||
|
||||
**When to disable:**
|
||||
- Compatibility issues with OpenCode updates
|
||||
@@ -354,6 +361,19 @@ CODEX_MODE=0 opencode run "task" # Temporarily disable
|
||||
CODEX_MODE=1 opencode run "task" # Temporarily enable
|
||||
```
|
||||
|
||||
### Prompt caching
|
||||
|
||||
- When OpenCode provides a `prompt_cache_key` (its session identifier), the plugin forwards it directly to Codex.
|
||||
- The same value is sent via headers (`conversation_id`, `session_id`) and request body, reducing latency and token usage.
|
||||
- The plugin does not synthesize a fallback key; hosts that omit `prompt_cache_key` will see uncached behaviour until they provide one.
|
||||
- No configuration needed—cache headers are injected during request transformation.
|
||||
|
||||
### Usage limit messaging
|
||||
|
||||
- When the ChatGPT subscription hits a limit, the plugin returns a Codex CLI-style summary (5-hour + weekly windows).
|
||||
- Messages bubble up in OpenCode exactly where SDK errors normally surface.
|
||||
- Helpful when working inside the OpenCode UI or CLI—users immediately see reset timing.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Files
|
||||
@@ -362,6 +382,8 @@ CODEX_MODE=1 opencode run "task" # Temporarily enable
|
||||
- [config/full-opencode.json](../config/full-opencode.json) - Complete with 9 variants
|
||||
- [config/minimal-opencode.json](../config/minimal-opencode.json) - Minimal setup
|
||||
|
||||
> **Why choose the full config?** OpenCode's auto-compaction and usage widgets rely on the per-model `limit` metadata present only in `full-opencode.json`. Use the minimal config only if you don't need those UI features.
|
||||
|
||||
**Your Configs:**
|
||||
- `~/.config/opencode/opencode.json` - Global config
|
||||
- `<project>/.opencode.json` - Project-specific config
|
||||
|
||||
@@ -284,14 +284,20 @@ let include: Vec<String> = if reasoning.is_some() {
|
||||
├─ Set reasoningSummary (auto/detailed)
|
||||
└─ Based on model variant
|
||||
|
||||
7. Final Body
|
||||
7. Prompt Caching & Session Headers
|
||||
├─ Preserve host-supplied prompt_cache_key (OpenCode session id)
|
||||
├─ Add conversation + account headers for Codex debugging when cache key exists
|
||||
└─ Leave headers unset if host does not provide a cache key
|
||||
|
||||
8. Final Body
|
||||
├─ store: false
|
||||
├─ stream: true
|
||||
├─ instructions: Codex system prompt
|
||||
├─ input: Filtered messages (no IDs)
|
||||
├─ reasoning: { effort, summary }
|
||||
├─ text: { verbosity }
|
||||
└─ include: ["reasoning.encrypted_content"]
|
||||
├─ include: ["reasoning.encrypted_content"]
|
||||
└─ prompt_cache_key: conversation-scoped UUID
|
||||
```
|
||||
|
||||
**Source**: `lib/request/request-transformer.ts:265-329`
|
||||
@@ -318,6 +324,7 @@ let include: Vec<String> = if reasoning.is_some() {
|
||||
|---------|-----------|-------------|------|
|
||||
| **Codex-OpenCode Bridge** | N/A (native) | ✅ Custom prompt | OpenCode → Codex translation |
|
||||
| **OpenCode Prompt Filtering** | N/A | ✅ Filter & replace | Remove OpenCode-specific prompts |
|
||||
| **Usage-limit messaging** | CLI prints status | ✅ Friendly error summary | Surface 5h/weekly windows in OpenCode |
|
||||
| **Per-Model Options** | CLI flags | ✅ Config file | Better UX in OpenCode |
|
||||
| **Custom Model Names** | No | ✅ Display names | UI convenience |
|
||||
|
||||
|
||||
@@ -92,10 +92,20 @@ Plugins can inject options via the `loader()` function.
|
||||
"provider": {
|
||||
"openai": {
|
||||
"models": {
|
||||
"GPT 5 Codex Medium (ChatGPT Subscription)": {
|
||||
"id": "gpt-5-codex",
|
||||
"gpt-5-codex-medium": {
|
||||
"name": "GPT 5 Codex Medium (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "medium"
|
||||
"reasoningEffort": "medium",
|
||||
"reasoningSummary": "auto",
|
||||
"textVerbosity": "medium",
|
||||
"include": [
|
||||
"reasoning.encrypted_content"
|
||||
],
|
||||
"store": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,9 +115,9 @@ Plugins can inject options via the `loader()` function.
|
||||
```
|
||||
|
||||
**What OpenCode Uses**:
|
||||
- **UI Display**: "GPT 5 Codex Medium (ChatGPT Subscription)" ✅
|
||||
- **Persistence**: `provider_id: "openai"` + `model_id: "gpt-5-codex"` ✅
|
||||
- **API Calls**: `"gpt-5-codex"` ✅
|
||||
- **UI Display**: "GPT 5 Codex Medium (OAuth)" ✅
|
||||
- **Persistence**: `provider_id: "openai"` + `model_id: "gpt-5-codex-medium"` ✅
|
||||
- **Plugin lookup**: `models["gpt-5-codex-medium"]` → used to build Codex request ✅
|
||||
|
||||
### TUI Persistence
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
"models": {
|
||||
"gpt-5-codex-low": {
|
||||
"name": "GPT 5 Codex Low (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -58,6 +62,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
},
|
||||
"gpt-5-codex-medium": {
|
||||
"name": "GPT 5 Codex Medium (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "medium",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -68,6 +76,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
},
|
||||
"gpt-5-codex-high": {
|
||||
"name": "GPT 5 Codex High (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "detailed",
|
||||
@@ -78,6 +90,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
},
|
||||
"gpt-5-minimal": {
|
||||
"name": "GPT 5 Minimal (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "minimal",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -88,6 +104,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
},
|
||||
"gpt-5-low": {
|
||||
"name": "GPT 5 Low (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -98,6 +118,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
},
|
||||
"gpt-5-medium": {
|
||||
"name": "GPT 5 Medium (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "medium",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -108,6 +132,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
},
|
||||
"gpt-5-high": {
|
||||
"name": "GPT 5 High (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "detailed",
|
||||
@@ -118,6 +146,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
},
|
||||
"gpt-5-mini": {
|
||||
"name": "GPT 5 Mini (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -128,6 +160,10 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
},
|
||||
"gpt-5-nano": {
|
||||
"name": "GPT 5 Nano (OAuth)",
|
||||
"limit": {
|
||||
"context": 272000,
|
||||
"output": 128000
|
||||
},
|
||||
"options": {
|
||||
"reasoningEffort": "minimal",
|
||||
"reasoningSummary": "auto",
|
||||
@@ -146,9 +182,14 @@ Add this to `~/.config/opencode/opencode.json`:
|
||||
- ✅ GPT-5 Codex (Low/Medium/High reasoning)
|
||||
- ✅ GPT-5 (Minimal/Low/Medium/High reasoning)
|
||||
- ✅ gpt-5-mini, gpt-5-nano (lightweight variants)
|
||||
- ✅ 272k context + 128k output window for every preset
|
||||
- ✅ All visible in OpenCode model selector
|
||||
- ✅ Optimal settings for each reasoning level
|
||||
|
||||
Prompt caching is enabled out of the box: when OpenCode sends its session identifier as `prompt_cache_key`, the plugin forwards it untouched so multi-turn runs reuse prior work. The plugin no longer synthesizes cache IDs; if the host omits that field, Codex treats the run as uncached. The CODEX_MODE bridge prompt bundled with the plugin is kept in sync with the latest Codex CLI release, so the OpenCode UI and Codex share the same tool contract. If you hit your ChatGPT subscription limits, the plugin returns a friendly Codex-style message with the 5-hour and weekly usage windows so you know when capacity resets.
|
||||
|
||||
> **Heads up:** OpenCode's context auto-compaction and usage sidebar only work when this full configuration is installed. The minimal configuration skips the per-model limits, so OpenCode cannot display token usage or compact history automatically.
|
||||
|
||||
#### Option B: Minimal Configuration
|
||||
|
||||
Just want to get started quickly?
|
||||
|
||||
@@ -24,33 +24,40 @@
|
||||
|
||||
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
|
||||
import type { Auth } from "@opencode-ai/sdk";
|
||||
import { createAuthorizationFlow, exchangeAuthorizationCode, decodeJWT, REDIRECT_URI } from "./lib/auth/auth.js";
|
||||
import { getCodexInstructions } from "./lib/prompts/codex.js";
|
||||
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
||||
import { logRequest } from "./lib/logger.js";
|
||||
import { openBrowserUrl } from "./lib/auth/browser.js";
|
||||
import {
|
||||
shouldRefreshToken,
|
||||
refreshAndUpdateToken,
|
||||
extractRequestUrl,
|
||||
rewriteUrlForCodex,
|
||||
transformRequestForCodex,
|
||||
createAuthorizationFlow,
|
||||
decodeJWT,
|
||||
exchangeAuthorizationCode,
|
||||
REDIRECT_URI,
|
||||
} from "./lib/auth/auth.js";
|
||||
import { openBrowserUrl } from "./lib/auth/browser.js";
|
||||
import { startLocalOAuthServer } from "./lib/auth/server.js";
|
||||
import { getCodexMode, loadPluginConfig } from "./lib/config.js";
|
||||
import {
|
||||
AUTH_LABELS,
|
||||
CODEX_BASE_URL,
|
||||
DUMMY_API_KEY,
|
||||
ERROR_MESSAGES,
|
||||
JWT_CLAIM_PATH,
|
||||
LOG_STAGES,
|
||||
OPENAI_HEADER_VALUES,
|
||||
OPENAI_HEADERS,
|
||||
PLUGIN_NAME,
|
||||
PROVIDER_ID,
|
||||
} from "./lib/constants.js";
|
||||
import { logRequest } from "./lib/logger.js";
|
||||
import { getCodexInstructions } from "./lib/prompts/codex.js";
|
||||
import {
|
||||
createCodexHeaders,
|
||||
extractRequestUrl,
|
||||
handleErrorResponse,
|
||||
handleSuccessResponse,
|
||||
refreshAndUpdateToken,
|
||||
rewriteUrlForCodex,
|
||||
shouldRefreshToken,
|
||||
transformRequestForCodex,
|
||||
} from "./lib/request/fetch-helpers.js";
|
||||
import { loadPluginConfig, getCodexMode } from "./lib/config.js";
|
||||
import type { UserConfig } from "./lib/types.js";
|
||||
import {
|
||||
DUMMY_API_KEY,
|
||||
CODEX_BASE_URL,
|
||||
PROVIDER_ID,
|
||||
JWT_CLAIM_PATH,
|
||||
PLUGIN_NAME,
|
||||
ERROR_MESSAGES,
|
||||
LOG_STAGES,
|
||||
AUTH_LABELS,
|
||||
} from "./lib/constants.js";
|
||||
|
||||
/**
|
||||
* OpenAI Codex OAuth authentication plugin for opencode
|
||||
@@ -97,13 +104,14 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
|
||||
const decoded = decodeJWT(auth.access);
|
||||
const accountId = decoded?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
|
||||
|
||||
if (!accountId) {
|
||||
console.error(`[${PLUGIN_NAME}] ${ERROR_MESSAGES.NO_ACCOUNT_ID}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!accountId) {
|
||||
console.error(`[${PLUGIN_NAME}] ${ERROR_MESSAGES.NO_ACCOUNT_ID}`);
|
||||
return {};
|
||||
}
|
||||
// Extract user configuration (global + per-model options)
|
||||
const providerConfig = provider as { options?: Record<string, unknown>; models?: UserConfig["models"] } | undefined;
|
||||
const providerConfig = provider as
|
||||
| { options?: Record<string, unknown>; models?: UserConfig["models"] }
|
||||
| undefined;
|
||||
const userConfig: UserConfig = {
|
||||
global: providerConfig?.options || {},
|
||||
models: providerConfig?.models || {},
|
||||
@@ -136,11 +144,17 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
|
||||
* @param init - Request options
|
||||
* @returns Response from Codex API
|
||||
*/
|
||||
async fetch(input: Request | string | URL, init?: RequestInit): Promise<Response> {
|
||||
async fetch(
|
||||
input: Request | string | URL,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> {
|
||||
// Step 1: Check and refresh token if needed
|
||||
const currentAuth = await getAuth();
|
||||
if (shouldRefreshToken(currentAuth)) {
|
||||
const refreshResult = await refreshAndUpdateToken(currentAuth, client);
|
||||
const refreshResult = await refreshAndUpdateToken(
|
||||
currentAuth,
|
||||
client,
|
||||
);
|
||||
if (!refreshResult.success) {
|
||||
return refreshResult.response;
|
||||
}
|
||||
@@ -151,13 +165,28 @@ export const OpenAIAuthPlugin: Plugin = async ({ client }: PluginInput) => {
|
||||
const url = rewriteUrlForCodex(originalUrl);
|
||||
|
||||
// Step 3: Transform request body with Codex instructions
|
||||
const transformation = await transformRequestForCodex(init, url, CODEX_INSTRUCTIONS, userConfig, codexMode);
|
||||
const transformation = await transformRequestForCodex(
|
||||
init,
|
||||
url,
|
||||
CODEX_INSTRUCTIONS,
|
||||
userConfig,
|
||||
codexMode,
|
||||
);
|
||||
const hasTools = transformation?.body.tools !== undefined;
|
||||
const requestInit = transformation?.updatedInit ?? init;
|
||||
|
||||
// Step 4: Create headers with OAuth and ChatGPT account info
|
||||
const accessToken = currentAuth.type === "oauth" ? currentAuth.access : "";
|
||||
const headers = createCodexHeaders(requestInit, accountId, accessToken);
|
||||
const accessToken =
|
||||
currentAuth.type === "oauth" ? currentAuth.access : "";
|
||||
const headers = createCodexHeaders(
|
||||
requestInit,
|
||||
accountId,
|
||||
accessToken,
|
||||
{
|
||||
model: transformation?.body.model,
|
||||
promptCacheKey: (transformation?.body as any)?.prompt_cache_key,
|
||||
},
|
||||
);
|
||||
|
||||
// Step 5: Make request to Codex API
|
||||
const response = await fetch(url, {
|
||||
|
||||
@@ -27,6 +27,7 @@ export const OPENAI_HEADERS = {
|
||||
ACCOUNT_ID: "chatgpt-account-id",
|
||||
ORIGINATOR: "originator",
|
||||
SESSION_ID: "session_id",
|
||||
CONVERSATION_ID: "conversation_id",
|
||||
} as const;
|
||||
|
||||
/** OpenAI-specific header values */
|
||||
|
||||
@@ -32,19 +32,32 @@ You are running Codex through OpenCode, an open-source terminal coding assistant
|
||||
|
||||
**File Operations:**
|
||||
- \`write\` - Create new files
|
||||
- Overwriting existing files requires a prior Read in this session; default to ASCII unless the file already uses Unicode.
|
||||
- \`edit\` - Modify existing files (REPLACES apply_patch)
|
||||
- Requires a prior Read in this session; preserve exact indentation; ensure \`oldString\` uniquely matches or use \`replaceAll\`; edit fails if ambiguous or missing.
|
||||
- \`read\` - Read file contents
|
||||
|
||||
**Search/Discovery:**
|
||||
- \`grep\` - Search file contents
|
||||
- \`glob\` - Find files by pattern
|
||||
- \`grep\` - Search file contents (tool, not bash grep); use \`include\` to filter patterns; set \`path\` only when not searching workspace root; for cross-file match counts use bash with \`rg\`.
|
||||
- \`glob\` - Find files by pattern; defaults to workspace cwd unless \`path\` is set.
|
||||
- \`list\` - List directories (requires absolute paths)
|
||||
|
||||
**Execution:**
|
||||
- \`bash\` - Run shell commands
|
||||
- No workdir parameter; do not include it in tool calls.
|
||||
- Always include a short description for the command.
|
||||
- Do not use cd; use absolute paths in commands.
|
||||
- Quote paths containing spaces with double quotes.
|
||||
- Chain multiple commands with ';' or '&&'; avoid newlines.
|
||||
- Use Grep/Glob tools for searches; only use bash with \`rg\` when you need counts or advanced features.
|
||||
- Do not use \`ls\`/\`cat\` in bash; use \`list\`/\`read\` tools instead.
|
||||
- For deletions (rm), verify by listing parent dir with \`list\`.
|
||||
|
||||
**Network:**
|
||||
- \`webfetch\` - Fetch web content
|
||||
- Use fully-formed URLs (http/https; http auto-upgrades to https).
|
||||
- Always set \`format\` to one of: text | markdown | html; prefer markdown unless otherwise required.
|
||||
- Read-only; short cache window.
|
||||
|
||||
**Task Management:**
|
||||
- \`todowrite\` - Manage tasks/plans (REPLACES update_plan)
|
||||
@@ -57,7 +70,11 @@ apply_patch → edit
|
||||
update_plan → todowrite
|
||||
read_plan → todoread
|
||||
|
||||
**Path Usage:** Follow each tool's schema—use absolute paths where required (e.g., \`read\`, \`edit\`, \`write\`, \`list\`) and relative paths only when the tool allows them.
|
||||
**Path Usage:** Use per-tool conventions to avoid conflicts:
|
||||
- Tool calls: \`read\`, \`edit\`, \`write\`, \`list\` require absolute paths.
|
||||
- Searches: \`grep\`/\`glob\` default to the workspace cwd; prefer relative include patterns; set \`path\` only when a different root is needed.
|
||||
- Presentation: In assistant messages, show workspace-relative paths; use absolute paths only inside tool calls.
|
||||
- Tool schema overrides general path preferences—do not convert required absolute paths to relative.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
@@ -89,11 +106,15 @@ If ANY answer is NO → STOP and correct before proceeding.
|
||||
## Advanced Tools
|
||||
|
||||
**Task Tool (Sub-Agents):**
|
||||
- Specialized agents are available via the Task tool
|
||||
- Use the Task tool (functions.task) to launch sub-agents
|
||||
- Check the Task tool description for current agent types and their capabilities
|
||||
- Useful for complex analysis, specialized workflows, or tasks requiring isolated context
|
||||
- The agent list is dynamically generated - refer to tool schema for available agents
|
||||
|
||||
**Parallelization:**
|
||||
- When multiple independent tool calls are needed, use multi_tool_use.parallel to run them concurrently.
|
||||
- Reserve sequential calls for ordered or data-dependent steps.
|
||||
|
||||
**MCP Tools:**
|
||||
- Model Context Protocol servers provide additional capabilities
|
||||
- MCP tools are prefixed: \`mcp__<server-name>__<tool-name>\`
|
||||
@@ -102,7 +123,13 @@ If ANY answer is NO → STOP and correct before proceeding.
|
||||
|
||||
## What Remains from Codex
|
||||
|
||||
Sandbox policies, approval mechanisms, final answer formatting, git commit protocols, and file reference formats all follow Codex instructions. In approval policy "never", never request escalations.`;
|
||||
Sandbox policies, approval mechanisms, final answer formatting, git commit protocols, and file reference formats all follow Codex instructions. In approval policy "never", never request escalations.
|
||||
|
||||
## Approvals & Safety
|
||||
- Assume workspace-write filesystem, network enabled, approval on-failure unless explicitly stated otherwise.
|
||||
- When a command fails due to sandboxing or permissions, retry with escalated permissions if allowed by policy, including a one-line justification.
|
||||
- Treat destructive commands (e.g., \`rm\`, \`git reset --hard\`) as requiring explicit user request or approval.
|
||||
- When uncertain, prefer non-destructive verification first (e.g., confirm file existence with \`list\`, then delete with \`bash\`).`;
|
||||
|
||||
export interface CodexOpenCodeBridgeMeta {
|
||||
estimatedTokens: number;
|
||||
|
||||
@@ -170,9 +170,10 @@ export async function transformRequestForCodex(
|
||||
* @returns Headers object with all required Codex headers
|
||||
*/
|
||||
export function createCodexHeaders(
|
||||
init: RequestInit | undefined,
|
||||
accountId: string,
|
||||
accessToken: string,
|
||||
init: RequestInit | undefined,
|
||||
accountId: string,
|
||||
accessToken: string,
|
||||
opts?: { model?: string; promptCacheKey?: string },
|
||||
): Headers {
|
||||
const headers = new Headers(init?.headers ?? {});
|
||||
headers.delete("x-api-key"); // Remove any existing API key
|
||||
@@ -180,8 +181,17 @@ export function createCodexHeaders(
|
||||
headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
||||
headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
||||
headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
||||
headers.set(OPENAI_HEADERS.SESSION_ID, crypto.randomUUID());
|
||||
return headers;
|
||||
|
||||
const cacheKey = opts?.promptCacheKey;
|
||||
if (cacheKey) {
|
||||
headers.set(OPENAI_HEADERS.CONVERSATION_ID, cacheKey);
|
||||
headers.set(OPENAI_HEADERS.SESSION_ID, cacheKey);
|
||||
} else {
|
||||
headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
|
||||
headers.delete(OPENAI_HEADERS.SESSION_ID);
|
||||
}
|
||||
headers.set("accept", "text/event-stream");
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,20 +200,70 @@ export function createCodexHeaders(
|
||||
* @returns Response with error details
|
||||
*/
|
||||
export async function handleErrorResponse(
|
||||
response: Response,
|
||||
response: Response,
|
||||
): Promise<Response> {
|
||||
const text = await response.text();
|
||||
console.error(`[${PLUGIN_NAME}] ${response.status} error:`, text);
|
||||
const raw = await response.text();
|
||||
|
||||
let enriched = raw;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as any;
|
||||
const err = parsed?.error ?? {};
|
||||
|
||||
// Parse Codex rate-limit headers if present
|
||||
const h = response.headers;
|
||||
const primary = {
|
||||
used_percent: toNumber(h.get("x-codex-primary-used-percent")),
|
||||
window_minutes: toInt(h.get("x-codex-primary-window-minutes")),
|
||||
resets_at: toInt(h.get("x-codex-primary-reset-at")),
|
||||
};
|
||||
const secondary = {
|
||||
used_percent: toNumber(h.get("x-codex-secondary-used-percent")),
|
||||
window_minutes: toInt(h.get("x-codex-secondary-window-minutes")),
|
||||
resets_at: toInt(h.get("x-codex-secondary-reset-at")),
|
||||
};
|
||||
const rate_limits =
|
||||
primary.used_percent !== undefined || secondary.used_percent !== undefined
|
||||
? { primary, secondary }
|
||||
: undefined;
|
||||
|
||||
// Friendly message for subscription/rate usage limits
|
||||
const code = (err.code ?? err.type ?? "").toString();
|
||||
const resetsAt = err.resets_at ?? primary.resets_at ?? secondary.resets_at;
|
||||
const mins = resetsAt ? Math.max(0, Math.round((resetsAt * 1000 - Date.now()) / 60000)) : undefined;
|
||||
let friendly_message: string | undefined;
|
||||
if (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {
|
||||
const plan = err.plan_type ? ` (${String(err.plan_type).toLowerCase()} plan)` : "";
|
||||
const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
|
||||
friendly_message = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();
|
||||
}
|
||||
|
||||
const enhanced = {
|
||||
error: {
|
||||
...err,
|
||||
message: err.message ?? friendly_message ?? "Usage limit reached.",
|
||||
friendly_message,
|
||||
rate_limits,
|
||||
status: response.status,
|
||||
},
|
||||
};
|
||||
enriched = JSON.stringify(enhanced);
|
||||
} catch {
|
||||
// Raw body not JSON; leave unchanged
|
||||
enriched = raw;
|
||||
}
|
||||
|
||||
console.error(`[${PLUGIN_NAME}] ${response.status} error:`, enriched);
|
||||
logRequest(LOG_STAGES.ERROR_RESPONSE, {
|
||||
status: response.status,
|
||||
error: text,
|
||||
error: enriched,
|
||||
});
|
||||
|
||||
return new Response(text, {
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set("content-type", "application/json; charset=utf-8");
|
||||
return new Response(enriched, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,10 +275,10 @@ export async function handleErrorResponse(
|
||||
* @returns Processed response (SSE→JSON for non-tool, stream for tool requests)
|
||||
*/
|
||||
export async function handleSuccessResponse(
|
||||
response: Response,
|
||||
hasTools: boolean,
|
||||
response: Response,
|
||||
hasTools: boolean,
|
||||
): Promise<Response> {
|
||||
const responseHeaders = ensureContentType(response.headers);
|
||||
const responseHeaders = ensureContentType(response.headers);
|
||||
|
||||
// For non-tool requests (compact/summarize), convert streaming SSE to JSON
|
||||
// generateText() expects a non-streaming JSON response, not SSE
|
||||
@@ -233,3 +293,14 @@ export async function handleSuccessResponse(
|
||||
headers: responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
function toNumber(v: string | null): number | undefined {
|
||||
if (v == null) return undefined;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
function toInt(v: string | null): number | undefined {
|
||||
if (v == null) return undefined;
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
@@ -307,6 +307,9 @@ export async function transformRequestBody(
|
||||
body.stream = true;
|
||||
body.instructions = codexInstructions;
|
||||
|
||||
// Prompt caching relies on the host providing a stable prompt_cache_key
|
||||
// (OpenCode passes its session identifier). We no longer synthesize one here.
|
||||
|
||||
// Filter and transform input
|
||||
if (body.input && Array.isArray(body.input)) {
|
||||
// Debug: Log original input message IDs before filtering
|
||||
|
||||
@@ -133,6 +133,8 @@ export interface RequestBody {
|
||||
verbosity?: "low" | "medium" | "high";
|
||||
};
|
||||
include?: string[];
|
||||
/** Stable key to enable prompt-token caching on Codex backend */
|
||||
prompt_cache_key?: string;
|
||||
max_output_tokens?: number;
|
||||
max_completion_tokens?: number;
|
||||
[key: string]: unknown;
|
||||
|
||||
+56
-33
@@ -1,9 +1,10 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
shouldRefreshToken,
|
||||
extractRequestUrl,
|
||||
rewriteUrlForCodex,
|
||||
createCodexHeaders,
|
||||
shouldRefreshToken,
|
||||
extractRequestUrl,
|
||||
rewriteUrlForCodex,
|
||||
createCodexHeaders,
|
||||
handleErrorResponse,
|
||||
} from '../lib/request/fetch-helpers.js';
|
||||
import type { Auth } from '../lib/types.js';
|
||||
import { URL_PATHS, OPENAI_HEADERS, OPENAI_HEADER_VALUES } from '../lib/constants.js';
|
||||
@@ -76,49 +77,71 @@ describe('Fetch Helpers Module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCodexHeaders', () => {
|
||||
const accountId = 'test-account-123';
|
||||
const accessToken = 'test-access-token';
|
||||
describe('createCodexHeaders', () => {
|
||||
const accountId = 'test-account-123';
|
||||
const accessToken = 'test-access-token';
|
||||
|
||||
it('should create headers with all required fields', () => {
|
||||
const headers = createCodexHeaders(undefined, accountId, accessToken);
|
||||
it('should create headers with all required fields when cache key provided', () => {
|
||||
const headers = createCodexHeaders(undefined, accountId, accessToken, { model: 'gpt-5-codex', promptCacheKey: 'session-1' });
|
||||
|
||||
expect(headers.get('Authorization')).toBe(`Bearer ${accessToken}`);
|
||||
expect(headers.get(OPENAI_HEADERS.ACCOUNT_ID)).toBe(accountId);
|
||||
expect(headers.get(OPENAI_HEADERS.BETA)).toBe(OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
||||
expect(headers.get(OPENAI_HEADERS.ORIGINATOR)).toBe(OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
||||
expect(headers.has(OPENAI_HEADERS.SESSION_ID)).toBe(true);
|
||||
});
|
||||
expect(headers.get('Authorization')).toBe(`Bearer ${accessToken}`);
|
||||
expect(headers.get(OPENAI_HEADERS.ACCOUNT_ID)).toBe(accountId);
|
||||
expect(headers.get(OPENAI_HEADERS.BETA)).toBe(OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
||||
expect(headers.get(OPENAI_HEADERS.ORIGINATOR)).toBe(OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
||||
expect(headers.get(OPENAI_HEADERS.SESSION_ID)).toBe('session-1');
|
||||
expect(headers.get(OPENAI_HEADERS.CONVERSATION_ID)).toBe('session-1');
|
||||
expect(headers.get('accept')).toBe('text/event-stream');
|
||||
});
|
||||
|
||||
it('enriches usage limit errors with friendly message and rate limits', async () => {
|
||||
const body = {
|
||||
error: {
|
||||
code: 'usage_limit_reached',
|
||||
message: 'limit reached',
|
||||
plan_type: 'pro',
|
||||
},
|
||||
};
|
||||
const headers = new Headers({
|
||||
'x-codex-primary-used-percent': '75',
|
||||
'x-codex-primary-window-minutes': '300',
|
||||
'x-codex-primary-reset-at': String(Math.floor(Date.now() / 1000) + 1800),
|
||||
});
|
||||
const resp = new Response(JSON.stringify(body), { status: 429, headers });
|
||||
const enriched = await handleErrorResponse(resp);
|
||||
expect(enriched.status).toBe(429);
|
||||
const json = await enriched.json() as any;
|
||||
expect(json.error).toBeTruthy();
|
||||
expect(json.error.friendly_message).toMatch(/usage limit/i);
|
||||
expect(json.error.rate_limits.primary.used_percent).toBe(75);
|
||||
expect(json.error.rate_limits.primary.window_minutes).toBe(300);
|
||||
expect(typeof json.error.rate_limits.primary.resets_at).toBe('number');
|
||||
});
|
||||
|
||||
it('should remove x-api-key header', () => {
|
||||
const init = { headers: { 'x-api-key': 'should-be-removed' } };
|
||||
const headers = createCodexHeaders(init, accountId, accessToken);
|
||||
const init = { headers: { 'x-api-key': 'should-be-removed' } } as any;
|
||||
const headers = createCodexHeaders(init, accountId, accessToken, { model: 'gpt-5', promptCacheKey: 'session-2' });
|
||||
|
||||
expect(headers.has('x-api-key')).toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve other existing headers', () => {
|
||||
const init = { headers: { 'Content-Type': 'application/json' } };
|
||||
const headers = createCodexHeaders(init, accountId, accessToken);
|
||||
const init = { headers: { 'Content-Type': 'application/json' } } as any;
|
||||
const headers = createCodexHeaders(init, accountId, accessToken, { model: 'gpt-5', promptCacheKey: 'session-3' });
|
||||
|
||||
expect(headers.get('Content-Type')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should generate unique session IDs', () => {
|
||||
const headers1 = createCodexHeaders(undefined, accountId, accessToken);
|
||||
const headers2 = createCodexHeaders(undefined, accountId, accessToken);
|
||||
|
||||
expect(headers1.get(OPENAI_HEADERS.SESSION_ID)).not.toBe(
|
||||
headers2.get(OPENAI_HEADERS.SESSION_ID)
|
||||
);
|
||||
it('should use provided promptCacheKey for both conversation_id and session_id', () => {
|
||||
const key = 'ses_abc123';
|
||||
const headers = createCodexHeaders(undefined, accountId, accessToken, { promptCacheKey: key });
|
||||
expect(headers.get(OPENAI_HEADERS.CONVERSATION_ID)).toBe(key);
|
||||
expect(headers.get(OPENAI_HEADERS.SESSION_ID)).toBe(key);
|
||||
});
|
||||
|
||||
it('should validate session ID format (UUID)', () => {
|
||||
const headers = createCodexHeaders(undefined, accountId, accessToken);
|
||||
const sessionId = headers.get(OPENAI_HEADERS.SESSION_ID);
|
||||
|
||||
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
||||
it('does not set conversation/session headers when no promptCacheKey provided', () => {
|
||||
const headers = createCodexHeaders(undefined, accountId, accessToken, { model: 'gpt-5' });
|
||||
expect(headers.get(OPENAI_HEADERS.CONVERSATION_ID)).toBeNull();
|
||||
expect(headers.get(OPENAI_HEADERS.SESSION_ID)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
normalizeModel,
|
||||
getModelConfig,
|
||||
filterInput,
|
||||
addToolRemapMessage,
|
||||
isOpenCodeSystemPrompt,
|
||||
filterOpenCodeSystemPrompts,
|
||||
addCodexBridgeMessage,
|
||||
transformRequestBody,
|
||||
normalizeModel,
|
||||
getModelConfig,
|
||||
filterInput,
|
||||
addToolRemapMessage,
|
||||
isOpenCodeSystemPrompt,
|
||||
filterOpenCodeSystemPrompts,
|
||||
addCodexBridgeMessage,
|
||||
transformRequestBody,
|
||||
} from '../lib/request/request-transformer.js';
|
||||
import { TOOL_REMAP_MESSAGE } from '../lib/prompts/codex.js';
|
||||
import { CODEX_OPENCODE_BRIDGE } from '../lib/prompts/codex-opencode-bridge.js';
|
||||
@@ -531,8 +531,29 @@ describe('Request Transformer Module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformRequestBody', () => {
|
||||
const codexInstructions = 'Test Codex Instructions';
|
||||
describe('transformRequestBody', () => {
|
||||
const codexInstructions = 'Test Codex Instructions';
|
||||
|
||||
it('preserves existing prompt_cache_key passed by host (OpenCode)', async () => {
|
||||
const body: RequestBody = {
|
||||
model: 'gpt-5-codex',
|
||||
input: [],
|
||||
// Host-provided key (OpenCode session id)
|
||||
// @ts-expect-error extra field allowed
|
||||
prompt_cache_key: 'ses_host_key_123',
|
||||
};
|
||||
const result: any = await transformRequestBody(body, codexInstructions);
|
||||
expect(result.prompt_cache_key).toBe('ses_host_key_123');
|
||||
});
|
||||
|
||||
it('leaves prompt_cache_key unset when host does not supply one', async () => {
|
||||
const body: RequestBody = {
|
||||
model: 'gpt-5',
|
||||
input: [],
|
||||
};
|
||||
const result: any = await transformRequestBody(body, codexInstructions);
|
||||
expect(result.prompt_cache_key).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set required Codex fields', async () => {
|
||||
const body: RequestBody = {
|
||||
|
||||
@@ -4,6 +4,17 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['test/**/*.test.ts'],
|
||||
exclude: [
|
||||
'node_modules/**',
|
||||
'.opencode/**',
|
||||
'dist/**',
|
||||
'tmp/**',
|
||||
'**/node_modules/**',
|
||||
'**/.opencode/**',
|
||||
'**/dist/**',
|
||||
'**/tmp/**',
|
||||
],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
|
||||
Reference in New Issue
Block a user