Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6e1ae13
feat: updates OpenAI to use Responses API
JasonTheAdams Dec 30, 2025
d19795c
fix: corrects custom options
JasonTheAdams Dec 30, 2025
e24f55f
feat: adds cli support for stin or file referencing
JasonTheAdams Dec 30, 2025
8a86119
fix: corrects always false return
JasonTheAdams Dec 30, 2025
6f21cb6
test: corrects incorrect namespace
JasonTheAdams Dec 30, 2025
00caa6e
feat: switches to using Images API for image generation model
JasonTheAdams Jan 5, 2026
aac8cbe
refactor: removes image generation from text model
JasonTheAdams Jan 5, 2026
437d0d9
feat: adds support for document inputs
JasonTheAdams Jan 7, 2026
32291e8
refactor: uses OpenAi compatible image model class
JasonTheAdams Jan 8, 2026
932f26d
refactor: simplifies redundancies
JasonTheAdams Jan 8, 2026
bf04bc6
fix: corrects function calling in input array
JasonTheAdams Jan 9, 2026
037e873
refactor: cleans up function call/response handling
JasonTheAdams Jan 13, 2026
97a5d6a
refactor: uses parent method
JasonTheAdams Jan 14, 2026
dfb63a7
chore: corrects `@since` tags
JasonTheAdams Jan 14, 2026
da89e4d
fix: adds unsetting output format back for DALL-E models
JasonTheAdams Jan 14, 2026
603b98b
refactor: removes code interpretation for now
JasonTheAdams Jan 14, 2026
8a563be
refactor: moves function call/response part validation to OpenAI
JasonTheAdams Jan 14, 2026
725b868
test: fixes linting
JasonTheAdams Jan 14, 2026
25fbc4e
refactor: uses $params instead of bypassing data
JasonTheAdams Jan 16, 2026
18aa79b
refactor: improves image generation parms typing
JasonTheAdams Jan 16, 2026
896ed95
Merge branch 'trunk' into add/proper-openai-provider-implementation
JasonTheAdams Jan 16, 2026
d7b7629
refactor: removes stream method as part of broader removal
JasonTheAdams Jan 16, 2026
70a097d
test: removes streaming test
JasonTheAdams Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
* GOOGLE_API_KEY=123456 php cli.php 'Your prompt here' --providerId=google --modelId=gemini-2.5-flash
* OPENAI_API_KEY=123456 php cli.php 'Your prompt here' --providerId=openai
* GOOGLE_API_KEY=123456 OPENAI_API_KEY=123456 php cli.php 'Your prompt here'
*
* For large prompts (e.g., with images), use stdin or file input:
* cat prompt.json | php cli.php - --providerId=openai --modelId=gpt-4o
* php cli.php @prompt.json --providerId=openai --modelId=gpt-4o
*/

declare(strict_types=1);
Expand Down Expand Up @@ -89,7 +93,23 @@ function logError(string $message, int $exit_code = 1): void
}

// Prompt input. Allow complex input as a JSON string.
// Use "-" to read from stdin, or "@/path/to/file" to read from a file.
$promptInput = $positional_args[0];
if ($promptInput === '-') {
$promptInput = file_get_contents('php://stdin');
if ($promptInput === false) {
logError('Failed to read prompt from stdin.');
}
} elseif (str_starts_with($promptInput, '@')) {
$filePath = substr($promptInput, 1);
if (!file_exists($filePath)) {
logError("Prompt file not found: {$filePath}");
}
$promptInput = file_get_contents($filePath);
if ($promptInput === false) {
logError("Failed to read prompt from file: {$filePath}");
}
}
if (str_starts_with($promptInput, '{') || str_starts_with($promptInput, '[')) {
$decodedInput = json_decode($promptInput, true);
if (json_last_error() === JSON_ERROR_NONE) {
Expand Down
6 changes: 4 additions & 2 deletions src/Messages/DTO/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ public function withPart(MessagePart $part): Message
private function validateParts(): void
{
foreach ($this->parts as $part) {
if ($this->role->isUser() && $part->getType()->isFunctionCall()) {
$type = $part->getType();

if ($this->role->isUser() && $type->isFunctionCall()) {
throw new InvalidArgumentException(
'User messages cannot contain function calls.'
);
}

if ($this->role->isModel() && $part->getType()->isFunctionResponse()) {
if ($this->role->isModel() && $type->isFunctionResponse()) {
throw new InvalidArgumentException(
'Model messages cannot contain function responses.'
);
Expand Down
156 changes: 150 additions & 6 deletions src/ProviderImplementations/OpenAi/OpenAiImageGenerationModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,32 @@

namespace WordPress\AiClient\ProviderImplementations\OpenAi;

use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\OpenAiCompatibleImplementation\AbstractOpenAiCompatibleImageGenerationModel;

/**
* Class for an OpenAI image generation model.
* Class for an OpenAI image generation model using the Images API.
*
* This uses the Images API directly to generate images with GPT image models
* (gpt-image-1, etc.) and DALL-E models (dall-e-2, dall-e-3).
*
* @since 0.1.0
*/
class OpenAiImageGenerationModel extends AbstractOpenAiCompatibleImageGenerationModel
{
/**
* @inheritDoc
* {@inheritDoc}
*
* @since 0.1.0
*/
protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request
{
protected function createRequest(
HttpMethodEnum $method,
string $path,
array $headers = [],
$data = null
): Request {
return new Request(
$method,
OpenAiProvider::url($path),
Expand All @@ -30,7 +40,9 @@ protected function createRequest(HttpMethodEnum $method, string $path, array $he
}

/**
* @inheritDoc
* {@inheritDoc}
*
* @since 0.1.0
*/
protected function prepareGenerateImageParams(array $prompt): array
{
Expand All @@ -40,12 +52,144 @@ protected function prepareGenerateImageParams(array $prompt): array
* Only the newer 'gpt-image-' models support passing a MIME type ('output_format').
* Conversely, they do not support 'response_format', but always return a base64 encoded image.
*/
if (isset($params['model']) && is_string($params['model']) && str_starts_with($params['model'], 'gpt-image-')) {
if ($this->isGptImageModel($params['model'])) {
unset($params['response_format']);
} else {
unset($params['output_format']);
}

return $params;
}

/**
* {@inheritDoc}
*
* @since n.e.x.t
*/
protected function prepareSizeParam(?MediaOrientationEnum $orientation, ?string $aspectRatio): string
{
$modelId = $this->metadata()->getId();

if ($this->isGptImageModel($modelId)) {
return $this->prepareGptImageSizeParam($orientation, $aspectRatio);
}

return $this->prepareDalleSizeParam($modelId, $orientation, $aspectRatio);
}

/**
* {@inheritDoc}
*
* @since n.e.x.t
*/
protected function getResultId(array $responseData): string
{
// The Images API returns `created` timestamp instead of `id`.
return isset($responseData['created']) && is_int($responseData['created'])
? 'img-' . $responseData['created']
: '';
}

/**
* Checks if the given model ID is a GPT image model.
*
* @since n.e.x.t
*
* @param string $modelId The model ID to check.
* @return bool True if it's a GPT image model, false otherwise.
*/
protected function isGptImageModel(string $modelId): bool
{
return str_starts_with($modelId, 'gpt-image-');
}

/**
* Prepares the size parameter for GPT image models.
*
* @since n.e.x.t
*
* @param MediaOrientationEnum|null $orientation The desired media orientation.
* @param string|null $aspectRatio The desired media aspect ratio.
* @return string The size parameter value.
*/
protected function prepareGptImageSizeParam(?MediaOrientationEnum $orientation, ?string $aspectRatio): string
{
// If aspect ratio is provided, map it to OpenAI size format.
if ($aspectRatio !== null) {
$aspectRatioMap = [
'1:1' => '1024x1024',
'3:2' => '1536x1024',
'2:3' => '1024x1536',
];
if (isset($aspectRatioMap[$aspectRatio])) {
return $aspectRatioMap[$aspectRatio];
}
}

// Map orientation to size.
if ($orientation !== null) {
if ($orientation->isLandscape()) {
return '1536x1024';
}
if ($orientation->isPortrait()) {
return '1024x1536';
}
}

// Default to square.
return '1024x1024';
}

/**
* Prepares the size parameter for DALL-E models.
*
* @since n.e.x.t
*
* @param string $modelId The model ID (dall-e-2 or dall-e-3).
* @param MediaOrientationEnum|null $orientation The desired media orientation.
* @param string|null $aspectRatio The desired media aspect ratio.
* @return string The size parameter value.
*/
protected function prepareDalleSizeParam(
string $modelId,
?MediaOrientationEnum $orientation,
?string $aspectRatio
): string {
$isDalle3 = $modelId === 'dall-e-3';

// If aspect ratio is provided, map it to size.
if ($aspectRatio !== null) {
if ($isDalle3) {
$aspectRatioMap = [
'1:1' => '1024x1024',
'7:4' => '1792x1024',
'4:7' => '1024x1792',
];
} else {
// DALL-E 2 only supports square images at various resolutions.
$aspectRatioMap = [
'1:1' => '1024x1024',
];
}
if (isset($aspectRatioMap[$aspectRatio])) {
return $aspectRatioMap[$aspectRatio];
}
}

// Map orientation to size.
if ($orientation !== null) {
if ($isDalle3) {
if ($orientation->isLandscape()) {
return '1792x1024';
}
if ($orientation->isPortrait()) {
return '1024x1792';
}
}
// DALL-E 2 only supports square, so orientation doesn't change the size.
}

// Default to square.
return '1024x1024';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ protected function parseResponseToModelMetadataList(Response $response): array
new SupportedOption(OptionEnum::outputMimeType(), ['text/plain', 'application/json']),
new SupportedOption(OptionEnum::outputSchema()),
new SupportedOption(OptionEnum::functionDeclarations()),
new SupportedOption(OptionEnum::webSearch()),
new SupportedOption(OptionEnum::customOptions()),
];
$gptOptions = array_merge($gptBaseOptions, [
Expand All @@ -88,6 +89,8 @@ protected function parseResponseToModelMetadataList(Response $response): array
[ModalityEnum::text()],
[ModalityEnum::text(), ModalityEnum::image()],
[ModalityEnum::text(), ModalityEnum::image(), ModalityEnum::audio()],
[ModalityEnum::text(), ModalityEnum::document()],
[ModalityEnum::text(), ModalityEnum::image(), ModalityEnum::document()],
]
),
new SupportedOption(OptionEnum::outputModalities(), [[ModalityEnum::text()]]),
Expand All @@ -99,6 +102,8 @@ protected function parseResponseToModelMetadataList(Response $response): array
[ModalityEnum::text()],
[ModalityEnum::text(), ModalityEnum::image()],
[ModalityEnum::text(), ModalityEnum::image(), ModalityEnum::audio()],
[ModalityEnum::text(), ModalityEnum::document()],
[ModalityEnum::text(), ModalityEnum::image(), ModalityEnum::document()],
]
),
new SupportedOption(
Expand Down
Loading