phore / ai-harness
Requires
- php: >=8.5
- ext-curl: *
- ext-json: *
- ext-yaml: *
- phore/filesystem: *
- phore/json-patch: dev-main
- phore/schema: dev-main
Requires (Dev)
- phpunit/phpunit: ^13.2.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-03 13:35:14 UTC
README
PHP helpers for the OpenAI Responses API, typed results, shared conversation
contexts and targeted text/file editing. Prefer the global phore_ai_*
functions for normal use; their existing signatures remain supported.
Quick start: functions first
For a one-shot request, start with the global helper. Common runtime options are shown once here, directly on the call:
echo phore_ai_text('Write a four-line poem about a rainy autumn morning.', [ 'client' => null, 'model' => 'gpt-5-mini', 'reasoning' => ['effort' => 'medium'], 'timeout' => 120, 'connect_timeout' => 10, 'debug_log' => true, ]);
Normally omit options you do not need. client => null resolves the configured
default client/credentials; the default reasoning setting is low effort.
Convenience functions
Use the helpers for isolated one- or two-shot work:
| Function | Purpose |
|---|---|
phore_ai_text() |
generate or edit text |
phore_ai_do() |
perform a work step without returning user-facing text |
phore_ai_choice() / phore_ai_choices() |
select one or several values |
phore_ai_yes_no() |
boolean decision, optionally null |
phore_ai_rank() / phore_ai_score() |
rank values or return a 0.0..1.0 score |
phore_ai_struct() / phore_ai_struct_array() |
hydrate one DTO or a DTO list |
phore_ai_edit_struct() |
patch an existing DTO |
phore_ai_image() |
generate an image |
phore_ai_edit_file() |
edit one or more explicit files |
get_last_ai_request() / get_last_ai_response() |
inspect the latest request/response |
get_ai_usage_stats() |
inspect process-wide usage and estimated cost |
A file can be attached directly to a one-shot call:
use Phore\AiHarness\PromptType\FilePrompt; $summary = phore_ai_text([ 'Summarize the important claims in three bullets.', FilePrompt::fromFile('/path/to/File.pdf'), ]);
When several requests belong together
The functional API can share a conversation by repeating exactly the same
ai_context name:
use Phore\AiHarness\PromptType\FilePrompt; use Phore\AiHarness\ToolType\WebAccessTool; phore_ai_do([ 'Verify the claims in this file against current web sources.', FilePrompt::fromFile('/path/to/File.pdf'), new WebAccessTool(), ], throw: true, options: ['ai_context' => 'article-review']); $correct = phore_ai_yes_no( 'Are the central claims in the previously checked article correct?', options: ['ai_context' => 'article-review'], ); $summary = phore_ai_text( 'Summarize the corrections that are needed.', ['ai_context' => 'article-review'], );
This is the same underlying context mechanism as the object API. The string name
is convenient, but a typo or renamed identifier silently selects another
context. For repeated, stateful work, prefer one AiContext object:
use Phore\AiHarness\AiContext; $context = new AiContext(prompts: [ FilePrompt::fromFile('/path/to/File.pdf'), new WebAccessTool(), ]); $context->do('Verify the claims in this file against current web sources.', throw: true); $context->setCheckpoint('verified'); $correct = $context->yesNo('Are the central claims in the checked article correct?'); $summary = $context->text('Summarize the corrections that are needed.');
From this point on, the detailed examples use AiContext; the helper functions
delegate to the same operations and do not need a parallel explanation.
State, checkpoints and sessions
setCheckpoint() and rollback() move only the conversation cursor. They do
not undo file writes, tool side effects or incurred cost. clone $context
creates an independent cursor/checkpoint branch while external dependencies are
still shared.
For a chat that spans HTTP requests, persist the exported state in the session and rebuild the same prepared prompt/tool setup before importing it:
session_start(); $context = new AiContext(prompts: [new WebAccessTool()]); if (isset($_SESSION['ai_state'])) { $context->importState($_SESSION['ai_state']); } $answer = $context->text($userMessage); $_SESSION['ai_state'] = $context->exportState();
The export contains provider/cursor metadata and checkpoints, not prompts, tools, client or model. Provider/setup mismatches fail by default; provider-side response retention still limits how long a cursor can be resumed.
Callbacks and do()
CallbackTool is just another prepared tool on the context:
use Phore\AiHarness\ToolType\CallbackTool; $context = new AiContext(prompts: [ new CallbackTool( static fn (string $customerId): array => ['customerId' => $customerId, 'status' => 'active'], name: 'load_customer', ), ]); $context->do('Load customer C-1001 and retain the relevant facts.', throw: true); $status = $context->text('What is the customer status?');
do() is useful when the work or tool side effect matters but no text result is
needed yet. RecoverableToolException is the only callback failure returned to
the model as retryable tool feedback; other exceptions abort the run.
Simple typed decisions
$tag = $context->choice('Which tag fits best?', ['news', 'guide', 'review']); $tags = $context->choices(null, ['news', 'guide', 'review'], min: 1, max: 2); $ready = $context->yesNo('Is the draft ready?', allowNull: true); $ranking = $context->rank('Rank by relevance.', ['news', 'guide', 'review']); $score = $context->score('How well does the draft fit the audience?');
prompt: null uses the method's short default prompt. allowNull: true permits
null when the current context is explicitly insufficient for a reliable
decision.
Targeted text and file edits
$generated = $context->text('Write a short introduction.'); $edited = $context->text('Correct spelling only.', input: 'Welcome to our practce.'); $summary = $context->file( 'Correct spelling in both files.', ['/path/to/intro.md', '/path/to/contact.md'], );
Text and file edits use exact search/replacement operations against the
original snapshot. A null search is a full rewrite and must be the only
operation for that target. A failed file remains unchanged while other valid
files in the batch may still be written; this is not a cross-file transaction.
See examples/01-basic-functions.php first,
then examples/02-context.php. The complete context
contract is documented in docs/ai-context.md.
Git Submodules
Beim Klonen direkt mit auschecken:
git clone --recurse-submodules <repo-url>
Nachträglich initialisieren oder aktualisieren:
git submodule update --init --recursive git submodule update --remote --merge
Preferred: prompts from files
For reusable prompt files, prepare PromptFile directly on the context:
use Phore\AiHarness\PromptType\PromptFile; $context = new \Phore\AiHarness\AiContext(prompts: [ new PromptFile(__DIR__ . '/prompts/review.prompt.md'), ]); $result = $context->text('Run the review.');
PromptFile means the file defines the prompt. FilePrompt means the file is
source material attached to a prompt. YAML frontmatter can compose ordered
extends, references and requires_aliases; see
examples/frontmatter-prompt/ for the complete
format and resolved Responses API content.
Reasoning options
The common options shown in the quick start can be supplied as AiContext
defaults or overridden for one call. Reasoning defaults to
['effort' => 'low']. Set reasoning => null to omit the parameter for models
that do not support it. Supported reasoning fields and effort values depend on
the selected model.
Optional debug logging
debug_log is one of the common options shown in the quick start. Set it on an
AiContext to keep the setting for all operations:
$context = new \Phore\AiHarness\AiContext(options: ['debug_log' => true]); $result = $context->text('Explain the status.');
false disables logging, true logs to STDERR, and a
Phore\AiHarness\Logging\LoggerInterface instance receives structured events.
Text and structured requests stream while logging is enabled; images remain
non-streaming. Return values and STDOUT are unaffected.
Only RecoverableToolException becomes retryable tool feedback. Other callback,
binding, transport, authentication and serialization errors propagate. The hard
limit is five callback rounds per invocation. Logs redact tool arguments and
known credentials; results are represented by byte counts.
The final RunStatistics event contains status, request/tool/error/retry counts,
tokens_in, tokens_out, tokens_total, tokens_cached and monotonic
total/API/tool durations. Cached tokens are part of input tokens and are not
counted twice.
Global usage and estimated cost
All OpenAiClient instances automatically accumulate process-local usage,
independently of debug logging, including facade/helper calls, callback follow-ups,
streaming, images and AiRequestSpooler. No setup is required.
phore_ai_text('First task'); phore_ai_text('Second task', ['model' => 'gpt-5-nano']); $stats = get_ai_usage_stats(); printf( "%d requests, %d errors, %d input / %d output / %d total tokens; approx. %s USD\n", $stats['requests'], $stats['errors'], $stats['inputTokens'], $stats['outputTokens'], $stats['totalTokens'], $stats['totalCostUsd'] === null ? 'unknown (partial usage/prices)' : number_format($stats['totalCostUsd'], 6), ); printf("Cached input tokens: %d\n", $stats['cachedInputTokens']); print_r($stats['models']); // Same counters and cost fields, keyed by response model.
requests counts client request attempts (including setup failures), not facade
runs or individual tools; every callback follow-up is another request.
errors counts failed client attempts, HTTP failures, failed/incomplete/cancelled
response statuses and stream callback aborts, once per request. Errors in local
tools, hydration or spooler result callbacks after a successful API response
are not API errors. pendingRequests reports attempts still in progress.
Tokens are summed only from reported usage. Cached input and reasoning output
are subsets, not added again to totals. The response model takes precedence;
the requested model is the fallback. Missing usage increments
missingUsageRequests; only absent/blank model names remain unpricedRequests.
Unknown named models receive a conservative fallback. fallbackRequests counts
these requests; each model row includes pricingSource and
pricesPerMillionTokensUsd. knownCostUsd includes fallback estimates.
knownCostUsd always contains the priced subtotal. totalCostUsd is null
when pending requests, missing usage or unknown prices prevent a complete estimate.
Reading stats returns a detached snapshot and never clears the counters.
State lasts for the PHP runtime (one request in typical PHP-FPM, whole execution
in CLI/long-lived workers); separate processes are not combined.
Phore\AiHarness\Usage\CostEstimator uses a price snapshot checked on 2026-09-11.
Sources: OpenAI pricing,
GPT-5.5,
GPT-5.5 Pro,
GPT-5.4,
Mini,
Nano,
GPT-5.2,
GPT-4.1 and
GPT-5.
| Model | Input USD/1M | Cached input USD/1M | Output USD/1M |
|---|---|---|---|
| chat-latest | 5 | 0.5 | 30 |
| gpt-4.1 | 2 | 0.5 | 8 |
| gpt-5.2 | 1.75 | 0.175 | 14 |
| gpt-5.3-codex | 1.75 | 0.175 | 14 |
| gpt-5.4 | 2.5 | 0.25 | 15 |
| gpt-5.4-mini | 0.75 | 0.075 | 4.5 |
| gpt-5.4-nano | 0.2 | 0.02 | 1.25 |
| gpt-5.5 | 10 | 1 | 45 |
| gpt-5.5-pro | 30 | no discount | 180 |
| gpt-5.6-cyber | 12.5 | 1.25 | 75 |
| gpt-5.6-luna | 0.4 | 0.04 | 1.8 |
| gpt-5.6-sol | 8 | 0.8 | 30 |
| gpt-5.6-terra | 4 | 0.4 | 18 |
| gpt-6-astra | 20 | 2 | 75 |
| gpt-5 | 1.25 | 0.125 | 10 |
| gpt-5-mini | 0.25 | 0.025 | 2 |
| gpt-5-nano | 0.05 | 0.005 | 0.40 |
GPT-6 Astra, GPT-5.6 Sol/Terra/Luna and GPT-5.5 deliberately use their higher long-context standard rates even for short requests. Other named entries use published standard rates. These are budgeting estimates, not exact billing.
Resolution order: explicit model/override, dated snapshot, then case-insensitive
regex matching of complete name segments separated by - _ . / :.
Unknown mini models use the highest input/output rates among listed mini models
(currently 0.75 / 4.50); unknown nano models use 0.20 / 1.25.
Premium segments (pro|max|ultra|opus|astra) take precedence over mini/nano.
All other unknown names, including future higher model versions, use at least
40 / 180 USD per million input/output tokens: the component-wise upper envelope
of GPT-6 Astra long-context Fast input and GPT-5.5 Pro output.
Fallbacks assume no cache discount and can rise with higher custom rates;
lower overrides never reduce the built-in fallback floor. Exact overrides still win.
pricingSource is exact, snapshot, mini-fallback, nano-fallback,
highest-fallback or unknown; exact describes name matching, not invoice accuracy.
Pass overrides in USD per million tokens to price additional models or use updated/custom rates; the resulting snapshot is recalculated from accumulated tokens:
$estimator = new \Phore\AiHarness\Usage\CostEstimator([ 'my-model' => ['input' => 1.0, 'cachedInput' => 0.1, 'output' => 5.0], ]); $stats = get_ai_usage_stats($estimator);
These are rough token-cost estimates, not invoices: hosted tool fees, separate image/audio generation charges, storage, taxes, service tiers and long-context surcharges beyond the conservative rates above are excluded. No network pricing lookup or automatic console output takes place. Only counters are retained, not prompts or response history.