diff --git a/src/ProviderImplementations/OpenAi/OpenAiTextGenerationModel.php b/src/ProviderImplementations/OpenAi/OpenAiTextGenerationModel.php index 2b33b4c7..d0e942bf 100644 --- a/src/ProviderImplementations/OpenAi/OpenAiTextGenerationModel.php +++ b/src/ProviderImplementations/OpenAi/OpenAiTextGenerationModel.php @@ -21,6 +21,7 @@ use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\DTO\TokenUsage; use WordPress\AiClient\Results\Enums\FinishReasonEnum; +use WordPress\AiClient\Tools\DTO\CodeExecution; use WordPress\AiClient\Tools\DTO\FunctionCall; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; use WordPress\AiClient\Tools\DTO\WebSearch; @@ -155,29 +156,22 @@ protected function prepareGenerateTextParams(array $prompt): array $functionDeclarations = $config->getFunctionDeclarations(); $webSearch = $config->getWebSearch(); + $codeExecution = $config->getCodeExecution(); $customOptions = $config->getCustomOptions(); - // Check for built-in tools via customOptions. - $codeInterpreter = !empty($customOptions['codeInterpreter']); - - if (is_array($functionDeclarations) || $webSearch || $codeInterpreter) { + if (is_array($functionDeclarations) || $webSearch || $codeExecution) { $params['tools'] = $this->prepareToolsParam( $functionDeclarations, $webSearch, - $codeInterpreter + $codeExecution ); } /* * Any custom options are added to the parameters as well. * This allows developers to pass other options that may be more niche or not yet supported by the SDK. - * Skip options we've already processed explicitly. */ - $processedCustomOptions = ['codeInterpreter']; foreach ($customOptions as $key => $value) { - if (in_array($key, $processedCustomOptions, true)) { - continue; - } if (isset($params[$key])) { throw new InvalidArgumentException( sprintf( @@ -396,13 +390,13 @@ protected function getMessagePartData(MessagePart $part): ?array * * @param list|null $functionDeclarations The function declarations, or null if none. * @param WebSearch|null $webSearch The web search config, or null if none. - * @param bool $codeInterpreter Whether to include the code interpreter tool. + * @param CodeExecution|null $codeExecution The code execution config, or null if none. * @return list> The prepared tools parameter. */ protected function prepareToolsParam( ?array $functionDeclarations, ?WebSearch $webSearch, - bool $codeInterpreter = false + ?CodeExecution $codeExecution = null ): array { $tools = []; @@ -424,10 +418,18 @@ protected function prepareToolsParam( $tools[] = $webSearchTool; } - if ($codeInterpreter) { + if ($codeExecution) { + $containerId = $codeExecution->getContainerId(); + if ($containerId !== null) { + // Use a specific container by ID. + $container = ['type' => 'container', 'container_id' => $containerId]; + } else { + // Use auto mode with optional custom options (e.g., memory_limit). + $container = array_merge(['type' => 'auto'], $codeExecution->getCustomOptions()); + } $tools[] = [ 'type' => 'code_interpreter', - 'container' => ['type' => 'auto'], + 'container' => $container, ]; } diff --git a/src/Providers/Models/DTO/ModelConfig.php b/src/Providers/Models/DTO/ModelConfig.php index ac834205..e1b06f7d 100644 --- a/src/Providers/Models/DTO/ModelConfig.php +++ b/src/Providers/Models/DTO/ModelConfig.php @@ -9,6 +9,7 @@ use WordPress\AiClient\Files\Enums\FileTypeEnum; use WordPress\AiClient\Files\Enums\MediaOrientationEnum; use WordPress\AiClient\Messages\Enums\ModalityEnum; +use WordPress\AiClient\Tools\DTO\CodeExecution; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; use WordPress\AiClient\Tools\DTO\WebSearch; @@ -21,6 +22,7 @@ * * @since 0.1.0 * + * @phpstan-import-type CodeExecutionArrayShape from CodeExecution * @phpstan-import-type FunctionDeclarationArrayShape from FunctionDeclaration * @phpstan-import-type WebSearchArrayShape from WebSearch * @@ -39,6 +41,7 @@ * topLogprobs?: int, * functionDeclarations?: list, * webSearch?: WebSearchArrayShape, + * codeExecution?: CodeExecutionArrayShape, * outputFileType?: string, * outputMimeType?: string, * outputSchema?: array, @@ -66,6 +69,7 @@ class ModelConfig extends AbstractDataTransferObject public const KEY_TOP_LOGPROBS = 'topLogprobs'; public const KEY_FUNCTION_DECLARATIONS = 'functionDeclarations'; public const KEY_WEB_SEARCH = 'webSearch'; + public const KEY_CODE_EXECUTION = 'codeExecution'; public const KEY_OUTPUT_FILE_TYPE = 'outputFileType'; public const KEY_OUTPUT_MIME_TYPE = 'outputMimeType'; public const KEY_OUTPUT_SCHEMA = 'outputSchema'; @@ -151,6 +155,11 @@ class ModelConfig extends AbstractDataTransferObject */ protected ?WebSearch $webSearch = null; + /** + * @var CodeExecution|null Code execution configuration for the model. + */ + protected ?CodeExecution $codeExecution = null; + /** * @var FileTypeEnum|null Output file type. */ @@ -540,6 +549,30 @@ public function getWebSearch(): ?WebSearch return $this->webSearch; } + /** + * Sets the code execution configuration. + * + * @since n.e.x.t + * + * @param CodeExecution $codeExecution The code execution configuration. + */ + public function setCodeExecution(CodeExecution $codeExecution): void + { + $this->codeExecution = $codeExecution; + } + + /** + * Gets the code execution configuration. + * + * @since n.e.x.t + * + * @return CodeExecution|null The code execution configuration. + */ + public function getCodeExecution(): ?CodeExecution + { + return $this->codeExecution; + } + /** * Sets the output file type. * @@ -857,6 +890,7 @@ public static function getJsonSchema(): array 'description' => 'Function declarations available to the model.', ], self::KEY_WEB_SEARCH => WebSearch::getJsonSchema(), + self::KEY_CODE_EXECUTION => CodeExecution::getJsonSchema(), self::KEY_OUTPUT_FILE_TYPE => [ 'type' => 'string', 'enum' => FileTypeEnum::getValues(), @@ -972,6 +1006,10 @@ static function (FunctionDeclaration $function_declaration): array { $data[self::KEY_WEB_SEARCH] = $this->webSearch->toArray(); } + if ($this->codeExecution !== null) { + $data[self::KEY_CODE_EXECUTION] = $this->codeExecution->toArray(); + } + if ($this->outputFileType !== null) { $data[self::KEY_OUTPUT_FILE_TYPE] = $this->outputFileType->value; } @@ -1077,6 +1115,10 @@ static function (array $function_declaration_data): FunctionDeclaration { $config->setWebSearch(WebSearch::fromArray($array[self::KEY_WEB_SEARCH])); } + if (isset($array[self::KEY_CODE_EXECUTION])) { + $config->setCodeExecution(CodeExecution::fromArray($array[self::KEY_CODE_EXECUTION])); + } + if (isset($array[self::KEY_OUTPUT_FILE_TYPE])) { $config->setOutputFileType(FileTypeEnum::from($array[self::KEY_OUTPUT_FILE_TYPE])); } diff --git a/src/Providers/Models/Enums/OptionEnum.php b/src/Providers/Models/Enums/OptionEnum.php index 27b2248f..5d5aedea 100644 --- a/src/Providers/Models/Enums/OptionEnum.php +++ b/src/Providers/Models/Enums/OptionEnum.php @@ -40,6 +40,7 @@ * @method static self topLogprobs() Creates an instance for TOP_LOGPROBS option. * @method static self topP() Creates an instance for TOP_P option. * @method static self webSearch() Creates an instance for WEB_SEARCH option. + * @method static self codeExecution() Creates an instance for CODE_EXECUTION option. * @method bool isCandidateCount() Checks if the option is CANDIDATE_COUNT. * @method bool isCustomOptions() Checks if the option is CUSTOM_OPTIONS. * @method bool isFrequencyPenalty() Checks if the option is FREQUENCY_PENALTY. @@ -61,6 +62,7 @@ * @method bool isTopLogprobs() Checks if the option is TOP_LOGPROBS. * @method bool isTopP() Checks if the option is TOP_P. * @method bool isWebSearch() Checks if the option is WEB_SEARCH. + * @method bool isCodeExecution() Checks if the option is CODE_EXECUTION. * * @since 0.1.0 */ diff --git a/src/Tools/DTO/CodeExecution.php b/src/Tools/DTO/CodeExecution.php new file mode 100644 index 00000000..5d432bf8 --- /dev/null +++ b/src/Tools/DTO/CodeExecution.php @@ -0,0 +1,133 @@ +} + * + * @extends AbstractDataTransferObject + */ +class CodeExecution extends AbstractDataTransferObject +{ + public const KEY_CONTAINER_ID = 'containerId'; + public const KEY_CUSTOM_OPTIONS = 'customOptions'; + + /** + * The container ID for code execution. + * + * When null, providers should use their default/auto mode. + * + * @var string|null + */ + private ?string $containerId; + + /** + * Provider-specific custom options. + * + * For OpenAI, this can include options like 'memory_limit' when using auto mode. + * + * @var array + */ + private array $customOptions; + + /** + * Constructor. + * + * @since n.e.x.t + * + * @param string|null $containerId The container ID, or null for auto mode. + * @param array $customOptions Provider-specific custom options. + */ + public function __construct(?string $containerId = null, array $customOptions = []) + { + $this->containerId = $containerId; + $this->customOptions = $customOptions; + } + + /** + * Gets the container ID. + * + * @since n.e.x.t + * + * @return string|null The container ID, or null for auto mode. + */ + public function getContainerId(): ?string + { + return $this->containerId; + } + + /** + * Gets the custom options. + * + * @since n.e.x.t + * + * @return array The custom options. + */ + public function getCustomOptions(): array + { + return $this->customOptions; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function getJsonSchema(): array + { + return [ + 'type' => 'object', + 'properties' => [ + self::KEY_CONTAINER_ID => [ + 'type' => ['string', 'null'], + 'description' => 'The container ID for code execution, or null for auto mode.', + ], + self::KEY_CUSTOM_OPTIONS => [ + 'type' => 'object', + 'additionalProperties' => true, + 'description' => 'Provider-specific custom options.', + ], + ], + 'required' => [], + ]; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + * + * @return CodeExecutionArrayShape + */ + public function toArray(): array + { + return [ + self::KEY_CONTAINER_ID => $this->containerId, + self::KEY_CUSTOM_OPTIONS => $this->customOptions, + ]; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public static function fromArray(array $array): self + { + return new self( + $array[self::KEY_CONTAINER_ID] ?? null, + $array[self::KEY_CUSTOM_OPTIONS] ?? [] + ); + } +} diff --git a/tests/unit/ProviderImplementations/OpenAi/MockOpenAiTextGenerationModel.php b/tests/unit/ProviderImplementations/OpenAi/MockOpenAiTextGenerationModel.php index f64cf119..df0418de 100644 --- a/tests/unit/ProviderImplementations/OpenAi/MockOpenAiTextGenerationModel.php +++ b/tests/unit/ProviderImplementations/OpenAi/MockOpenAiTextGenerationModel.php @@ -16,6 +16,7 @@ use WordPress\AiClient\Results\DTO\Candidate; use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\Enums\FinishReasonEnum; +use WordPress\AiClient\Tools\DTO\CodeExecution; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; use WordPress\AiClient\Tools\DTO\WebSearch; @@ -157,15 +158,15 @@ public function exposeGetMessagePartData(MessagePart $part): ?array * * @param list|null $functionDeclarations * @param WebSearch|null $webSearch - * @param bool $codeInterpreter + * @param CodeExecution|null $codeExecution * @return list> */ public function exposePrepareToolsParam( ?array $functionDeclarations, ?WebSearch $webSearch, - bool $codeInterpreter = false + ?CodeExecution $codeExecution = null ): array { - return $this->prepareToolsParam($functionDeclarations, $webSearch, $codeInterpreter); + return $this->prepareToolsParam($functionDeclarations, $webSearch, $codeExecution); } /** diff --git a/tests/unit/ProviderImplementations/OpenAi/OpenAiTextGenerationModelTest.php b/tests/unit/ProviderImplementations/OpenAi/OpenAiTextGenerationModelTest.php index 635712e9..8f263e2c 100644 --- a/tests/unit/ProviderImplementations/OpenAi/OpenAiTextGenerationModelTest.php +++ b/tests/unit/ProviderImplementations/OpenAi/OpenAiTextGenerationModelTest.php @@ -20,6 +20,7 @@ use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\Enums\FinishReasonEnum; +use WordPress\AiClient\Tools\DTO\CodeExecution; use WordPress\AiClient\Tools\DTO\FunctionCall; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; use WordPress\AiClient\Tools\DTO\FunctionResponse; @@ -312,11 +313,11 @@ public function testPrepareGenerateTextParamsWithWebSearch(): void * * @return void */ - public function testPrepareGenerateTextParamsWithCodeInterpreter(): void + public function testPrepareGenerateTextParamsWithCodeExecution(): void { $prompt = [new Message(MessageRoleEnum::user(), [new MessagePart('Run some code')])]; $config = new ModelConfig(); - $config->setCustomOptions(['codeInterpreter' => true]); + $config->setCodeExecution(new CodeExecution()); $model = $this->createModel($config); $params = $model->exposePrepareGenerateTextParams($prompt); @@ -511,11 +512,12 @@ public function testPrepareToolsParamWithAllTools(): void ['type' => 'object'] ); $webSearch = new WebSearch(); + $codeExecution = new CodeExecution(); $tools = $model->exposePrepareToolsParam( [$functionDeclaration], $webSearch, - true + $codeExecution ); $this->assertCount(3, $tools); diff --git a/tests/unit/Providers/Models/Enums/OptionEnumTest.php b/tests/unit/Providers/Models/Enums/OptionEnumTest.php index 9248a9ad..a5c6efb0 100644 --- a/tests/unit/Providers/Models/Enums/OptionEnumTest.php +++ b/tests/unit/Providers/Models/Enums/OptionEnumTest.php @@ -51,6 +51,7 @@ protected function getExpectedValues(): array 'TOP_LOGPROBS' => 'topLogprobs', 'FUNCTION_DECLARATIONS' => 'functionDeclarations', 'WEB_SEARCH' => 'webSearch', + 'CODE_EXECUTION' => 'codeExecution', 'OUTPUT_FILE_TYPE' => 'outputFileType', 'OUTPUT_MIME_TYPE' => 'outputMimeType', 'OUTPUT_SCHEMA' => 'outputSchema',