neosoftware / openai-codex-sdk
PHP 8.2 SDK for OpenAI Codex CLI agent
Requires
- php: ^8.2
README
PHP 8.2 SDK for working with the OpenAI Codex CLI agent.
An exact port of the @openai/codex-sdk package (TypeScript).
Requirements
- PHP 8.2+
- Composer
Installation
composer require neosoftware/openai-codex-sdk
Codex Binary
The SDK runs codex (Codex CLI) as a subprocess. Specify its path in one of the following ways:
Via CodexOptions — explicitly for a specific instance:
new CodexOptions(codexPathOverride: '/usr/local/bin/codex')
Installation into vendor/bin — convenient for containers and servers where Codex CLI is not installed globally.
The command downloads the binary for your platform from npm and adds a vendor/bin/codex symlink:
vendor/bin/install-codex
allow-plugins permissions are not required.
Quick Start
<?php require 'vendor/autoload.php'; use OpenAI\Codex\Codex; use OpenAI\Codex\Types\Options\CodexOptions; use OpenAI\Codex\Types\Options\ThreadOptions; $codex = new Codex(new CodexOptions( apiKey: $_ENV['OPENAI_API_KEY'], )); $thread = $codex->startThread(new ThreadOptions( model: 'codex-mini-latest', skipGitRepoCheck: true, )); $turn = $thread->run('Explain what the array_chunk function does in PHP.'); echo $turn->finalResponse;
Initialization
new Codex(CodexOptions $options = null)
Creates the main SDK object.
$codex = new Codex(new CodexOptions( apiKey: 'sk-...', // OpenAI API key (or via the OPENAI_API_KEY env variable) baseUrl: 'https://...', // custom API endpoint (optional) codexPathOverride: '/usr/local/bin/codex', // custom binary (optional) config: [ // additional --config flags 'disable_response_storage' => true, ], env: [ // if set, process env is NOT inherited 'OPENAI_API_KEY' => 'sk-...', 'HOME' => '/home/user', ], ));
CodexOptions
| Property | Type | Default | Description |
|---|---|---|---|
apiKey |
?string |
null |
OpenAI API key. Passed to the binary as CODEX_API_KEY |
baseUrl |
?string |
null |
Custom API URL (--config openai_base_url=...) |
codexPathOverride |
?string |
null |
Absolute path to your custom codex binary |
config |
?array |
null |
Arbitrary key => value pairs for --config flags |
env |
?array |
null |
Environment variables for the subprocess. If null, the subprocess inherits the entire PHP process environment. If explicitly set, the subprocess receives only the specified variables |
Threads
Codex works within threads — each thread stores the conversation history.
// New thread $thread = $codex->startThread(new ThreadOptions(...)); // Resume an existing thread by ID $thread = $codex->resumeThread('019dc15c-a8aa-...', new ThreadOptions(...)); // Get the ID after the first request echo $thread->getId(); // string|null
ThreadOptions
| Property | Type | Default | Description |
|---|---|---|---|
model |
?string |
null |
Model name (codex-mini-latest, gpt-4o, …) |
sandboxMode |
?SandboxMode |
null |
Sandbox mode for running commands |
workingDirectory |
?string |
null |
Agent working directory (--cd) |
skipGitRepoCheck |
bool |
false |
Do not require a Git repository |
modelReasoningEffort |
?ModelReasoningEffort |
null |
Depth of the model's “reasoning” |
networkAccessEnabled |
?bool |
null |
Network access from the sandbox |
webSearchMode |
?WebSearchMode |
null |
Web search mode |
webSearchEnabled |
?bool |
null |
Enable/disable web search (legacy) |
approvalPolicy |
?ApprovalMode |
null |
Action approval policy |
additionalDirectories |
?string[] |
null |
Additional directories (--add-dir) |
Running Requests
run() — blocking
Runs a request, waits for the full response, and returns a Turn.
$turn = $thread->run('Find a bug in src/Payment.php'); echo $turn->finalResponse; // final text response from the agent echo $turn->usage->outputTokens; // tokens consumed foreach ($turn->items as $item) { // all completed items // AgentMessageItem | CommandExecutionItem | FileChangeItem | ... }
Turn
| Property | Type | Description |
|---|---|---|
finalResponse |
string |
The agent's final message (or JSON when using outputSchema) |
items |
object[] |
List of all completed ThreadItem objects |
usage |
?Usage |
Token usage information |
runStreamed() — streaming
Returns StreamedTurn immediately. Events are read as they arrive through a generator.
$streamed = $thread->runStreamed('Check all tests and fix the errors'); foreach ($streamed->events() as $event) { match (true) { $event instanceof ThreadStartedEvent => print("Thread: {$event->threadId}\n"), $event instanceof TurnCompletedEvent => print("Tokens: {$event->usage?->outputTokens}\n"), $event instanceof ItemStartedEvent => print("Started: {$event->item->type}\n"), $event instanceof ItemCompletedEvent => match (true) { $event->item instanceof AgentMessageItem => print($event->item->text . "\n"), $event->item instanceof CommandExecutionItem => print("$ {$event->item->command}\n"), $event->item instanceof FileChangeItem => print("Files changed: " . count($event->item->changes) . "\n"), default => null, }, default => null, }; }
StreamedTurn::events() → Generator<ThreadEvent>
A generator that sequentially yields events:
| Event | Fields | Description |
|---|---|---|
ThreadStartedEvent |
threadId |
Thread created; ID is now available |
TurnStartedEvent |
— | Request processing started |
ItemStartedEvent |
item |
Item started running |
ItemUpdatedEvent |
item |
Intermediate item update (command output, etc.) |
ItemCompletedEvent |
item |
Item completed |
TurnCompletedEvent |
usage |
Request completed; token statistics |
TurnFailedEvent |
error |
The agent returned an error (throws TurnFailedException) |
ThreadErrorEvent |
message |
Non-fatal error |
Input Types
The run() and runStreamed() methods accept either a string or an array of objects:
// Simple string $thread->run('Explain this code'); // Text + images use OpenAI\Codex\Types\Input\TextInput; use OpenAI\Codex\Types\Input\LocalImageInput; $thread->run([ new TextInput('What is wrong with this screenshot?'), new LocalImageInput('/path/to/screenshot.png'), new TextInput('Pay attention to the upper-right corner.'), ]);
Multiple TextInput instances are concatenated with \n\n. Images are passed as --image flags.
ThreadItems
Each completed agent step is an item of one of the following types:
AgentMessageItem
Agent text response.
$item->id; // string $item->text; // string — agent message
ReasoningItem
Internal model “reasoning” (if enabled).
$item->id; // string $item->text; // string
CommandExecutionItem
Executed shell command.
$item->id; // string $item->command; // string — command, for example "/bin/zsh -c 'php artisan test'" $item->aggregatedOutput; // string — full command output $item->exitCode; // ?int — return code $item->status; // CommandExecutionStatus::InProgress|Completed|Failed
FileChangeItem
File changes in the working directory.
$item->id; // string $item->status; // PatchApplyStatus::Completed|Failed foreach ($item->changes as $change) { $change->path; // string — file path $change->kind; // PatchChangeKind::Add|Delete|Update $change->applyStatus; // PatchApplyStatus::Completed|Failed }
McpToolCallItem
Tool call via the Model Context Protocol.
$item->id; // string $item->server; // string — MCP server name $item->tool; // string — tool name $item->arguments; // mixed — arguments $item->result; // mixed — result (after completion) $item->error; // ?string — error (if any) $item->status; // McpToolCallStatus::InProgress|Completed|Failed
WebSearchItem
Web search request.
$item->id; // string $item->query; // string — search query
TodoListItem
Agent task list.
$item->id; // string foreach ($item->items as $todo) { $todo->id; // string $todo->description; // string $todo->completed; // bool }
ErrorItem
Non-fatal error during execution.
$item->id; // string $item->message; // string
TurnOptions
Options for a specific request (passed as the second argument to run() / runStreamed()):
use OpenAI\Codex\Types\Options\TurnOptions; $turn = $thread->run('Task', new TurnOptions( outputSchema: [...], // JSON Schema for structured output timeoutSeconds: 120, // response wait timeout in seconds ));
| Property | Type | Description |
|---|---|---|
outputSchema |
?array |
JSON Schema — the agent will return JSON compatible with the schema |
timeoutSeconds |
?int |
Maximum response wait time |
Structured Output
The OpenAI API requires "additionalProperties": false in the schema:
$turn = $thread->run( 'Return project data', new TurnOptions( outputSchema: [ 'type' => 'object', 'additionalProperties' => false, 'properties' => [ 'name' => ['type' => 'string'], 'version' => ['type' => 'string'], 'license' => ['type' => 'string'], ], 'required' => ['name', 'version'], ], ), ); $data = json_decode($turn->finalResponse, true); echo $data['name'];
Enumerations (Enums)
SandboxMode
use OpenAI\Codex\Types\Enums\SandboxMode; SandboxMode::ReadOnly // file system read-only SandboxMode::WorkspaceWrite // read + write in the working directory SandboxMode::DangerFullAccess // full access (no restrictions)
ModelReasoningEffort
use OpenAI\Codex\Types\Enums\ModelReasoningEffort; ModelReasoningEffort::Minimal ModelReasoningEffort::Low ModelReasoningEffort::Medium ModelReasoningEffort::High ModelReasoningEffort::Xhigh
WebSearchMode
use OpenAI\Codex\Types\Enums\WebSearchMode; WebSearchMode::Disabled // web search disabled WebSearchMode::Cached // cached results only WebSearchMode::Live // live search
ApprovalMode
use OpenAI\Codex\Types\Enums\ApprovalMode; ApprovalMode::Never // run everything without confirmation ApprovalMode::OnRequest // ask the user for permission ApprovalMode::OnFailure // ask only on failure ApprovalMode::Untrusted // strict mode
Error Handling
use OpenAI\Codex\Exceptions\CodexException; use OpenAI\Codex\Exceptions\SpawnException; use OpenAI\Codex\Exceptions\TurnFailedException; use OpenAI\Codex\Exceptions\ParseException; try { $turn = $thread->run('...'); } catch (TurnFailedException $e) { // The agent returned an error (API error, invalid schema, etc.) echo "Agent error: " . $e->getMessage(); } catch (SpawnException $e) { // Startup error or subprocess timeout echo "Process error: " . $e->getMessage(); } catch (ParseException $e) { // The agent returned invalid JSONL echo "Parse error: " . $e->getMessage(); } catch (CodexException $e) { // Any other SDK error echo "SDK error: " . $e->getMessage(); }
| Exception | When |
|---|---|
TurnFailedException |
The agent returned turn.failed (API error, invalid schema, …) |
SpawnException |
Binary not found, failed to start, hung, or exited with a non-zero code |
ParseException |
Binary returned invalid JSON in the JSONL stream |
CodexException |
Base class for all SDK exceptions |
Complete Example
<?php require 'vendor/autoload.php'; use OpenAI\Codex\Codex; use OpenAI\Codex\Types\Enums\ApprovalMode; use OpenAI\Codex\Types\Enums\ModelReasoningEffort; use OpenAI\Codex\Types\Enums\SandboxMode; use OpenAI\Codex\Types\Events\ItemCompletedEvent; use OpenAI\Codex\Types\Events\ThreadStartedEvent; use OpenAI\Codex\Types\Events\TurnCompletedEvent; use OpenAI\Codex\Types\Input\LocalImageInput; use OpenAI\Codex\Types\Input\TextInput; use OpenAI\Codex\Types\Items\AgentMessageItem; use OpenAI\Codex\Types\Items\CommandExecutionItem; use OpenAI\Codex\Types\Items\FileChangeItem; use OpenAI\Codex\Types\Options\CodexOptions; use OpenAI\Codex\Types\Options\ThreadOptions; use OpenAI\Codex\Types\Options\TurnOptions; $codex = new Codex(new CodexOptions( apiKey: $_ENV['OPENAI_API_KEY'], )); $thread = $codex->startThread(new ThreadOptions( model: 'codex-mini-latest', sandboxMode: SandboxMode::WorkspaceWrite, workingDirectory: '/path/to/project', modelReasoningEffort: ModelReasoningEffort::High, approvalPolicy: ApprovalMode::Never, )); // Streaming with an image $streamed = $thread->runStreamed( [ new TextInput('What is wrong with this code?'), new LocalImageInput('/tmp/screenshot.png'), ], new TurnOptions(timeoutSeconds: 180), ); foreach ($streamed->events() as $event) { match (true) { $event instanceof ThreadStartedEvent => printf("Thread: %s\n", $event->threadId), $event instanceof TurnCompletedEvent => printf("Tokens: %d\n", $event->usage?->outputTokens ?? 0), $event instanceof ItemCompletedEvent => match (true) { $event->item instanceof AgentMessageItem => printf("%s\n", $event->item->text), $event->item instanceof CommandExecutionItem => printf("$ %s\n", $event->item->command), $event->item instanceof FileChangeItem => printf("Files: %d\n", count($event->item->changes)), default => null, }, default => null, }; } // Continue the conversation in the same thread $turn = $thread->run('Now write a test for the fixed code.'); echo $turn->finalResponse; // Resume the thread in a new session $resumed = $codex->resumeThread($thread->getId()); $turn = $resumed->run('What did you do last time?'); echo $turn->finalResponse;
License
MIT