Commit f182fbd1 authored by Your Name's avatar Your Name

All working but qwen-oauth2

parent 8b50a0ae
{
"name": ".kilo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.14"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.14.tgz",
"integrity": "sha512-mS+WA9HZIBH2qQ9ARA+v0q4MdQTSdfOvKbe4AOSkjP+P5hVA70OM/UVM9DVcvmjSOxU+wuUxmOy+j/EQIrgFmw==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.14",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.14.tgz",
"integrity": "sha512-Naz83lFrsbavuDp6UwxRuglOaSNvRBsZfcRNvb7RpWYAwbuJP0dBdhpXj6uO3ta5qxeQ2JzxKNC9Ffz+LCLLDg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
...@@ -25,7 +25,7 @@ import json ...@@ -25,7 +25,7 @@ import json
import logging import logging
import time import time
import uuid import uuid
from typing import Dict, List, Optional, Union, AsyncIterator from typing import Dict, List, Optional, Union, AsyncIterator, Tuple
from openai import OpenAI from openai import OpenAI
import httpx import httpx
...@@ -49,8 +49,8 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -49,8 +49,8 @@ class CodexProviderHandler(BaseProviderHandler):
- Standard OpenAI protocol with Bearer token - Standard OpenAI protocol with Bearer token
**OAuth2 Mode** (no api_key, OAuth2 credentials): **OAuth2 Mode** (no api_key, OAuth2 credentials):
- Uses ChatGPT backend API: https://chatgpt.com/backend-api/codex - Uses ChatGPT backend API: https://chatgpt.com/backend-api
- Uses Responses API endpoint: /v1/responses - Uses Responses API endpoint: /codex/responses
- ChatGPT-specific protocol with SSE streaming - ChatGPT-specific protocol with SSE streaming
- Includes ChatGPT-Account-ID header - Includes ChatGPT-Account-ID header
...@@ -104,7 +104,7 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -104,7 +104,7 @@ class CodexProviderHandler(BaseProviderHandler):
# OAuth2 Mode: Check if OAuth2 is authenticated # OAuth2 Mode: Check if OAuth2 is authenticated
# If authenticated, use ChatGPT backend; otherwise use configured endpoint # If authenticated, use ChatGPT backend; otherwise use configured endpoint
if self.oauth2.is_authenticated(): if self.oauth2.is_authenticated():
self.base_url = "https://chatgpt.com/backend-api/codex" self.base_url = "https://chatgpt.com/backend-api"
logger.info(f"CodexProviderHandler: Initialized in OAuth2 mode with ChatGPT backend: {self.base_url}") logger.info(f"CodexProviderHandler: Initialized in OAuth2 mode with ChatGPT backend: {self.base_url}")
else: else:
# Not yet authenticated, keep configured endpoint # Not yet authenticated, keep configured endpoint
...@@ -160,8 +160,8 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -160,8 +160,8 @@ class CodexProviderHandler(BaseProviderHandler):
self._account_id = self.oauth2.credentials['tokens'].get('account_id') self._account_id = self.oauth2.credentials['tokens'].get('account_id')
# Switch to ChatGPT backend if OAuth2 is now authenticated # Switch to ChatGPT backend if OAuth2 is now authenticated
if not self._use_api_key_mode and self.base_url != "https://chatgpt.com/backend-api/codex": if not self._use_api_key_mode and self.base_url != "https://chatgpt.com/backend-api":
self.base_url = "https://chatgpt.com/backend-api/codex" self.base_url = "https://chatgpt.com/backend-api"
logger.info(f"CodexProviderHandler: Switched to ChatGPT backend after OAuth2 authentication: {self.base_url}") logger.info(f"CodexProviderHandler: Switched to ChatGPT backend after OAuth2 authentication: {self.base_url}")
# Update the configuration with the new endpoint # Update the configuration with the new endpoint
...@@ -257,19 +257,35 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -257,19 +257,35 @@ class CodexProviderHandler(BaseProviderHandler):
# OAuth2 Mode Methods (ChatGPT Responses API) # OAuth2 Mode Methods (ChatGPT Responses API)
# ========================================================================= # =========================================================================
def _convert_messages_to_responses_format(self, messages: List[Dict]) -> List[Dict]: def _convert_messages_to_responses_format(self, messages: List[Dict]) -> Tuple[List[Dict], Optional[str]]:
""" """
Convert OpenAI Chat Completions messages to Responses API format. Convert OpenAI Chat Completions messages to Responses API format.
OpenAI format: {"role": "user", "content": "text"} OpenAI format: {"role": "user", "content": "text"}
Responses format: {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "text"}]} Responses format: {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "text"}]}
Returns:
tuple: (converted_messages, system_instruction)
- converted_messages: List of messages in Responses API format
- system_instruction: Combined system message content (if any)
""" """
result = [] result = []
system_instructions = []
for msg in messages: for msg in messages:
role = msg.get("role", "user") role = msg.get("role", "user")
content = msg.get("content", "") content = msg.get("content", "")
# Handle system messages - extract for instructions field
if role == "system":
if isinstance(content, str):
system_instructions.append(content)
elif isinstance(content, list):
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
system_instructions.append(item.get("text", ""))
continue
# Handle tool messages # Handle tool messages
if role == "tool": if role == "tool":
result.append({ result.append({
...@@ -299,17 +315,17 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -299,17 +315,17 @@ class CodexProviderHandler(BaseProviderHandler):
}) })
continue continue
# Handle regular messages # Handle regular messages (user, developer, assistant)
content_items = [] content_items = []
if isinstance(content, str): if isinstance(content, str):
content_type = "input_text" if role in ["user", "system", "developer"] else "output_text" content_type = "input_text" if role in ["user", "developer"] else "output_text"
content_items.append({"type": content_type, "text": content}) content_items.append({"type": content_type, "text": content})
elif isinstance(content, list): elif isinstance(content, list):
# Handle multimodal content # Handle multimodal content
for item in content: for item in content:
if isinstance(item, dict): if isinstance(item, dict):
if item.get("type") == "text": if item.get("type") == "text":
content_type = "input_text" if role in ["user", "system", "developer"] else "output_text" content_type = "input_text" if role in ["user", "developer"] else "output_text"
content_items.append({"type": content_type, "text": item.get("text", "")}) content_items.append({"type": content_type, "text": item.get("text", "")})
elif item.get("type") == "image_url": elif item.get("type") == "image_url":
content_items.append({ content_items.append({
...@@ -324,7 +340,40 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -324,7 +340,40 @@ class CodexProviderHandler(BaseProviderHandler):
"content": content_items "content": content_items
}) })
return result # Combine system instructions
combined_system = " ".join(system_instructions) if system_instructions else None
return result, combined_system
def _convert_tools_to_codex_format(self, tools: Optional[List[Dict]]) -> List[Dict]:
"""
Convert OpenAI tool format to Codex/ChatGPT format.
OpenAI format: {"type": "function", "function": {"name": "...", "description": "...", "parameters": {...}}}
Codex format: {"type": "function", "name": "...", "description": "...", "parameters": {...}}
Key difference: No nested "function" object in Codex format.
"""
if not tools:
return []
converted_tools = []
for tool in tools:
if tool.get("type") == "function" and "function" in tool:
# OpenAI format - flatten it
func = tool["function"]
converted_tool = {
"type": "function",
"name": func.get("name"),
"description": func.get("description", ""),
"parameters": func.get("parameters", {}),
}
converted_tools.append(converted_tool)
else:
# Already in Codex format or other type
converted_tools.append(tool)
return converted_tools
def _build_responses_request( def _build_responses_request(
self, self,
...@@ -336,29 +385,32 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -336,29 +385,32 @@ class CodexProviderHandler(BaseProviderHandler):
tool_choice: Optional[Union[str, Dict]] = None, tool_choice: Optional[Union[str, Dict]] = None,
) -> Dict: ) -> Dict:
"""Build a Responses API request payload.""" """Build a Responses API request payload."""
# Convert messages to Responses format # Convert messages to Responses format and extract system instructions
input_items = self._convert_messages_to_responses_format(messages) input_items, system_instruction = self._convert_messages_to_responses_format(messages)
# Use system instruction from messages if available, otherwise use default
instructions = system_instruction if system_instruction else "You are Codex, a helpful AI assistant for coding tasks."
# Convert tools to Codex format (flatten the structure)
codex_tools = self._convert_tools_to_codex_format(tools)
# Build base request # Build base request
request = { request = {
"model": model, "model": model,
"instructions": "You are Codex, a helpful AI assistant for coding tasks.", "instructions": instructions,
"input": input_items, "input": input_items,
"stream": True, "stream": True,
"store": False, "store": False,
"tools": codex_tools,
"tool_choice": "auto",
"parallel_tool_calls": True,
} }
# Add optional parameters # Add optional parameters
if max_tokens is not None: # Note: temperature and max_tokens are not supported by /codex/responses endpoint
request["max_tokens"] = max_tokens # They are handled internally by the model
if temperature is not None:
request["temperature"] = temperature
if tools:
# Convert OpenAI tool format to Responses API format
request["tools"] = tools
# Override tool_choice if explicitly provided
if tool_choice: if tool_choice:
request["tool_choice"] = tool_choice if isinstance(tool_choice, str) else "auto" request["tool_choice"] = tool_choice if isinstance(tool_choice, str) else "auto"
...@@ -506,16 +558,24 @@ class CodexProviderHandler(BaseProviderHandler): ...@@ -506,16 +558,24 @@ class CodexProviderHandler(BaseProviderHandler):
headers = self._build_headers(api_key, conversation_id) headers = self._build_headers(api_key, conversation_id)
# Make request to Responses API # Make request to Responses API
url = f"{self.base_url}/v1/responses" url = f"{self.base_url}/codex/responses"
logger.info(f"CodexProviderHandler: Sending request to {url}") logger.info(f"CodexProviderHandler: Sending request to {url}")
if AISBF_DEBUG:
logger.info(f"CodexProviderHandler: Request payload: {json.dumps(request_payload, indent=2)}")
logger.info(f"CodexProviderHandler: Request headers: {json.dumps({k: v for k, v in headers.items() if k.lower() != 'authorization'}, indent=2)}")
async with httpx.AsyncClient(timeout=300.0) as client: async with httpx.AsyncClient(timeout=300.0) as client:
response = await client.post( async with client.stream(
"POST",
url, url,
headers=headers, headers=headers,
json=request_payload, json=request_payload,
) ) as response:
if response.status_code >= 400:
error_body = await response.aread()
logger.error(f"CodexProviderHandler: Error response status: {response.status_code}")
logger.error(f"CodexProviderHandler: Error response body: {error_body.decode('utf-8')}")
response.raise_for_status() response.raise_for_status()
......
...@@ -25,6 +25,7 @@ import asyncio ...@@ -25,6 +25,7 @@ import asyncio
import time import time
import json import json
import platform import platform
import uuid
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from openai import AsyncOpenAI from openai import AsyncOpenAI
from ..models import Model from ..models import Model
...@@ -130,19 +131,35 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -130,19 +131,35 @@ class QwenProviderHandler(BaseProviderHandler):
logger.info("QwenProviderHandler: Using OAuth2 authentication") logger.info("QwenProviderHandler: Using OAuth2 authentication")
auth_key = access_token auth_key = access_token
# Use provider configured endpoint for OAuth2 (fixed endpoints) # Get resource URL from auth and normalize it properly
base_url = self.provider_config.endpoint base_url = self.auth.get_resource_url()
# Normalize endpoint # Normalize endpoint exactly as specified in documentation
if not base_url.startswith("http"): if not base_url.startswith("http"):
base_url = f"https://{base_url}" base_url = f"https://{base_url}"
# DashScope endpoint already includes /v1 so do not append again
if not base_url.endswith("/v1"):
base_url = f"{base_url}/v1"
logger.info(f"QwenProviderHandler: Final endpoint: {base_url}")
# Build required DashScope headers
import uuid
user_agent = f"QwenCode/1.0.0 ({platform.system().lower()}; {platform.machine()})"
default_headers = {
"Accept": "application/json",
"X-DashScope-CacheControl": "enable",
"X-DashScope-UserAgent": user_agent,
"X-DashScope-AuthType": "qwen-oauth",
"x-request-id": str(uuid.uuid4()),
}
self._sdk_client = AsyncOpenAI( self._sdk_client = AsyncOpenAI(
api_key=auth_key, api_key=auth_key,
base_url=base_url, base_url=base_url,
max_retries=3, max_retries=3,
timeout=httpx.Timeout(300.0, connect=30.0), timeout=httpx.Timeout(300.0, connect=30.0),
default_headers=default_headers,
) )
logger.info(f"QwenProviderHandler: Created SDK client (endpoint: {base_url})") logger.info(f"QwenProviderHandler: Created SDK client (endpoint: {base_url})")
...@@ -223,12 +240,22 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -223,12 +240,22 @@ class QwenProviderHandler(BaseProviderHandler):
# Get SDK client with current OAuth token # Get SDK client with current OAuth token
client = await self._get_sdk_client() client = await self._get_sdk_client()
# Generate session tracking IDs
session_id = str(uuid.uuid4())
prompt_id = str(uuid.uuid4())
# Build request parameters # Build request parameters
request_params = { request_params = {
"model": model, "model": model,
"messages": messages, "messages": messages,
"max_tokens": max_tokens or 4096, "max_tokens": max_tokens or 4096,
"stream": stream, "stream": stream,
"extra_body": {
"metadata": {
"sessionId": session_id,
"promptId": prompt_id
}
}
} }
if temperature is not None and temperature > 0: if temperature is not None and temperature > 0:
...@@ -240,6 +267,10 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -240,6 +267,10 @@ class QwenProviderHandler(BaseProviderHandler):
if tool_choice and tools: if tool_choice and tools:
request_params["tool_choice"] = tool_choice request_params["tool_choice"] = tool_choice
# Add stream_options for streaming requests
if stream:
request_params["stream_options"] = {"include_usage": True}
try: try:
if stream: if stream:
logger.info("QwenProviderHandler: Using streaming mode") logger.info("QwenProviderHandler: Using streaming mode")
...@@ -440,16 +471,37 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -440,16 +471,37 @@ class QwenProviderHandler(BaseProviderHandler):
using_api_key = qwen_config and isinstance(qwen_config, dict) and qwen_config.get('api_key') using_api_key = qwen_config and isinstance(qwen_config, dict) and qwen_config.get('api_key')
if not using_api_key: if not using_api_key:
# OAuth2 authentication: return fixed model list # OAuth2 authentication: return full model list
logger.info("QwenProviderHandler: Using OAuth2 authentication, returning fixed model list") logger.info("QwenProviderHandler: Using OAuth2 authentication, returning full model list")
return [ return [
Model( Model(
id="coder-model", id="qwen-turbo",
name="Coder Model", name="Qwen Turbo",
provider_id=self.provider_id, provider_id=self.provider_id,
context_size=1000000, context_size=32000,
context_length=1000000, context_length=32000,
) ),
Model(
id="qwen-plus",
name="Qwen Plus",
provider_id=self.provider_id,
context_size=128000,
context_length=128000,
),
Model(
id="qwen-max",
name="Qwen Max",
provider_id=self.provider_id,
context_size=128000,
context_length=128000,
),
Model(
id="qwen3-coder-plus",
name="Qwen 3 Coder Plus",
provider_id=self.provider_id,
context_size=128000,
context_length=128000,
),
] ]
# API token authentication: fetch from models endpoint # API token authentication: fetch from models endpoint
...@@ -502,10 +554,10 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -502,10 +554,10 @@ class QwenProviderHandler(BaseProviderHandler):
# Fallback to static model list # Fallback to static model list
logger.warning("QwenProviderHandler: No models returned from API, using static list") logger.warning("QwenProviderHandler: No models returned from API, using static list")
models = [ models = [
Model(id="qwen-plus", name="Qwen Plus", provider_id=self.provider_id, context_size=32000), Model(id="qwen-turbo", name="Qwen Turbo", provider_id=self.provider_id, context_size=32000),
Model(id="qwen-turbo", name="Qwen Turbo", provider_id=self.provider_id, context_size=8000), Model(id="qwen-plus", name="Qwen Plus", provider_id=self.provider_id, context_size=128000),
Model(id="qwen-max", name="Qwen Max", provider_id=self.provider_id, context_size=8000), Model(id="qwen-max", name="Qwen Max", provider_id=self.provider_id, context_size=128000),
Model(id="coder-model", name="Qwen Coder", provider_id=self.provider_id, context_size=32000), Model(id="qwen3-coder-plus", name="Qwen 3 Coder Plus", provider_id=self.provider_id, context_size=128000),
] ]
logger.info(f"QwenProviderHandler: Returning {len(models)} models") logger.info(f"QwenProviderHandler: Returning {len(models)} models")
...@@ -517,8 +569,8 @@ class QwenProviderHandler(BaseProviderHandler): ...@@ -517,8 +569,8 @@ class QwenProviderHandler(BaseProviderHandler):
# Return static fallback list # Return static fallback list
logger.info("QwenProviderHandler: Using static fallback model list") logger.info("QwenProviderHandler: Using static fallback model list")
return [ return [
Model(id="qwen-plus", name="Qwen Plus", provider_id=self.provider_id, context_size=32000), Model(id="qwen-turbo", name="Qwen Turbo", provider_id=self.provider_id, context_size=32000),
Model(id="qwen-turbo", name="Qwen Turbo", provider_id=self.provider_id, context_size=8000), Model(id="qwen-plus", name="Qwen Plus", provider_id=self.provider_id, context_size=128000),
Model(id="qwen-max", name="Qwen Max", provider_id=self.provider_id, context_size=8000), Model(id="qwen-max", name="Qwen Max", provider_id=self.provider_id, context_size=128000),
Model(id="coder-model", name="Qwen Coder", provider_id=self.provider_id, context_size=32000), Model(id="qwen3-coder-plus", name="Qwen 3 Coder Plus", provider_id=self.provider_id, context_size=128000),
] ]
# ChatGPT API Implementation Guide for Codex-CLI # Complete ChatGPT/OpenAI API Request Flow Documentation
This document provides a comprehensive analysis of how codex-cli communicates with ChatGPT API endpoints, including exact endpoints, headers, authentication, request schemas, and implementation details. This document provides a comprehensive analysis of how Codex sends requests to the ChatGPT/OpenAI API, including authentication, headers, request format, session management, and endpoints.
## Table of Contents ## Table of Contents
1. [Overview](#overview) 1. [Authentication & OAuth2 Flow](#1-authentication--oauth2-flow)
2. [API Endpoints](#api-endpoints) 2. [API Endpoints](#2-api-endpoints)
3. [Authentication](#authentication) 3. [Request Headers](#3-request-headers)
4. [Request Headers](#request-headers) 4. [Request Body Format](#4-request-body-format)
5. [Request/Response Schemas](#requestresponse-schemas) 5. [Session Management](#5-session-management)
6. [Model List Retrieval](#model-list-retrieval) 6. [Transport Mechanisms](#6-transport-mechanisms)
7. [Streaming Responses](#streaming-responses) 7. [System Prompt (Instructions)](#7-system-prompt-instructions)
8. [WebSocket Support](#websocket-support) 8. [Message Format Conversion](#8-message-format-conversion-openai-compatible--chatgpt)
9. [Message Conversion Between OpenAI and Codex Formats](#message-conversion-between-openai-and-codex-formats) 9. [Complete Request Flow](#9-complete-request-flow)
10. [Python Implementation Examples](#python-implementation-examples) 10. [Python Implementation Example](#10-python-implementation-example)
11. [Implementation Examples (Rust)](#implementation-examples-rust)
12. [Developer Role Messages](#developer-role-messages)
13. [Session Flow](#session-flow)
--- ---
## Overview ## 1. Authentication & OAuth2 Flow
Codex-CLI uses OpenAI's **Responses API** (not the Chat Completions API) to communicate with ChatGPT. The primary endpoints are: ### OAuth2 Token Management
- **Base URL (ChatGPT mode)**: `https://chatgpt.com/backend-api/codex` **Location**: `codex-rs/login/src/auth/manager.rs`
- **Base URL (API Key mode)**: `https://api.openai.com/v1`
The client supports both HTTP/SSE and WebSocket transports for streaming responses. Codex uses OAuth2 device code flow for ChatGPT authentication:
--- #### 1.1 Device Code Request
## API Endpoints
### Primary Endpoints - **Endpoint**: `https://auth0.openai.com/oauth/device/code`
- **Client ID**: Retrieved from configuration
- **Scope**: `openid profile email offline_access`
#### 1. Responses Endpoint (Streaming) #### 1.2 Token Exchange
- **Path**: `/v1/responses` (or `/responses` relative to base)
- **Method**: `POST`
- **Purpose**: Stream AI responses for a given prompt
- **Transport**: HTTP with Server-Sent Events (SSE) or WebSocket
#### 2. Models Endpoint - **Endpoint**: `https://auth0.openai.com/oauth/token`
- **Path**: `/v1/models` (or `/models` relative to base) - **Grant type**: `urn:ietf:params:oauth:grant-type:device_code`
- **Method**: `GET` - **Returns**: `access_token`, `refresh_token`, `id_token`
- **Purpose**: Retrieve available models and their capabilities
- **Query Parameters**: `client_version=<version>` (e.g., `0.99.0`)
#### 3. Compact Endpoint #### 1.3 Token Storage
- **Path**: `/v1/responses/compact` (or `/responses/compact` relative to base)
- **Method**: `POST`
- **Purpose**: Compact conversation history
#### 4. Memory Summarization Endpoint - Tokens stored in `~/.codex/auth.json` or system keyring
- **Path**: `/v1/memories/trace_summarize` (or `/memories/trace_summarize` relative to base) - ID token contains `chatgpt_account_id` claim
- **Method**: `POST` - Access token used for API authentication
- **Purpose**: Summarize memory traces
### ChatGPT-Specific Backend Endpoints (OAuth Mode) #### 1.4 Token Refresh
When using ChatGPT authentication, additional endpoints are available: - Automatic refresh on 401 responses
- Uses refresh token to get new access token
- Implements retry logic with exponential backoff
#### 5. Config Requirements ### Token Data Structure
- **Path**: `/backend-api/wham/config/requirements`
- **Method**: `GET`
- **Purpose**: Retrieve cloud configuration requirements
#### 6. Rate Limits ```rust
- **Path**: `/backend-api/api/codex/usage` pub struct TokenData {
- **Method**: `GET` pub access_token: String,
- **Purpose**: Get account rate limits pub refresh_token: Option<String>,
pub account_id: Option<String>, // From chatgpt_account_id claim
pub id_token: IdTokenClaims,
}
```
#### 7. Plugin/App Management ---
- **Path**: `/backend-api/plugins/list`
- **Method**: `GET`
- **Purpose**: List installed plugins
- **Path**: `/backend-api/plugins/featured` ## 2. API Endpoints
- **Method**: `GET`
- **Query Parameters**: `platform=codex`
- **Purpose**: Get featured plugins
- **Path**: `/backend-api/plugins/{plugin_id}/enable` ### Base URL
- **Method**: `POST`
- **Purpose**: Enable a plugin
- **Path**: `/backend-api/plugins/{plugin_id}/uninstall` - **Default**: `https://chatgpt.com/backend-api/`
- **Method**: `POST` - **Configurable**: via `chatgpt_base_url` in config
- **Purpose**: Uninstall a plugin
#### 8. MCP Apps ### Primary Endpoints
- **Path**: `/backend-api/wham/apps`
- **Method**: WebSocket connection
- **Purpose**: MCP (Model Context Protocol) server communication
**Important Note**: When using ChatGPT OAuth authentication, the base instructions field in the request is **required** and should reference "Codex" specifically. The example from the blog post shows: #### 2.1 Responses API (Main chat endpoint)
```json - **Path**: `/v1/responses`
{ - **Method**: `POST`
"instructions": "You are Codex, based on GPT-5. You are running as a coding agent ..." - **Transport**: HTTP (SSE) or WebSocket
} - **Purpose**: Streaming chat completions
```
This appears to be a requirement for the ChatGPT backend API to accept requests properly. #### 2.2 Compact API (History compression)
--- - **Path**: `/v1/responses/compact`
- **Method**: `POST`
- **Purpose**: Compress conversation history
## Authentication #### 2.3 Memories API (Memory summarization)
### Two Authentication Modes - **Path**: `/v1/memories/trace_summarize`
- **Method**: `POST`
- **Purpose**: Summarize conversation memories
#### 1. API Key Mode #### 2.4 Models List
- **Header**: `Authorization: Bearer <api_key>`
- **Source**: Environment variable (typically `OPENAI_API_KEY`)
- **Base URL**: `https://api.openai.com/v1`
#### 2. ChatGPT Mode (OAuth2) - **Path**: `/models`
- **Header**: `Authorization: Bearer <access_token>` - **Method**: `GET`
- **Additional Header**: `ChatGPT-Account-ID: <account_id>` - **Purpose**: Retrieve available models
- **Base URL**: `https://chatgpt.com/backend-api/codex`
- **Token Management**: Automatic refresh on 401 responses
### Authentication Implementation #### 2.5 Plugins
The authentication is handled through the `AuthProvider` trait: - **Path**: `/plugins/list`
- **Path**: `/plugins/featured`
- **Path**: `/plugins/export/curated`
```rust #### 2.6 Files (OpenAI file uploads)
pub trait AuthProvider: Send + Sync {
fn bearer_token(&self) -> Option<String>;
fn account_id(&self) -> Option<String> {
None
}
}
```
Headers are added via: - **Path**: `/files`
- **Method**: `POST`
- **Path**: `/files/{file_id}/uploaded`
- **Method**: `POST`
```rust #### 2.7 Realtime (Voice/WebRTC)
pub(crate) fn add_auth_headers_to_header_map<A: AuthProvider>(auth: &A, headers: &mut HeaderMap) {
if let Some(token) = auth.bearer_token()
&& let Ok(header) = HeaderValue::from_str(&format!("Bearer {token}"))
{
let _ = headers.insert(http::header::AUTHORIZATION, header);
}
if let Some(account_id) = auth.account_id()
&& let Ok(header) = HeaderValue::from_str(&account_id)
{
let _ = headers.insert("ChatGPT-Account-ID", header);
}
}
```
**Location**: `codex-rs/codex-api/src/auth.rs` - **Path**: `/v1/realtime/calls`
- **Method**: `POST`
- **Purpose**: Create WebRTC voice sessions
--- ---
## Request Headers ## 3. Request Headers
### Standard Headers (All Requests)
1. **User-Agent**
- Format: `{originator}/{version} ({os} {os_version}; {arch}) {terminal_type} ({suffix})`
- Example: `codex_cli_rs/0.99.0 (Linux 6.12; x86_64) xterm-256color (vscode; 1.86.0)`
- **Location**: `codex-rs/login/src/auth/default_client.rs:131-155`
2. **originator**
- Value: `codex_cli_rs` (default) or custom via `CODEX_INTERNAL_ORIGINATOR_OVERRIDE`
- Purpose: Identifies the client application
3. **Content-Type** ### 3.1 Authentication Headers
- Value: `application/json` (for POST requests)
4. **Accept** **Location**: `codex-rs/model-provider/src/bearer_auth_provider.rs`
- Value: `text/event-stream` (for SSE streaming)
### Responses API Specific Headers ```http
Authorization: Bearer {access_token}
ChatGPT-Account-ID: {account_id}
```
5. **x-client-request-id** For FedRAMP accounts:
- Value: Thread/conversation ID
- Purpose: Request correlation
6. **session_id** ```http
- Value: Thread/conversation ID X-OpenAI-Fedramp: true
- Purpose: Session tracking ```
7. **x-codex-turn-state** ### 3.2 Standard Headers
- Value: Sticky routing token from previous response
- Purpose: Maintain routing to same backend instance within a turn
8. **x-codex-turn-metadata** **Location**: `codex-rs/login/src/auth/default_client.rs`
- Value: Optional turn metadata (JSON)
- Purpose: Observability and debugging
9. **x-codex-window-id** ```http
- Format: `{conversation_id}:{window_generation}` User-Agent: {originator}/{version} ({os} {os_version}; {arch}) {terminal_info} ({suffix})
- Purpose: Window/context tracking originator: codex_cli_rs
Content-Type: application/json
```
10. **x-openai-subagent** Example User-Agent:
- Values: `review`, `compact`, `memory_consolidation`, `collab_spawn`
- Purpose: Identify subagent requests
11. **x-codex-parent-thread-id** ```
- Value: Parent thread ID (for spawned threads) codex_cli_rs/0.1.0 (Linux 5.15.0; x86_64) iTerm2/3.4.0
- Purpose: Thread hierarchy tracking ```
12. **x-codex-beta-features** ### 3.3 Codex-Specific Headers
- Value: Comma-separated beta feature keys
- Purpose: Enable experimental features
13. **x-responsesapi-include-timing-metrics** **Location**: `codex-rs/core/src/client.rs`
- Value: `true`
- Purpose: Request timing metrics in response
### WebSocket Specific Headers #### Session Identification
14. **OpenAI-Beta** ```http
- Value: `responses_websockets=2026-02-06` session_id: {conversation_id}
- Purpose: Enable WebSocket protocol version x-client-request-id: {conversation_id}
```
### Developer Role Messages #### Installation Tracking
When using the ChatGPT OAuth API, requests often include a `developer` role message in the input array. This is distinct from the `instructions` field: ```http
x-codex-installation-id: {installation_id}
```
- **`instructions` field**: Base system instructions (e.g., "You are Codex, based on GPT-5...") #### Window/Turn Tracking
- **`developer` role message**: Additional contextual instructions injected as a message in the conversation
Example from the blog post: ```http
```json x-codex-window-id: {conversation_id}:{window_generation}
{ x-codex-turn-state: {sticky_routing_token}
"input": [ x-codex-turn-metadata: {base64_encoded_metadata}
{
"type": "message",
"role": "developer",
"content": [
{
"type": "input_text",
"text": "You are a helpful assistant. Respond directly to the user request without running tools or shell commands."
}
]
},
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Generate an SVG of a pelican riding a bicycle"
}
]
}
]
}
``` ```
The `developer` role is used for: #### Subagent Identification
- Permission instructions (sandbox mode, approval policies)
- Capability instructions (available tools, restrictions)
- Context-specific guidance (collaboration mode, personality specs)
- Model switching notifications
- Realtime conversation boundaries
**Location**: `codex-rs/protocol/src/models.rs:755-767`
### Optional Headers ```http
x-openai-subagent: review|compact|memory_consolidation|collab_spawn
x-codex-parent-thread-id: {parent_thread_id}
```
15. **OpenAI-Organization** #### Feature Flags
- Source: `OPENAI_ORGANIZATION` environment variable
- Purpose: Organization routing
16. **OpenAI-Project** ```http
- Source: `OPENAI_PROJECT` environment variable x-codex-beta-features: {comma_separated_features}
- Purpose: Project routing OpenAI-Beta: responses_websockets=2026-02-06
```
17. **version** #### Telemetry
- Value: Package version (e.g., `0.99.0`)
- Purpose: Client version tracking
### Request Compression ```http
x-responsesapi-include-timing-metrics: true
```
When using ChatGPT authentication with OpenAI provider: #### Residency (Enterprise)
- **Content-Encoding**: `zstd`
- Body is compressed using Zstandard algorithm
**Location**: `codex-rs/core/src/client.rs:1040-1049` ```http
x-openai-internal-codex-residency: us
```
--- ---
## Request/Response Schemas ## 4. Request Body Format
### Responses API Request Schema ### 4.1 Main Request Structure
**Location**: `codex-rs/codex-api/src/common.rs`
```json ```json
{ {
"model": "gpt-4", "model": "gpt-4",
"instructions": "You are a helpful assistant...", "instructions": "You are a helpful coding assistant...",
"input": [ "input": [
{ {
"type": "message", "type": "message",
...@@ -317,1244 +236,1248 @@ When using ChatGPT authentication with OpenAI provider: ...@@ -317,1244 +236,1248 @@ When using ChatGPT authentication with OpenAI provider:
"stream": true, "stream": true,
"include": ["reasoning.encrypted_content"], "include": ["reasoning.encrypted_content"],
"service_tier": "default", "service_tier": "default",
"prompt_cache_key": "<conversation_id>", "prompt_cache_key": "{conversation_id}",
"text": { "text": {
"type": "text", "verbosity": "medium",
"verbosity": "normal" "format": {
"type": "json_schema",
"strict": true,
"schema": {},
"name": "codex_output_schema"
}
},
"client_metadata": {
"x-codex-installation-id": "...",
"ws_request_header_traceparent": "...",
"ws_request_header_tracestate": "..."
} }
} }
``` ```
**Key Fields**: ### 4.2 Input Items (ResponseItem)
- `model`: Model identifier (e.g., `gpt-4`, `o1-preview`)
- `instructions`: System instructions/base prompt
- `input`: Array of conversation items (messages, tool calls, tool results)
- `tools`: Available tools in JSON Schema format
- `reasoning`: Reasoning configuration (effort level, summary mode)
- `store`: Whether to store in Azure (provider-specific)
- `stream`: Always `true` for streaming
- `include`: Additional fields to include in response
- `service_tier`: Priority level (`default`, `priority`, `flex`)
- `prompt_cache_key`: Cache key for prompt caching
- `text`: Text generation parameters (verbosity, output schema)
**Location**: `codex-rs/codex-api/src/endpoint/responses.rs`
### Response Stream Events (SSE)
The server sends Server-Sent Events with the following event types:
1. **response.created**
```json
event: response.created
data: {"response_id": "resp_123", "status": "in_progress"}
```
2. **response.output_item.added**
```json
event: response.output_item.added
data: {"item": {"type": "message", "role": "assistant", "content": []}}
```
3. **response.content_part.added**
```json
event: response.content_part.added
data: {"part": {"type": "text", "text": ""}}
```
4. **response.content_part.delta**
```json
event: response.content_part.delta
data: {"delta": {"text": "Hello"}}
```
5. **response.output_item.done**
```json
event: response.output_item.done
data: {"item": {"type": "message", "role": "assistant", "content": [...]}}
```
6. **response.done**
```json
event: response.done
data: {
"response_id": "resp_123",
"usage": {
"input_tokens": 100,
"output_tokens": 50,
"total_tokens": 150,
"cached_input_tokens": 0,
"reasoning_output_tokens": 0
}
}
```
7. **error**
```json
event: error
data: {"error": {"message": "Rate limit exceeded", "code": "rate_limit_exceeded"}}
```
**Location**: Event parsing in `codex-rs/codex-api/src/sse.rs` **Location**: `codex-rs/protocol/src/models.rs`
### Models Response Schema The `input` array contains conversation history as `ResponseItem` objects:
```json ```rust
{ pub enum ResponseItem {
"models": [ Message {
{ role: String, // "user", "assistant", "developer"
"slug": "gpt-4", content: Vec<ContentItem>,
"display_name": "GPT-4", },
"description": "Most capable model", FunctionCall {
"default_reasoning_level": "medium", call_id: String,
"supported_reasoning_levels": [ name: String,
{"effort": "low", "description": "Fast"}, arguments: String,
{"effort": "medium", "description": "Balanced"}, },
{"effort": "high", "description": "Thorough"} FunctionCallOutput {
], call_id: String,
"shell_type": "shell_command", output: FunctionCallOutputPayload,
"visibility": "list", },
"minimal_client_version": [0, 99, 0], CustomToolCall {
"supported_in_api": true, call_id: String,
"priority": 1, name: String,
"upgrade": null, input: Value,
"base_instructions": "You are a helpful assistant", },
"supports_reasoning_summaries": true, CustomToolCallOutput {
"support_verbosity": true, call_id: String,
"default_verbosity": "normal", output: FunctionCallOutputPayload,
"apply_patch_tool_type": "unified_diff", },
"truncation_policy": {"mode": "bytes", "limit": 100000}, Reasoning {
"supports_parallel_tool_calls": true, content: Vec<ReasoningContent>,
"supports_image_detail_original": true, summary: Vec<String>,
"context_window": 128000, },
"experimental_supported_tools": [] // ... other types
}
]
} }
``` ```
**Location**: `codex-rs/codex-api/src/endpoint/models.rs` ### 4.3 Content Items
```rust
pub enum ContentItem {
InputText { text: String },
InputImage {
source: ImageSource,
detail: Option<String>,
},
OutputText { text: String },
// ... other types
}
```
--- ---
## Model List Retrieval ## 5. Session Management
### Request Details ### 5.1 Conversation ID (ThreadId)
**Endpoint**: `GET /v1/models?client_version=<version>` **Location**: `codex-rs/protocol/src/protocol.rs`
**Headers**: - **Format**: UUID v4
- `Authorization: Bearer <token>` - **Persistence**: Across entire conversation
- `ChatGPT-Account-ID: <account_id>` (if ChatGPT mode) - **Used for**:
- `User-Agent: <codex_user_agent>` - Session identification (`session_id` header)
- `originator: <originator>` - Prompt caching (`prompt_cache_key`)
- Window tracking (`x-codex-window-id`)
**Query Parameters**: ### 5.2 Window Generation
- `client_version`: Client version string (e.g., `0.99.0`)
### Response Handling **Location**: `codex-rs/core/src/client.rs`
The response includes an `ETag` header for caching: - Increments when conversation context is reset
- **Format**: `{conversation_id}:{window_generation}`
- **Sent in**: `x-codex-window-id` header
```rust ### 5.3 Turn State (Sticky Routing)
let header_etag = resp
.headers
.get(ETAG)
.and_then(|value| value.to_str().ok())
.map(ToString::to_string);
```
**Location**: `codex-rs/codex-api/src/endpoint/models.rs:58-62` **Location**: `codex-rs/core/src/client.rs`
### Model Selection ```http
# Server sends in response header:
x-codex-turn-state: {opaque_token}
Models are filtered based on: # Client echoes back in subsequent requests within same turn:
1. `visibility`: Must be `"list"` to appear in UI x-codex-turn-state: {same_token}
2. `minimal_client_version`: Client version must meet minimum ```
3. `supported_in_api`: Must be `true` for API usage
**Purpose**: Ensures requests within a turn hit the same backend instance
--- ---
## Streaming Responses ## 6. Transport Mechanisms
### HTTP/SSE Transport ### 6.1 HTTP (SSE - Server-Sent Events)
1. **Connection Setup** **Location**: `codex-rs/core/src/client.rs:1132`
- POST request to `/v1/responses`
- `Accept: text/event-stream` header
- Optional `Content-Encoding: zstd` for compression
2. **Event Stream Processing** 1. POST to `/v1/responses`
- Parse SSE events line-by-line 2. Response: `Content-Type: text/event-stream`
- Handle `event:` and `data:` lines 3. **Events**:
- Reconstruct JSON from multi-line data - `response.created`
- Parse event-specific payloads - `response.output_item.added`
- `response.output_item.done`
- `response.completed`
- `response.failed`
3. **Idle Timeout** ### 6.2 WebSocket
- Default: 300 seconds (5 minutes)
- Configurable via `stream_idle_timeout_ms`
- Connection reset if no data received within timeout
4. **Retry Logic** **Location**: `codex-rs/core/src/client.rs:1229`
- Default max retries: 5 attempts
- Exponential backoff: 200ms base delay
- Retry on: 5xx errors, transport errors
- No retry on: 429 (rate limit), 401 (unauthorized)
**Location**: `codex-rs/codex-api/src/sse.rs` #### Connection
### WebSocket Transport - Upgrade HTTP to WebSocket
- **Path**: `/v1/responses`
- **Protocol**: `responses_websockets=2026-02-06`
1. **Connection Handshake** #### Request Format
- Upgrade HTTP connection to WebSocket
- URL: `wss://chatgpt.com/backend-api/codex/responses`
- Headers: Same as HTTP plus `OpenAI-Beta: responses_websockets=2026-02-06`
2. **Request Format** ```json
```json {
{
"type": "response.create", "type": "response.create",
"response": {
"model": "gpt-4", "model": "gpt-4",
"instructions": "...", "instructions": "...",
"input": [...], "input": [],
"client_metadata": { "previous_response_id": "resp_123",
"x-codex-window-id": "...", "generate": false,
"x-openai-subagent": "...", ...
"x-codex-parent-thread-id": "...", }
"x-codex-turn-metadata": "..." ```
}
}
}
```
3. **Incremental Requests** #### Incremental Requests
- Reuse WebSocket connection for multiple requests
- Send only delta items with `previous_response_id`
- Server maintains conversation state
4. **Connection Reuse** - Reuses WebSocket connection
- Connection cached per turn - Sends only delta of new input items
- Reused across multiple requests in same turn - References `previous_response_id`
- Reset on window generation change
5. **Fallback to HTTP** #### Fallback
- On `426 Upgrade Required` response
- On connection timeout (15 seconds default)
- On WebSocket errors
**Location**: `codex-rs/codex-api/src/websocket.rs` - Falls back to HTTP on connection failure
- Session-scoped: once activated, stays on HTTP
--- ---
## Message Conversion Between OpenAI and Codex Formats ## 7. System Prompt (Instructions)
### Overview ### 7.1 Base Instructions
Codex uses the OpenAI Responses API format which is more structured than the traditional Chat Completions API. Understanding how to convert between standard OpenAI message formats and Codex's internal format is essential for implementing compatible clients. **Location**: `codex-rs/protocol/src/models.rs`
```rust
pub struct BaseInstructions {
pub text: String,
pub personality: Option<Personality>,
}
```
### Key Type Definitions ### 7.2 Instruction Sources (Priority Order)
#### Codex Internal Types (`ResponseInputItem` and `ResponseItem`) 1. **User Override**: `config.base_instructions`
2. **Model Default**: `model_info.base_instructions`
3. **Personality Template**: Applied if no override
Codex uses two main types for messages: ### 7.3 Instruction Composition
1. **`ResponseInputItem`** - Messages sent TO the API **Location**: `codex-rs/core/src/session/session.rs`
2. **`ResponseItem`** - Messages received FROM the API
```rust ```rust
// From codex-rs/protocol/src/models.rs // Final instructions sent to API:
instructions = base_instructions + tool_instructions + context_tags
```
// Input items (sent to API) Context tags include:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseInputItem {
Message {
role: String,
content: Vec<ContentItem>,
},
FunctionCallOutput {
call_id: String,
output: FunctionCallOutputPayload,
},
McpToolCallOutput {
call_id: String,
output: CallToolResult,
},
CustomToolCallOutput {
call_id: String,
name: Option<String>,
output: FunctionCallOutputPayload,
},
ToolSearchOutput {
call_id: String,
status: String,
execution: String,
tools: Vec<serde_json::Value>,
},
}
// Output items (received from API) - Sandbox mode information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] - Collaboration mode
#[serde(tag = "type", rename_all = "snake_case")] - Realtime conversation state
pub enum ResponseItem { - Approval settings
Message { id: Option<String>, role: String, content: Vec<ContentItem>, ... },
Reasoning { id: String, summary: Vec<ReasoningItemReasoningSummary>, ... },
FunctionCall { id: Option<String>, name: String, arguments: String, call_id: String, ... },
CustomToolCall { id: Option<String>, status: Option<String>, call_id: String, name: String, ... },
// ... and more
}
// Content items within messages ---
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentItem {
InputText { text: String },
InputImage { image_url: String },
OutputText { text: String },
}
```
**Location**: `codex-rs/protocol/src/models.rs:119-159` ## 8. Message Format Conversion (OpenAI Compatible → ChatGPT)
### Converting User Input to Codex Format ### 8.1 Key Differences
#### Simple Text Messages 1. **No Direct OpenAI Format**:
- Codex doesn't use OpenAI's `messages` array format
- Uses custom `ResponseItem` enum instead
**Standard OpenAI format:** 2. **Input Array**:
```json - **OpenAI**: `messages: [{role, content}]`
{ - **Codex**: `input: [ResponseItem]`
"messages": [
{"role": "user", "content": "Hello, how are you?"}
]
}
```
**Codex format:** 3. **Instructions vs System Message**:
```json - **OpenAI**: System message in `messages` array
{ - **Codex**: Separate `instructions` field
"input": [
{
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "Hello, how are you?"}
]
}
]
}
```
**Conversion logic:** 4. **Tool Calls**:
```rust - **OpenAI**: Embedded in message content
// From codex-rs/protocol/src/models.rs:1015-1053 - **Codex**: Separate `ResponseItem` types (`FunctionCall`, `FunctionCallOutput`)
impl From<Vec<UserInput>> for ResponseInputItem {
fn from(items: Vec<UserInput>) -> Self {
Self::Message {
role: "user".to_string(),
content: items
.into_iter()
.flat_map(|c| match c {
UserInput::Text { text, .. } => {
vec![ContentItem::InputText { text }]
}
// ... handle images, local images, etc.
})
.collect(),
}
}
}
```
#### Messages with Images ### 8.2 Conversion Example
**OpenAI Format**:
**Standard OpenAI format:**
```json ```json
{ {
"model": "gpt-4",
"messages": [ "messages": [
{ { "role": "system", "content": "You are helpful" },
"role": "user", { "role": "user", "content": "Hello" },
"content": [ { "role": "assistant", "content": "Hi there!" }
{"type": "text", "text": "What's in this image?"},
{
"type": "image_url",
"image_url": {"url": "data:image/png;base64,..."}
}
]
}
] ]
} }
``` ```
**Codex format (with image tags):** **Codex Format**:
```json ```json
{ {
"model": "gpt-4",
"instructions": "You are helpful",
"input": [ "input": [
{ {
"type": "message", "type": "message",
"role": "user", "role": "user",
"content": [ "content": [{ "type": "input_text", "text": "Hello" }]
{"type": "input_text", "text": "<image>"}, },
{"type": "input_image", "image_url": "data:image/png;base64,..."}, {
{"type": "input_text", "text": "</image>What's in this image?"} "type": "message",
] "role": "assistant",
"content": [{ "type": "output_text", "text": "Hi there!" }]
} }
] ]
} }
``` ```
**Image tagging rules:** ---
- Remote images: wrapped with `<image>` and `</image>` tags
- Local images: wrapped with `<image name=[Image #N]>` and `</image>` tags
- Multiple images share a sequential label counter
**Location**: `codex-rs/protocol/src/models.rs:867-906` ## 9. Complete Request Flow
### Converting Tool Calls/Function Calls ### Step-by-Step Process
#### Tool Call Request (Model → API) #### Step 1: Authentication
**Codex receives from model:** ```python
```rust # Load tokens from storage
// From codex-rs/protocol/src/models.rs:227-240 tokens = load_from_auth_json()
ResponseItem::FunctionCall { access_token = tokens['access_token']
name: "shell", account_id = tokens['id_token']['chatgpt_account_id']
arguments: "{\"command\": [\"ls\"]}",
call_id: "call_123",
namespace: Some("mcp"),
}
``` ```
**Wire format (Responses API):** #### Step 2: Build Headers
```json
{ ```python
"type": "function_call", headers = {
"name": "shell", 'Authorization': f'Bearer {access_token}',
"arguments": "{\"command\": [\"ls\"]}", 'ChatGPT-Account-ID': account_id,
"call_id": "call_123" 'User-Agent': 'codex_cli_rs/0.1.0 (Linux 5.15.0; x86_64) iTerm2',
'originator': 'codex_cli_rs',
'Content-Type': 'application/json',
'session_id': conversation_id,
'x-codex-installation-id': installation_id,
'x-codex-window-id': f'{conversation_id}:0',
'OpenAI-Beta': 'responses_websockets=2026-02-06',
} }
``` ```
#### Tool Result Response (API → Model) #### Step 3: Build Request Body
**Codex sends back to API:** ```python
```rust body = {
ResponseInputItem::FunctionCallOutput { 'model': 'gpt-4',
call_id: "call_123".to_string(), 'instructions': base_instructions,
output: FunctionCallOutputPayload { 'input': conversation_history, # List of ResponseItem
body: FunctionCallOutputBody::Text("total 0".to_string()), 'tools': tool_definitions,
success: Some(true), 'tool_choice': 'auto',
}, 'parallel_tool_calls': True,
'stream': True,
'store': False,
'prompt_cache_key': conversation_id,
} }
``` ```
**Wire format:** #### Step 4: Send Request
```json
{ ```python
"type": "function_call_output", # HTTP/SSE
"call_id": "call_123", response = requests.post(
"output": "total 0" 'https://chatgpt.com/backend-api/v1/responses',
} headers=headers,
json=body,
stream=True
)
# Or WebSocket
ws = websocket.create_connection(
'wss://chatgpt.com/backend-api/v1/responses',
header=headers
)
ws.send(json.dumps({'type': 'response.create', **body}))
``` ```
Or for multimodal tool outputs: #### Step 5: Handle Response
```json
{ ```python
"type": "function_call_output", # SSE
"call_id": "call_123", for line in response.iter_lines():
"output": [ if line.startswith(b'data: '):
{"type": "input_text", "text": "File listing:"}, event = json.loads(line[6:])
{"type": "input_image", "image_url": "data:image/png;base64,..."} handle_event(event)
]
} # WebSocket
while True:
message = json.loads(ws.recv())
handle_event(message)
``` ```
**Location**: `codex-rs/protocol/src/models.rs:1180-1288` #### Step 6: Handle 401 (Token Refresh)
### Tool Definition Format ```python
if response.status_code == 401:
# Refresh token
new_tokens = refresh_access_token(refresh_token)
# Retry request with new token
```
**Codex tool format (JSON Schema):** #### Step 7: Update Turn State
```rust
// Tools are passed to the Responses API in OpenAI function format ```python
{ # Extract from response headers
"type": "function", turn_state = response.headers.get('x-codex-turn-state')
"name": "shell", # Include in next request within same turn
"description": "Execute a shell command", headers['x-codex-turn-state'] = turn_state
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "array",
"items": {"type": "string"},
"description": "Command as array of strings"
},
"workdir": {
"type": "string",
"description": "Working directory"
}
},
"required": ["command"]
}
}
``` ```
**Location**: `codex-rs/codex-tools/src/lib.rs` ---
### Streaming Message Handling ## 10. Python Implementation Example
```python
import requests
import json
import uuid
from typing import List, Dict, Any
class CodexChatGPTClient:
def __init__(self, access_token: str, account_id: str):
self.base_url = "https://chatgpt.com/backend-api"
self.access_token = access_token
self.account_id = account_id
self.conversation_id = str(uuid.uuid4())
self.installation_id = str(uuid.uuid4())
self.window_generation = 0
self.turn_state = None
def _build_headers(self) -> Dict[str, str]:
headers = {
'Authorization': f'Bearer {self.access_token}',
'ChatGPT-Account-ID': self.account_id,
'User-Agent': 'custom_client/1.0.0 (Linux; x86_64)',
'originator': 'custom_client',
'Content-Type': 'application/json',
'session_id': self.conversation_id,
'x-client-request-id': self.conversation_id,
'x-codex-installation-id': self.installation_id,
'x-codex-window-id': f'{self.conversation_id}:{self.window_generation}',
}
#### SSE Events to ResponseItem Conversion if self.turn_state:
headers['x-codex-turn-state'] = self.turn_state
The Responses API streams events that must be assembled into `ResponseItem` objects: return headers
```rust def send_message(self,
// From codex-rs/codex-api/src/sse.rs user_message: str,
conversation_history: List[Dict],
// Event sequence: instructions: str = "You are a helpful assistant.") -> None:
// 1. response.created - Start of response
// 2. response.output_item.added - New item created
// 3. response.content_part.added - Content started
// 4. response.content_part.delta - Incremental content
// 5. response.output_item.done - Item complete
// 6. response.done - All done
// Each event type maps to internal types:
// - response.output_item.added (type=message) -> ResponseItem::Message
// - response.output_item.added (type=function_call) -> ResponseItem::FunctionCall
// - response.output_item.added (type=reasoning) -> ResponseItem::Reasoning
```
**Location**: `codex-rs/codex-api/src/sse.rs` # Build input array
input_items = []
for item in conversation_history:
input_items.append({
'type': 'message',
'role': item['role'],
'content': [{'type': 'input_text', 'text': item['content']}]
})
#### Converting Streaming Deltas to Codex Format # Add current message
input_items.append({
'type': 'message',
'role': 'user',
'content': [{'type': 'input_text', 'text': user_message}]
})
```rust # Build request body
// Incremental text delta body = {
{ 'model': 'gpt-4',
"event": "response.content_part.delta", 'instructions': instructions,
"data": { 'input': input_items,
"delta": {"text": "Hello"} 'tools': [],
'tool_choice': 'auto',
'parallel_tool_calls': True,
'stream': True,
'store': False,
'prompt_cache_key': self.conversation_id,
} }
}
// Becomes: # Send request
ResponseItem::Message { response = requests.post(
content: vec![ContentItem::OutputText { text: "Hello" }], f'{self.base_url}/v1/responses',
// ... headers=self._build_headers(),
} json=body,
stream=True
)
# Update turn state from response
if 'x-codex-turn-state' in response.headers:
self.turn_state = response.headers['x-codex-turn-state']
# Process SSE stream
for line in response.iter_lines():
if line.startswith(b'data: '):
try:
event = json.loads(line[6:])
self._handle_event(event)
except json.JSONDecodeError:
continue
def _handle_event(self, event: Dict[str, Any]):
event_type = event.get('type')
if event_type == 'response.output_item.done':
item = event.get('item', {})
if item.get('type') == 'message':
content = item.get('content', [])
for c in content:
if c.get('type') == 'output_text':
print(c.get('text', ''), end='', flush=True)
elif event_type == 'response.completed':
print() # New line after completion
# Usage example
if __name__ == "__main__":
# Assuming you have valid tokens from OAuth2 flow
client = CodexChatGPTClient(
access_token="your_access_token_here",
account_id="your_account_id_here"
)
# Send a message
client.send_message(
user_message="Hello, how are you?",
conversation_history=[],
instructions="You are a helpful coding assistant."
)
``` ```
### Complete Conversion Flow ---
#### Non-Streaming Request Flow
1. **User Input → Codex Input:**
```rust
// User input (text, images, etc.)
let user_input = vec![UserInput::Text { text: "Hello".to_string() }];
// Convert to ResponseInputItem
let input_item: ResponseInputItem = user_input.into();
// Results in: ResponseInputItem::Message { role: "user", content: [...] }
```
2. **Build Request:**
```rust
// From codex-rs/core/src/client.rs:749-815
let request = ResponsesApiRequest {
model: "gpt-4".to_string(),
instructions: base_instructions.text,
input: vec![input_item], // ResponseInputItem array
tools: create_tools_json(tools)?,
// ... other fields
};
```
3. **Response → ResponseItem:**
```rust
// Parse JSON response into ResponseItem
let response_item: ResponseItem = serde_json::from_value(response_json)?;
// Handle Message, FunctionCall, Reasoning, etc.
```
#### Streaming Request Flow
1. **Send Request** (same as non-streaming)
2. **Process SSE Events:**
```rust
// From codex-rs/core/src/client.rs:1496-1576
while let Some(event) = stream.next().await {
match event {
Ok(ResponseEvent::OutputItemDone(item)) => {
// Item is complete, convert to ResponseItem
items_added.push(item);
}
Ok(ResponseEvent::ContentPartDelta { delta }) => {
// Accumulate incremental text
}
Ok(ResponseEvent::Completed { response_id, usage }) => {
// Final response with usage stats
}
}
}
```
3. **Reconstruct Full Message:** ## Summary
```rust
// Multiple deltas are assembled into complete ResponseItem
// e.g., multiple response.content_part.delta events -> complete message
```
### WebSocket Message Format This documentation provides a complete reference for implementing a standalone client that mimics Codex's request behavior to ChatGPT's backend API. Key takeaways:
For WebSocket transport, messages use a different structure: 1. **Authentication**: Uses OAuth2 device code flow with token refresh
2. **Headers**: Requires `Authorization`, `ChatGPT-Account-ID`, and various Codex-specific headers
3. **Format**: Uses custom `ResponseItem` format instead of OpenAI's `messages` array
4. **Transport**: Supports both HTTP/SSE and WebSocket with automatic fallback
5. **Session Management**: Uses conversation IDs, window generations, and turn state for routing
6. **Instructions**: Separate `instructions` field instead of system messages
```rust All code locations reference the Codex Rust codebase for verification and deeper exploration.
// From codex-rs/codex-api/src/websocket.rs
// Request (WebSocket) ---
## 11. Detailed Request Examples with Tool Usage
### 11.1 Simple Text Request (No Tools)
**Request:**
```json
{ {
"type": "response.create",
"response": {
"model": "gpt-4", "model": "gpt-4",
"instructions": "...", "instructions": "You are a helpful coding assistant.",
"input": [...], "input": [
"client_metadata": { {
"x-codex-window-id": "...", "type": "message",
"x-openai-subagent": "..." "role": "user",
"content": [
{
"type": "input_text",
"text": "What is Python?"
} }
]
}
],
"tools": [],
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": null,
"store": false,
"stream": true,
"include": [],
"service_tier": null,
"prompt_cache_key": "550e8400-e29b-41d4-a716-446655440000",
"text": null,
"client_metadata": {
"x-codex-installation-id": "123e4567-e89b-12d3-a456-426614174000"
} }
} }
```
// Incremental request (subsequent requests in same turn) ### 11.2 Request with Function Tools
**Request:**
```json
{ {
"type": "response.create", "model": "gpt-4",
"previous_response_id": "resp_123", "instructions": "You are a helpful coding assistant with access to shell commands.",
"input": [...], // Only new items, not full history "input": [
"response": { ... } {
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "List files in the current directory"
}
]
}
],
"tools": [
{
"type": "function",
"name": "shell",
"description": "Execute a shell command and return the output",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute"
},
"workdir": {
"type": "string",
"description": "Working directory for command execution"
}
},
"required": ["command"],
"additionalProperties": false
}
}
],
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": null,
"store": false,
"stream": true,
"include": [],
"service_tier": null,
"prompt_cache_key": "550e8400-e29b-41d4-a716-446655440000",
"text": null,
"client_metadata": {
"x-codex-installation-id": "123e4567-e89b-12d3-a456-426614174000"
}
} }
``` ```
**Location**: `codex-rs/codex-api/src/websocket.rs` ### 11.3 Request with Tool Call and Output in History
### Summary: Key Conversion Points
| Aspect | OpenAI Standard | Codex Internal |
|--------|----------------|----------------|
| Messages field | `messages` | `input` |
| Message structure | `{role, content}` | `{type: message, role, content[]}` |
| Content structure | `{type, text}` or `{type, image_url}` | `{type: input_text/input_image/output_text}` |
| Tool calls | `tool_calls` array | `ResponseItem::FunctionCall` |
| Tool results | `function_call_output` (string) | `FunctionCallOutputPayload` (string or array) |
| Images | Simple array | Wrapped with `<image>` tags |
| Streaming | SSE with deltas | Assembled into complete `ResponseItem` |
### Important Implementation Notes **Request:**
```json
1. **Content Array**: Messages always have a `content` array, even for single text {
2. **Type Tags**: All items use snake_case type tags (`message`, `function_call`, `input_text`) "model": "gpt-4",
3. **Image Handling**: Images require special tagging with `<image>` and `</image>` "instructions": "You are a helpful coding assistant.",
4. **Tool Output**: Can be plain text OR array of content items for multimodal "input": [
5. **Namespace**: MCP tools include `namespace` field; built-in tools don't {
"type": "message",
--- "role": "user",
"content": [
{
"type": "input_text",
"text": "List files in the current directory"
}
]
},
{
"type": "message",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "I'll list the files for you."
}
]
},
{
"type": "function_call",
"call_id": "call_abc123",
"name": "shell",
"arguments": "{\"command\":\"ls -la\"}"
},
{
"type": "function_call_output",
"call_id": "call_abc123",
"output": {
"type": "text",
"text": "total 48\ndrwxr-xr-x 12 user staff 384 Apr 19 10:30 .\ndrwxr-xr-x 6 user staff 192 Apr 18 15:20 ..\n-rw-r--r-- 1 user staff 1234 Apr 19 10:25 README.md\n-rw-r--r-- 1 user staff 567 Apr 19 10:30 main.py"
}
},
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What's in main.py?"
}
]
}
],
"tools": [
{
"type": "function",
"name": "shell",
"description": "Execute a shell command",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string"
}
},
"required": ["command"]
}
}
],
"tool_choice": "auto",
"parallel_tool_calls": true,
"store": false,
"stream": true,
"prompt_cache_key": "550e8400-e29b-41d4-a716-446655440000"
}
```
## WebSocket Support ### 11.4 Request with Built-in Tools
### Prewarm/Preconnect **Request with local_shell, web_search, and image_generation:**
```json
{
"model": "gpt-4",
"instructions": "You are a helpful assistant with access to shell, web search, and image generation.",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Search for Python tutorials and create a diagram"
}
]
}
],
"tools": [
{
"type": "local_shell"
},
{
"type": "web_search",
"external_web_access": true,
"search_context_size": "medium",
"search_content_types": ["text", "image"]
},
{
"type": "image_generation",
"output_format": "url"
}
],
"tool_choice": "auto",
"parallel_tool_calls": true,
"store": false,
"stream": true,
"prompt_cache_key": "550e8400-e29b-41d4-a716-446655440000"
}
```
Before sending the first request, the client can establish a WebSocket connection: ### 11.5 Request with Reasoning Controls
```rust **Request:**
pub async fn preconnect_websocket( ```json
&mut self, {
session_telemetry: &SessionTelemetry, "model": "gpt-4",
_model_info: &ModelInfo, "instructions": "You are a helpful coding assistant.",
) -> std::result::Result<(), ApiError> "input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Explain how quicksort works"
}
]
}
],
"tools": [],
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {
"effort": "high",
"summary": "auto"
},
"store": false,
"stream": true,
"include": ["reasoning.encrypted_content"],
"service_tier": null,
"prompt_cache_key": "550e8400-e29b-41d4-a716-446655440000"
}
``` ```
This reduces latency for the first actual request. **Reasoning effort options:**
- `"low"` - Minimal reasoning
- `"medium"` - Balanced reasoning (default)
- `"high"` - Extended reasoning
### Warmup Request **Reasoning summary options:**
- `"auto"` - Automatic summary generation
- `"none"` - No summary
- `"concise"` - Brief summary
- `"detailed"` - Detailed summary
A special request with `generate: false` to establish connection without generating output: ### 11.6 Request with Verbosity Control
**Request:**
```json ```json
{ {
"type": "response.create",
"response": {
"model": "gpt-4", "model": "gpt-4",
"generate": false, "instructions": "You are a helpful coding assistant.",
... "input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Explain Python decorators"
}
]
} }
],
"tools": [],
"tool_choice": "auto",
"parallel_tool_calls": true,
"store": false,
"stream": true,
"text": {
"verbosity": "high"
},
"prompt_cache_key": "550e8400-e29b-41d4-a716-446655440000"
} }
``` ```
The client waits for completion before sending the actual request. **Verbosity options:**
- `"low"` - Concise responses
- `"medium"` - Balanced responses (default)
- `"high"` - Detailed responses
**Location**: `codex-rs/core/src/client.rs:1303-1351` ### 11.7 Request with JSON Schema Output
--- **Request:**
```json
## Implementation Examples {
"model": "gpt-4",
### Example 1: Basic Request (HTTP/SSE) "instructions": "You are a helpful assistant that returns structured data.",
"input": [
```rust {
use codex_api::{ResponsesClient, ResponsesApiRequest, ResponsesOptions}; "type": "message",
use codex_api::requests::responses::Compression; "role": "user",
"content": [
// Setup {
let transport = ReqwestTransport::new(build_reqwest_client()); "type": "input_text",
let provider = Provider { "text": "List 3 programming languages with their use cases"
name: "OpenAI".to_string(), }
base_url: "https://api.openai.com/v1".to_string(), ]
query_params: None, }
headers: HeaderMap::new(), ],
retry: RetryConfig { /* ... */ }, "tools": [],
stream_idle_timeout: Duration::from_secs(300), "tool_choice": "auto",
}; "parallel_tool_calls": true,
let auth = /* implement AuthProvider */; "store": false,
"stream": true,
let client = ResponsesClient::new(transport, provider, auth); "text": {
"format": {
// Build request "type": "json_schema",
let request = ResponsesApiRequest { "strict": true,
model: "gpt-4".to_string(), "name": "programming_languages",
instructions: "You are a helpful assistant".to_string(), "schema": {
input: vec![/* conversation items */], "type": "object",
tools: vec![], "properties": {
tool_choice: "auto".to_string(), "languages": {
parallel_tool_calls: true, "type": "array",
reasoning: None, "items": {
store: false, "type": "object",
stream: true, "properties": {
include: vec![], "name": {
service_tier: None, "type": "string"
prompt_cache_key: Some("conversation_123".to_string()), },
text: None, "use_case": {
}; "type": "string"
let options = ResponsesOptions {
conversation_id: Some("conversation_123".to_string()),
session_source: None,
extra_headers: HeaderMap::new(),
compression: Compression::None,
turn_state: None,
};
// Stream response
let mut stream = client.stream_request(request, options).await?;
while let Some(event) = stream.next().await {
match event {
Ok(ResponseEvent::ContentPartDelta { delta }) => {
print!("{}", delta.text);
} }
Ok(ResponseEvent::Completed { response_id, token_usage }) => { },
println!("\nCompleted: {}", response_id); "required": ["name", "use_case"],
"additionalProperties": false
} }
Err(e) => {
eprintln!("Error: {}", e);
break;
} }
_ => {} },
"required": ["languages"],
"additionalProperties": false
}
} }
},
"prompt_cache_key": "550e8400-e29b-41d4-a716-446655440000"
} }
``` ```
### Example 2: Retrieve Models ### 11.8 Request with Image Input
```rust
use codex_api::{ModelsClient, Provider};
let transport = ReqwestTransport::new(build_reqwest_client());
let provider = Provider { /* ... */ };
let auth = /* implement AuthProvider */;
let client = ModelsClient::new(transport, provider, auth); **Request:**
```json
let (models, etag) = client {
.list_models("0.99.0", HeaderMap::new()) "model": "gpt-4",
.await?; "instructions": "You are a helpful assistant that can analyze images.",
"input": [
for model in models { {
println!("{}: {}", model.slug, model.display_name); "type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "What's in this image?"
},
{
"type": "input_image",
"source": {
"type": "url",
"url": "https://example.com/image.jpg"
},
"detail": "high"
}
]
}
],
"tools": [],
"tool_choice": "auto",
"parallel_tool_calls": true,
"store": false,
"stream": true,
"prompt_cache_key": "550e8400-e29b-41d4-a716-446655440000"
} }
``` ```
### Example 3: WebSocket Request **Image detail options:**
- `"low"` - Low resolution analysis
```rust - `"high"` - High resolution analysis
use codex_api::{ResponsesWebsocketClient, ResponseCreateWsRequest}; - `"auto"` - Automatic selection
let provider = Provider { /* ... */ };
let auth = /* implement AuthProvider */;
let ws_client = ResponsesWebsocketClient::new(provider, auth); ---
// Connect ## 12. Key Differences from Standard OpenAI API
let mut connection = ws_client
.connect(headers, default_headers, turn_state, telemetry)
.await?;
// Send request ### 12.1 Missing Fields in Codex/ChatGPT Format
let request = ResponsesWsRequest::ResponseCreate(ResponseCreateWsRequest {
model: "gpt-4".to_string(),
instructions: "You are a helpful assistant".to_string(),
input: vec![/* items */],
client_metadata: HashMap::new(),
/* ... */
});
let mut stream = connection.stream_request(request, false).await?; The following OpenAI API fields are **NOT supported** in Codex's ChatGPT backend format:
// Process events #### Not Supported:
while let Some(event) = stream.next().await { - `temperature` - Not configurable per request
// Handle events - `top_p` - Not configurable per request
} - `max_tokens` - Not configurable per request (handled internally)
``` - `max_completion_tokens` - Not configurable per request
- `presence_penalty` - Not supported
- `frequency_penalty` - Not supported
- `logit_bias` - Not supported
- `logprobs` - Not supported
- `top_logprobs` - Not supported
- `n` - Always 1 (single response)
- `stop` - Not configurable
- `seed` - Not supported
- `user` - Not used (account ID in header instead)
### Example 4: Authentication Headers ### 12.2 Codex-Specific Fields
```rust Fields that exist in Codex but not in standard OpenAI API:
// API Key Mode
struct ApiKeyAuth {
api_key: String,
}
impl AuthProvider for ApiKeyAuth { #### Codex-Only Fields:
fn bearer_token(&self) -> Option<String> { - `instructions` - Replaces system message
Some(self.api_key.clone()) - `input` - Replaces `messages` array
} - `prompt_cache_key` - For prompt caching
} - `client_metadata` - For telemetry and tracking
- `include` - For including reasoning content
- `text.verbosity` - Response verbosity control
- `reasoning.effort` - Reasoning effort level
- `reasoning.summary` - Reasoning summary type
// ChatGPT Mode ### 12.3 Tool Definition Differences
struct ChatGptAuth {
access_token: String,
account_id: String,
}
impl AuthProvider for ChatGptAuth { **OpenAI Standard:**
fn bearer_token(&self) -> Option<String> { ```json
Some(self.access_token.clone()) {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather information",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
} }
fn account_id(&self) -> Option<String> {
Some(self.account_id.clone())
} }
} }
``` ```
### Example 5: User-Agent Construction **Codex/ChatGPT Format:**
```json
```rust {
pub fn get_codex_user_agent() -> String { "type": "function",
let build_version = env!("CARGO_PKG_VERSION"); "name": "get_weather",
let os_info = os_info::get(); "description": "Get weather information",
let originator = originator(); "parameters": {
let prefix = format!( "type": "object",
"{}/{build_version} ({} {}; {}) {}", "properties": {
originator.value.as_str(), "location": {"type": "string"}
os_info.os_type(), },
os_info.version(), "required": ["location"],
os_info.architecture().unwrap_or("unknown"), "additionalProperties": false
user_agent() // terminal detection }
);
let suffix = USER_AGENT_SUFFIX
.lock()
.ok()
.and_then(|guard| guard.clone());
let suffix = suffix
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map_or_else(String::new, |value| format!(" ({value})"));
format!("{prefix}{suffix}")
} }
``` ```
**Result**: `codex_cli_rs/0.99.0 (Linux 6.12; x86_64) xterm-256color (vscode; 1.86.0)` **Key differences:**
- No nested `function` object
- `additionalProperties: false` is typically included
- Tool definition is flatter
--- ### 12.4 Built-in Tool Types
## Key Implementation Details
### 1. Base URL Selection Codex supports special built-in tool types not in standard OpenAI:
```rust ```json
pub fn to_api_provider(&self, auth_mode: Option<AuthMode>) -> CodexResult<ApiProvider> { {
let default_base_url = if matches!(auth_mode, Some(AuthMode::Chatgpt)) { "type": "local_shell"
"https://chatgpt.com/backend-api/codex"
} else {
"https://api.openai.com/v1"
};
let base_url = self
.base_url
.clone()
.unwrap_or_else(|| default_base_url.to_string());
// ...
} }
``` ```
**Location**: `codex-rs/model-provider-info/src/lib.rs:184-193` ```json
{
### 2. Request Compression "type": "web_search",
"external_web_access": true,
```rust "search_context_size": "medium"
fn responses_request_compression(&self, auth: Option<&CodexAuth>) -> Compression {
if self.client.state.enable_request_compression
&& auth.is_some_and(CodexAuth::is_chatgpt_auth)
&& self.client.state.provider.is_openai()
{
Compression::Zstd
} else {
Compression::None
}
} }
``` ```
**Location**: `codex-rs/core/src/client.rs:1040-1049` ```json
{
### 3. Retry Logic "type": "image_generation",
"output_format": "url"
```rust
pub struct RetryConfig {
pub max_attempts: u64,
pub base_delay: Duration,
pub retry_429: bool,
pub retry_5xx: bool,
pub retry_transport: bool,
} }
``` ```
Default values: ```json
- `max_attempts`: 4 (requests), 5 (streams) {
- `base_delay`: 200ms "type": "tool_search",
- `retry_429`: false "execution": "client",
- `retry_5xx`: true "description": "Search for available tools",
- `retry_transport`: true "parameters": {...}
}
**Location**: `codex-rs/codex-api/src/provider.rs:16-22`
### 4. Sticky Routing (Turn State)
```rust
/// Turn state for sticky routing.
///
/// This is an `OnceLock` that stores the turn state value received from the server
/// on turn start via the `x-codex-turn-state` response header. Once set, this value
/// should be sent back to the server in the `x-codex-turn-state` request header for
/// all subsequent requests within the same turn to maintain sticky routing.
turn_state: Arc<OnceLock<String>>,
``` ```
**Location**: `codex-rs/core/src/client.rs:209-219` ```json
{
### 5. Session vs Turn Scope "type": "namespace",
"name": "mcp_server_name",
- **Session-scoped**: `ModelClient` - lives for entire conversation "description": "Tools from MCP server",
- **Turn-scoped**: `ModelClientSession` - created per turn, manages WebSocket connection "tools": [...]
```rust
pub fn new_session(&self) -> ModelClientSession {
ModelClientSession {
client: self.clone(),
websocket_session: self.take_cached_websocket_session(),
turn_state: Arc::new(OnceLock::new()),
}
} }
``` ```
**Location**: `codex-rs/core/src/client.rs:300-306` ### 12.5 Response Format Differences
--- **OpenAI Standard Response:**
```json
{
"id": "chatcmpl-123",
"object": "chat.completion.chunk",
"created": 1677652288,
"model": "gpt-4",
"choices": [{
"index": 0,
"delta": {
"content": "Hello"
},
"finish_reason": null
}]
}
```
## Session Flow **Codex/ChatGPT SSE Events:**
```
data: {"type":"response.created","response_id":"resp_123"}
### Overview of a Complete Session data: {"type":"response.output_item.added","item":{"type":"message","role":"assistant","content":[]}}
A session represents an entire conversation from start to finish. Understanding the flow of requests and how conversation state is maintained is essential for implementing a compatible client. data: {"type":"response.output_text.delta","delta":"Hello"}
### Lifecycle of a Session data: {"type":"response.output_item.done","item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Hello"}]}}
``` data: {"type":"response.completed","response_id":"resp_123","token_usage":{"input_tokens":10,"output_tokens":5}}
┌─────────────────────────────────────────────────────────────────────────────┐
│ SESSION LIFETIME │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Initialization │
│ ├── Load/validate authentication credentials │
│ ├── Fetch available models from /v1/models │
│ └── Create ModelClient (session-scoped) │
│ │
│ 2. Turn 1: User Input → First Response │
│ ├── Create ModelClientSession (turn-scoped) │
│ ├── Preconnect WebSocket (optional, recommended) │
│ ├── Build request with base instructions │
│ ├── Send user message in input array │
│ ├── Receive streaming response (SSE events) │
│ └── Extract response items, tool calls │
│ │
│ 3. Tool Execution Loop (within Turn N) │
│ ├── API emits FunctionCall item │
│ ├── Client executes tool locally │
│ ├── Send FunctionCallOutput back in next request │
│ └── Continue receiving response (may loop) │
│ │
│ 4. Turn N: Subsequent Requests │
│ ├── Build request with previous_response_id │
│ ├── Include all tool call results since turn start │
│ ├── Optionally compact conversation history │
│ └── Receive updated conversation state │
│ │
│ 5. Session End │
│ ├── Final response received │
│ ├── Close WebSocket connection │
│ └── Session cleanup │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
``` ```
### Detailed Turn Flow ### 12.6 Service Tier
#### Turn 1: Initial Request Codex uses `service_tier` field for priority routing:
```rust ```json
// 1. Create session (turn-scoped) {
let session = client.new_session(); "service_tier": "default"
// 2. Optionally preconnect WebSocket for lower latency
session.preconnect_websocket(telemetry, &model_info).await?;
// 3. Build first request
let request = ResponsesApiRequest {
model: "gpt-4".to_string(),
instructions: base_instructions.text, // Required for ChatGPT mode
input: vec![
// Developer message (optional, for system context)
ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: permissions_instructions
}],
},
// User message
ResponseInputItem::Message {
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: user_input.clone()
}],
},
],
tools: create_tools_json(tools)?,
// ...other fields
};
// 4. Send request and process streaming response
let mut stream = session.stream_request(request, options).await?;
while let Some(event) = stream.next().await {
match event {
Ok(ResponseEvent::OutputItemAdded { item }) => {
// New item created - could be Message, FunctionCall, Reasoning
match item {
ResponseItem::FunctionCall { name, arguments, call_id, .. } => {
// Tool call to execute
tool_calls_to_execute.push((call_id, name, arguments));
}
ResponseItem::Message { content, .. } => {
// Text response
}
_ => {}
}
}
Ok(ResponseEvent::ContentPartDelta { delta }) => {
// Accumulate incremental text
}
Ok(ResponseEvent::Completed { response_id, usage }) => {
// Turn complete
}
Err(e) => { /* Handle error */ }
}
} }
``` ```
**Location**: `codex-rs/core/src/client.rs:1303-1576` **Options:**
- `"default"` - Standard priority
- `"priority"` - High priority (maps to "fast" internally)
- `null` - No specific tier
#### Subsequent Turns: Tool Execution Loop This is different from OpenAI's standard API which doesn't have this field.
```rust ---
// After receiving a FunctionCall, execute the tool and send results back
// 1. Execute tool (shell command, file operation, etc.)
let tool_result = execute_tool(&tool_name, tool_args).await?;
// 2. Send tool result back to API
let next_request = ResponsesApiRequest {
model: "gpt-4".to_string(),
instructions: base_instructions.text,
input: vec![
// Include original user message
user_message.clone(),
// Include the assistant's function call
ResponseItem::FunctionCall { ... }.into(),
// Send tool result
ResponseInputItem::FunctionCallOutput {
call_id: tool_call.call_id,
output: FunctionCallOutputPayload {
body: FunctionCallOutputBody::Text(tool_result),
success: Some(true),
},
},
],
// ...
};
```
**Location**: `codex-rs/protocol/src/models.rs:1180-1288` ## 13. Complete Conversion Guide: OpenAI → Codex Format
### Conversation State Management ### 13.1 Basic Conversion Function
#### Maintaining Conversation History ```python
def convert_openai_to_codex(openai_request: dict) -> dict:
"""
Convert OpenAI API format to Codex/ChatGPT format
"""
codex_request = {
"model": openai_request.get("model", "gpt-4"),
"instructions": "",
"input": [],
"tools": [],
"tool_choice": openai_request.get("tool_choice", "auto"),
"parallel_tool_calls": openai_request.get("parallel_tool_calls", True),
"store": False,
"stream": openai_request.get("stream", True),
"include": [],
"service_tier": None,
"prompt_cache_key": str(uuid.uuid4()),
}
Codex maintains conversation state across requests within a turn. This is handled in two ways: # Extract system message as instructions
messages = openai_request.get("messages", [])
for msg in messages:
if msg.get("role") == "system":
codex_request["instructions"] = msg.get("content", "")
break
1. **HTTP/SSE**: Full `input` array sent with each request # Convert messages to input items
2. **WebSocket**: Server maintains state, client sends `previous_response_id` for msg in messages:
if msg.get("role") == "system":
continue # Already handled
```rust role = msg.get("role")
// For HTTP: Include full conversation history content = msg.get("content", "")
let input_items: Vec<ResponseInputItem> = conversation
.items()
.iter()
.flat_map(|item| item.to_input_item())
.collect();
let request = ResponsesApiRequest {
// ...
input: input_items,
// ...
};
// For WebSocket: Only send new items + previous_response_id
let request = ResponseCreateWsRequest {
previous_response_id: last_response_id, // Links to previous
input: new_items_only, // Just the new messages
// ...
};
```
**Location**: `codex-rs/codex-api/src/websocket.rs:89-156` # Handle string content
if isinstance(content, str):
codex_request["input"].append({
"type": "message",
"role": role,
"content": [{
"type": "input_text" if role == "user" else "output_text",
"text": content
}]
})
# Handle array content (multimodal)
elif isinstance(content, list):
content_items = []
for item in content:
if item.get("type") == "text":
content_items.append({
"type": "input_text" if role == "user" else "output_text",
"text": item.get("text", "")
})
elif item.get("type") == "image_url":
content_items.append({
"type": "input_image",
"source": {
"type": "url",
"url": item.get("image_url", {}).get("url", "")
},
"detail": item.get("image_url", {}).get("detail", "auto")
})
#### Session vs Turn codex_request["input"].append({
"type": "message",
"role": role,
"content": content_items
})
Understanding the distinction between session and turn is critical: # Handle tool calls in assistant messages
if msg.get("tool_calls"):
for tool_call in msg["tool_calls"]:
codex_request["input"].append({
"type": "function_call",
"call_id": tool_call.get("id", ""),
"name": tool_call.get("function", {}).get("name", ""),
"arguments": tool_call.get("function", {}).get("arguments", "{}")
})
| Concept | Scope | Description | # Handle tool responses
|--------------|---------------------|----------------------------------------------------------| if msg.get("role") == "tool":
| **Session** | Entire conversation | From first user message to session end | codex_request["input"].append({
| **Turn** | Single exchange | One user message + all subsequent tool calls + response | "type": "function_call_output",
| **Window** | UI context | Visible context in the interface | "call_id": msg.get("tool_call_id", ""),
"output": {
"type": "text",
"text": msg.get("content", "")
}
})
```rust # Convert tools
// Session lives for the entire conversation if "tools" in openai_request:
pub struct ModelClient { for tool in openai_request["tools"]:
// Authentication and transport (permanent) if tool.get("type") == "function":
transport: T, func = tool.get("function", {})
provider: Provider, codex_request["tools"].append({
auth: Box<dyn CodexAuth>, "type": "function",
} "name": func.get("name", ""),
"description": func.get("description", ""),
"parameters": func.get("parameters", {})
})
// Turn is created fresh for each user message return codex_request
pub struct ModelClientSession {
client: ModelClient,
websocket_session: Option<WebSocketSession>,
turn_state: Arc<OnceLock<String>>, // Turn-scoped sticky routing
}
``` ```
**Location**: `codex-rs/core/src/client.rs:183-306` ### 13.2 Usage Example
### Compact/Summarize Flow ```python
# OpenAI format request
openai_request = {
"model": "gpt-4",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
{"role": "assistant", "content": "Hi there!"},
{"role": "user", "content": "How are you?"}
],
"temperature": 0.7, # Will be ignored
"max_tokens": 100, # Will be ignored
"stream": True
}
When conversation history grows too large, Codex can compact it: # Convert to Codex format
codex_request = convert_openai_to_codex(openai_request)
```rust # Send to ChatGPT backend
// Compact request (HTTP) response = requests.post(
let compact_request = ResponsesApiRequest { "https://chatgpt.com/backend-api/v1/responses",
model: model.slug.clone(), headers=headers,
instructions: base_instructions.text, json=codex_request,
input: vec![/* conversation items to compact */], stream=True
tools: vec![], )
// ...
};
// Or via MCP subagent
let subagent_request = ResponsesApiRequestWithMetadata {
// ...
metadata: RequestMetadata {
x_openai_subagent: Some("compact"),
// ...
},
};
``` ```
**Location**: `codex-rs/core/src/client.rs:1610-1650` ---
### Error Handling and Recovery ## 14. Summary of Request Fields
#### Retry on Transient Errors ### 14.1 Required Fields
```rust - `model` - Model identifier (e.g., "gpt-4")
async fn with_retry<R, F, Fut>(&self, request: R, mut attempts: u64) -> Result<F::Output, ApiError> - `input` - Array of ResponseItem objects
where - `tools` - Array of tool definitions (can be empty)
R: Clone, - `tool_choice` - Tool selection mode ("auto", "none", or specific tool)
F: Fn(T, R) -> Fut, - `parallel_tool_calls` - Boolean for parallel execution
Fut: Future<Output = Result<F::Output, ApiError>>, - `store` - Boolean for storing conversation
{ - `stream` - Boolean for streaming responses
let base_delay = Duration::from_millis(200);
loop {
match self.send_request(request.clone()).await {
Ok(response) => return Ok(response),
Err(ApiError::ServerError(status)) if status.as_u16() >= 500 && attempts > 0 => {
// Exponential backoff
tokio::time::sleep(base_delay * 2_u64.pow(4 - attempts)).await;
attempts -= 1;
}
Err(e) => return Err(e),
}
}
}
```
#### Rate Limit Handling ### 14.2 Optional Fields
```rust - `instructions` - System prompt (empty string if not provided)
match error { - `reasoning` - Reasoning controls (effort and summary)
ApiError::RateLimited { retry_after } => { - `include` - Array of fields to include (e.g., reasoning content)
// Wait and retry (or notify user) - `service_tier` - Priority routing ("default", "priority", or null)
tokio::time::sleep(Duration::from_secs(retry_after)).await; - `prompt_cache_key` - Cache key for prompt caching
} - `text` - Text controls (verbosity and format)
_ => return Err(error), - `client_metadata` - Metadata for telemetry
}
```
--- ### 14.3 Field Value Constraints
## Summary **model:**
- Must be a valid model identifier
- Examples: "gpt-4", "gpt-4-turbo", "gpt-3.5-turbo"
**tool_choice:**
- `"auto"` - Model decides when to use tools
- `"none"` - Never use tools
- `{"type": "function", "name": "tool_name"}` - Force specific tool
**parallel_tool_calls:**
- `true` - Allow multiple tool calls in parallel
- `false` - Execute tools sequentially
To implement a compatible client that mimics codex-cli: **store:**
- `false` - Don't store (typical for Codex)
- `true` - Store conversation
1. **Use the Responses API** (`/v1/responses`), not Chat Completions **stream:**
2. **Implement proper authentication** with Bearer token and optional ChatGPT-Account-ID - `true` - Stream responses via SSE
3. **Set correct User-Agent** following the format: `{originator}/{version} ({os} {version}; {arch}) {terminal}` - `false` - Return complete response
4. **Include required headers**: `originator`, `x-client-request-id`, `session_id`
5. **Handle SSE streaming** with proper event parsing
6. **Implement retry logic** with exponential backoff
7. **Support WebSocket transport** for better performance
8. **Handle turn state** for sticky routing
9. **Compress requests** with zstd when using ChatGPT auth
10. **Parse model capabilities** from `/v1/models` endpoint
All code references are from the `codex-rs` directory in the repository. This completes the comprehensive documentation of the ChatGPT/Codex API request format with detailed examples and conversion guidance.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment