ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PKY\]] db/hooks.phpnu[. /** * Hook listener callbacks for the Open AI provider. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $callbacks = [ [ 'hook' => \core_ai\hook\after_ai_provider_form_hook::class, 'callback' => \aiprovider_openai\hook_listener::class . '::set_form_definition_for_aiprovider_openai', ], [ 'hook' => \core_ai\hook\after_ai_action_settings_form_hook::class, 'callback' => \aiprovider_openai\hook_listener::class . '::set_model_form_definition_for_aiprovider_openai', ], ]; PKY\]GDDlang/en/aiprovider_openai.phpnu[. /** * Strings for component aiprovider_openai, language 'en'. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['action:explain_text:endpoint'] = 'API endpoint'; $string['action:explain_text:model'] = 'AI model'; $string['action:explain_text:model_help'] = 'The model used to explain the provided text.'; $string['action:explain_text:systeminstruction'] = 'System instruction'; $string['action:explain_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['action:generate_image:endpoint'] = 'API endpoint'; $string['action:generate_image:model'] = 'AI model'; $string['action:generate_image:model_help'] = 'The model used to generate images from user prompts.'; $string['action:generate_text:endpoint'] = 'API endpoint'; $string['action:generate_text:model'] = 'AI model'; $string['action:generate_text:model_help'] = 'The model used to generate the text response.'; $string['action:generate_text:systeminstruction'] = 'System instruction'; $string['action:generate_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['action:summarise_text:endpoint'] = 'API endpoint'; $string['action:summarise_text:model'] = 'AI model'; $string['action:summarise_text:model_help'] = 'The model used to summarise the provided text.'; $string['action:summarise_text:systeminstruction'] = 'System instruction'; $string['action:summarise_text:systeminstruction_help'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['apikey'] = 'OpenAI API key'; $string['apikey_help'] = 'Get a key from your OpenAI API keys.'; $string['custom_model_name'] = 'Custom model name'; $string['extraparams'] = 'Extra parameters'; $string['extraparams_help'] = 'Extra parameters can be configured here. We support JSON format. For example:
{
    "temperature": 0.5,
    "max_completion_tokens": 100
}
'; $string['invalidjson'] = 'Invalid JSON string'; $string['orgid'] = 'OpenAI organization ID'; $string['orgid_help'] = 'Get your OpenAI organization ID from your OpenAI account.'; $string['pluginname'] = 'OpenAI API provider'; $string['privacy:metadata'] = 'The OpenAI API provider plugin does not store any personal data.'; $string['privacy:metadata:aiprovider_openai:externalpurpose'] = 'This information is sent to the OpenAI API in order for a response to be generated. Your OpenAI account settings may change how OpenAI stores and retains this data. No user data is explicitly sent to OpenAI or stored in Moodle LMS by this plugin.'; $string['privacy:metadata:aiprovider_openai:model'] = 'The model used to generate the response.'; $string['privacy:metadata:aiprovider_openai:numberimages'] = 'When generating images the number of images used in the response.'; $string['privacy:metadata:aiprovider_openai:prompttext'] = 'The user entered text prompt used to generate the response.'; $string['privacy:metadata:aiprovider_openai:responseformat'] = 'The format of the response. When generating images.'; $string['settings'] = 'Settings'; $string['settings_frequency_penalty'] = 'frequency_penalty'; $string['settings_frequency_penalty_help'] = 'The frequency penalty adjusts how often words are repeated. The higher the penalty, the less repetitions in the generated text.'; $string['settings_help'] = 'Adjust the settings below to customise how requests are sent to OpenAI.'; $string['settings_max_completion_tokens'] = 'max_completion_tokens'; $string['settings_max_completion_tokens_help'] = 'The maximum number of tokens used in the generated text.'; $string['settings_presence_penalty'] = 'presence_penalty'; $string['settings_presence_penalty_help'] = 'The presence penalty encourages the model to use new words by increasing the likelihood of choosing words it hasn\'t used before. A higher value makes the generated text more diverse, while a lower value allows more repetition.'; $string['settings_top_p'] = 'top_p'; $string['settings_top_p_help'] = 'top_p (nucleus sampling) determines how many possible words to consider. A high value (e.g. 0.9) means the model looks at more words, which makes the generated text more diverse.'; // Deprecated since Moodle 5.0. $string['action:generate_image:model_desc'] = 'The model used to generate images from user prompts.'; $string['action:generate_text:model_desc'] = 'The model used to generate the text response.'; $string['action:generate_text:systeminstruction_desc'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['action:summarise_text:model_desc'] = 'The model used to summarise the provided text.'; $string['action:summarise_text:systeminstruction_desc'] = 'This instruction is sent to the AI model along with the user\'s prompt. Editing this instruction is not recommended unless absolutely required.'; $string['apikey_desc'] = 'Get a key from OpenAI Platform API keys.'; $string['enableglobalratelimit'] = 'Set site-wide rate limit'; $string['enableglobalratelimit_desc'] = 'Limit the number of requests that the OpenAI API provider can receive across the entire site every hour.'; $string['enableuserratelimit'] = 'Set user rate limit'; $string['enableuserratelimit_desc'] = 'Limit the number of requests each user can make to the OpenAI API provider every hour.'; $string['globalratelimit'] = 'Maximum number of site-wide requests'; $string['globalratelimit_desc'] = 'The number of site-wide requests allowed per hour.'; $string['orgid_desc'] = 'Get an OpenAI organization ID from your OpenAI Platform account.'; $string['userratelimit'] = 'Maximum number of requests per user'; $string['userratelimit_desc'] = 'The number of requests allowed per hour, per user.'; $string['settings_max_tokens'] = 'max_tokens'; $string['settings_max_tokens_help'] = 'The maximum number of tokens used in the generated text.'; PKY\:lang/en/deprecated.txtnu[action:generate_image:model_desc,aiprovider_openai action:generate_text:model_desc,aiprovider_openai action:generate_text:systeminstruction_desc,aiprovider_openai action:summarise_text:model_desc,aiprovider_openai action:summarise_text:systeminstruction_desc,aiprovider_openai apikey_desc,aiprovider_openai enableglobalratelimit,aiprovider_openai enableglobalratelimit_desc,aiprovider_openai enableuserratelimit,aiprovider_openai enableuserratelimit_desc,aiprovider_openai globalratelimit,aiprovider_openai globalratelimit_desc,aiprovider_openai orgid_desc,aiprovider_openai userratelimit,aiprovider_openai userratelimit_desc,aiprovider_openai settings_max_tokens,aiprovider_openai settings_max_tokens_help,aiprovider_openai PKY\sA/DD version.phpnu[. /** * Version information for aiprovider_openai. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'aiprovider_openai'; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->maturity = MATURITY_STABLE; PKY\( +classes/form/action_generate_image_form.phpnu[. namespace aiprovider_openai\form; use aiprovider_openai\aimodel\openai_base; /** * Generate image action provider settings form. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_generate_image_form extends action_form { #[\Override] protected function definition(): void { parent::definition(); $mform = $this->_form; $this->add_model_fields(openai_base::MODEL_TYPE_IMAGE); // API endpoint. $mform->addElement( 'text', 'endpoint', get_string("action:{$this->actionname}:endpoint", 'aiprovider_openai'), 'maxlength="255" size="30"', ); $mform->setType('endpoint', PARAM_URL); $mform->addRule('endpoint', null, 'required', null, 'client'); $mform->setDefault('endpoint', $this->actionconfig['endpoint'] ?? 'https://api.openai.com/v1/images/generations'); if ($this->returnurl) { $mform->addElement('hidden', 'returnurl', $this->returnurl); $mform->setType('returnurl', PARAM_LOCALURL); } // Add the action class as a hidden field. $mform->addElement('hidden', 'action', $this->action); $mform->setType('action', PARAM_TEXT); // Add the provider class as a hidden field. $mform->addElement('hidden', 'provider', $this->providername); $mform->setType('provider', PARAM_TEXT); // Add the provider id as a hidden field. $mform->addElement('hidden', 'providerid', $this->providerid); $mform->setType('providerid', PARAM_INT); $this->set_data($this->actionconfig); } } PKY\=)kclasses/form/action_form.phpnu[. namespace aiprovider_openai\form; use aiprovider_openai\helper; use core_ai\form\action_settings_form; /** * Base action settings form for OpenAI provider. * * @package aiprovider_openai * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_form extends action_settings_form { /** * @var array Action configuration. */ protected array $actionconfig; /** * @var string|null Return URL. */ protected ?string $returnurl; /** * @var string Action name. */ protected string $actionname; /** * @var string Action class. */ protected string $action; /** * @var int Provider ID. */ protected int $providerid; /** * @var string Provider name. */ protected string $providername; #[\Override] protected function definition(): void { $mform = $this->_form; $this->actionconfig = $this->_customdata['actionconfig']['settings'] ?? []; $this->returnurl = $this->_customdata['returnurl'] ?? null; $this->actionname = $this->_customdata['actionname']; $this->action = $this->_customdata['action']; $this->providerid = $this->_customdata['providerid'] ?? 0; $this->providername = $this->_customdata['providername'] ?? 'aiprovider_openai'; $mform->addElement('header', 'generalsettingsheader', get_string('general', 'core')); } #[\Override] public function set_data($data): void { if (!empty($data['modelextraparams'])) { $data['modelextraparams'] = json_encode(json_decode($data['modelextraparams']), JSON_PRETTY_PRINT); } parent::set_data($data); } #[\Override] public function get_data(): ?\stdClass { $data = parent::get_data(); if ($data) { if (isset($data->modeltemplate)) { if ($data->modeltemplate === 'custom') { $data->model = $data->custommodel; } else { // Set the model to the selected model template. $data->model = $data->modeltemplate; // Cast settings to their intended types. if ($data->model === 'gpt-4o' || $data->model === 'o1') { if (isset($data->top_p)) { $data->top_p = floatval($data->top_p); } if (isset($data->max_completion_tokens)) { $data->max_completion_tokens = intval($data->max_completion_tokens); } if (isset($data->presence_penalty)) { $data->presence_penalty = floatval($data->presence_penalty); } if (isset($data->frequency_penalty)) { $data->frequency_penalty = floatval($data->frequency_penalty); } } } } // Unset the model template. unset($data->custommodel); unset($data->modeltemplate); // Unset any false-y values. $data = (object) array_filter((array) $data); } return $data; } #[\Override] public function validation($data, $files): array { $errors = parent::validation($data, $files); // Validate the extra parameters. if (!empty($data['modelextraparams'])) { json_decode($data['modelextraparams']); if (json_last_error() !== JSON_ERROR_NONE) { $errors['modelextraparams'] = get_string('invalidjson', 'aiprovider_openai'); } } // Validate the model. if ($data['modeltemplate'] === 'custom' && empty($data['custommodel'])) { $errors['custommodel'] = get_string('required'); } return $errors; } #[\Override] public function get_defaults(): array { $data = parent::get_defaults(); unset( $data['modeltemplate'], $data['custommodel'], $data['modelextraparams'], ); return $data; } /** * Add model fields to the form. * * @param int $modeltype Model type. */ protected function add_model_fields(int $modeltype): void { global $PAGE; $PAGE->requires->js_call_amd('aiprovider_openai/modelchooser', 'init'); $mform = $this->_form; $actionname = $this->actionname; // Action model to use. $mform->addElement( 'select', 'modeltemplate', get_string("action:{$this->actionname}:model", 'aiprovider_openai'), $this->get_model_list($modeltype), ['data-modelchooser-field' => 'selector'], ); $mform->setType('modeltemplate', PARAM_TEXT); $mform->addRule('modeltemplate', null, 'required', null, 'client'); if (!empty($this->actionconfig['model']) && (!array_key_exists($this->actionconfig['model'], $this->get_model_list($modeltype)) || !empty($this->actionconfig['modelextraparams']))) { $defaultmodel = 'custom'; } else if (empty($this->actionconfig['model'])) { $defaultmodel = ($actionname === 'generate_image') ? 'dall-e-3' : 'gpt-4o'; } else { $defaultmodel = $this->actionconfig['model']; } $mform->setDefault('modeltemplate', $defaultmodel); $mform->addHelpButton('modeltemplate', "action:{$this->actionname}:model", 'aiprovider_openai'); $mform->addElement('hidden', 'model', $defaultmodel); $mform->setType('model', PARAM_TEXT); $mform->addElement('text', 'custommodel', get_string('custom_model_name', 'aiprovider_openai')); $mform->setType('custommodel', PARAM_TEXT); $mform->setDefault('custommodel', $this->actionconfig['model'] ?? ''); $mform->hideIf('custommodel', 'modeltemplate', 'neq', 'custom'); $mform->registerNoSubmitButton('updateactionsettings'); $mform->addElement( 'submit', 'updateactionsettings', 'updateactionsettings', ['data-modelchooser-field' => 'updateButton', 'class' => 'd-none'] ); } /** * Get the list of models. * * @param int $modeltype Model type. * @return array List of models. */ protected function get_model_list(int $modeltype): array { $models = []; $models['custom'] = get_string('custom', 'core_form'); foreach (helper::get_model_classes() as $class) { $model = new $class(); if (in_array($modeltype, $model->model_type())) { $models[$model->get_model_name()] = $model->get_model_display_name(); } } return $models; } } PKY\lX *classes/form/action_generate_text_form.phpnu[. namespace aiprovider_openai\form; use aiprovider_openai\aimodel\openai_base; /** * Generate text action provider settings form. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class action_generate_text_form extends action_form { #[\Override] protected function definition(): void { parent::definition(); $mform = $this->_form; $this->add_model_fields(openai_base::MODEL_TYPE_TEXT); // API endpoint. $mform->addElement( 'text', 'endpoint', get_string("action:{$this->actionname}:endpoint", 'aiprovider_openai'), 'maxlength="255" size="30"', ); $mform->setType('endpoint', PARAM_URL); $mform->addRule('endpoint', null, 'required', null, 'client'); $mform->setDefault('endpoint', $actionconfig['endpoint'] ?? 'https://api.openai.com/v1/chat/completions'); // System Instructions. $mform->addElement( 'textarea', 'systeminstruction', get_string("action:{$this->actionname}:systeminstruction", 'aiprovider_openai'), 'wrap="virtual" rows="5" cols="20"', ); $mform->setType('systeminstruction', PARAM_TEXT); $mform->setDefault('systeminstruction', $actionconfig['systeminstruction'] ?? $this->action::get_system_instruction()); $mform->addHelpButton('systeminstruction', "action:{$this->actionname}:systeminstruction", 'aiprovider_openai'); if ($this->returnurl) { $mform->addElement('hidden', 'returnurl', $this->returnurl); $mform->setType('returnurl', PARAM_LOCALURL); } // Add the action class as a hidden field. $mform->addElement('hidden', 'action', $this->action); $mform->setType('action', PARAM_TEXT); // Add the provider class as a hidden field. $mform->addElement('hidden', 'provider', $this->providername); $mform->setType('provider', PARAM_TEXT); // Add the provider id as a hidden field. $mform->addElement('hidden', 'providerid', $this->providerid); $mform->setType('providerid', PARAM_INT); $this->set_data($this->actionconfig); } } PKY\ classes/privacy/provider.phpnu[. namespace aiprovider_openai\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\userlist; /** * Privacy provider implementation for OpenAI provider * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @codeCoverageIgnore */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { #[\Override] public static function get_metadata(collection $collection): collection { $collection->add_external_location_link('aiprovider_openai', [ 'prompttext' => 'privacy:metadata:aiprovider_openai:prompttext', 'model' => 'privacy:metadata:aiprovider_openai:model', 'numberimages' => 'privacy:metadata:aiprovider_openai:numberimages', 'responseformat' => 'privacy:metadata:aiprovider_openai:responseformat', ], 'privacy:metadata:aiprovider_openai:externalpurpose'); return $collection; } #[\Override] public static function get_contexts_for_userid(int $userid): contextlist { return new contextlist(); } #[\Override] public static function get_users_in_context(userlist $userlist) { } #[\Override] public static function export_user_data(approved_contextlist $contextlist) { } #[\Override] public static function delete_data_for_all_users_in_context(\context $context) { } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { } #[\Override] public static function delete_data_for_user(approved_contextlist $contextlist) { } } PKY\Π"classes/process_generate_image.phpnu[. namespace aiprovider_openai; use core\http_client; use core_ai\ai_image; use GuzzleHttp\Psr7\Request; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; /** * Class process image generation. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_generate_image extends abstract_processor { /** @var int The number of images to generate dall-e-3 only supports 1. */ private int $numberimages = 1; /** @var string Response format: url or b64_json. */ private string $responseformat = 'url'; #[\Override] protected function query_ai_api(): array { $response = parent::query_ai_api(); // If the request was successful, save the URL to a file. if ($response['success']) { $fileobj = $this->url_to_file( $this->action->get_configuration('userid'), $response['sourceurl'] ); // Add the file to the response, so the calling placement can do whatever they want with it. $response['draftfile'] = $fileobj; } return $response; } /** * Convert the given aspect ratio to an image size * that is compatible with the OpenAI API. * * @param string $ratio The aspect ratio of the image. * @return string The size of the image. */ private function calculate_size(string $ratio): string { if ($ratio === 'square') { $size = '1024x1024'; } else if ($ratio === 'landscape') { $size = '1792x1024'; } else if ($ratio === 'portrait') { $size = '1024x1792'; } else { throw new \coding_exception('Invalid aspect ratio: ' . $ratio); } return $size; } #[\Override] protected function create_request_object(string $userid): RequestInterface { // Create the request object. $requestobj = new \stdClass(); $requestobj->model = $this->get_model(); $requestobj->user = $userid; $requestobj->prompt = $this->action->get_configuration('prompttext'); $requestobj->n = $this->numberimages; $requestobj->quality = $this->action->get_configuration('quality'); $requestobj->response_format = $this->responseformat; $requestobj->size = $this->calculate_size($this->action->get_configuration('aspectratio')); $requestobj->style = $this->action->get_configuration('style'); // Append the extra model settings. $modelsettings = $this->get_model_settings(); foreach ($modelsettings as $setting => $value) { $requestobj->$setting = $value; } return new Request( method: 'POST', uri: '', headers: [ 'Content-Type' => 'application/json', ], body: json_encode($requestobj), ); } #[\Override] protected function handle_api_success(ResponseInterface $response): array { $responsebody = $response->getBody(); $bodyobj = json_decode($responsebody->getContents()); return [ 'success' => true, 'sourceurl' => $bodyobj->data[0]->url, 'revisedprompt' => $bodyobj->data[0]->revised_prompt, 'model' => $this->get_model(), // There is no model in the response, use config. ]; } /** * Convert the url for the image to a file. * * Placements can't interact with the provider AI directly, * therefore we need to provide the image file in a format that can * be used by placements. So we use the file API. * * @param int $userid The user id. * @param string $url The URL to the image. * @return \stored_file The file object. */ private function url_to_file(int $userid, string $url): \stored_file { global $CFG; require_once("{$CFG->libdir}/filelib.php"); $parsedurl = parse_url($url, PHP_URL_PATH); // Parse the URL to get the path. $filename = basename($parsedurl); // Get the basename of the path. $client = \core\di::get(http_client::class); // Download the image and add the watermark. $tempdst = make_request_directory() . DIRECTORY_SEPARATOR . $filename; $client->get($url, [ 'sink' => $tempdst, 'timeout' => $CFG->repositorygetfiletimeout, ]); $image = new ai_image($tempdst); $image->add_watermark()->save(); // We put the file in the user draft area initially. // Placements (on behalf of the user) can then move it to the correct location. $fileinfo = new \stdClass(); $fileinfo->contextid = \context_user::instance($userid)->id; $fileinfo->filearea = 'draft'; $fileinfo->component = 'user'; $fileinfo->itemid = file_get_unused_draft_itemid(); $fileinfo->filepath = '/'; $fileinfo->filename = $filename; $fs = get_file_storage(); return $fs->create_file_from_string($fileinfo, file_get_contents($tempdst)); } } PKY\[classes/aimodel/dalle3.phpnu[. namespace aiprovider_openai\aimodel; use core_ai\aimodel\base; /** * DALL-e-3 AI model. * * @package aiprovider_openai * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dalle3 extends base implements openai_base { #[\Override] public function get_model_name(): string { return 'dall-e-3'; } #[\Override] public function get_model_display_name(): string { return 'DALL-e-3'; } #[\Override] public function has_model_settings(): bool { return false; } #[\Override] public function model_type(): array { return [self::MODEL_TYPE_IMAGE]; } } PKY\ classes/aimodel/openai_base.phpnu[. namespace aiprovider_openai\aimodel; /** * OpenAI base AI model interface. * * @package aiprovider_openai * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface openai_base { /** @var int MODEL_TYPE_TEXT Text model type. */ public const MODEL_TYPE_TEXT = 1; /** @var int MODEL_TYPE_IMAGE Image model type. */ public const MODEL_TYPE_IMAGE = 2; /** * Get model types. * * @return array List of model types. */ public function model_type(): array; } PKY\cclasses/aimodel/o1.phpnu[. namespace aiprovider_openai\aimodel; use MoodleQuickForm; /** * O1 AI model. * * @package aiprovider_openai * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class o1 extends gpt4o { #[\Override] public function get_model_name(): string { return 'o1'; } #[\Override] public function get_model_display_name(): string { return 'O1'; } #[\Override] public function has_model_settings(): bool { return true; } #[\Override] public function add_model_settings(MoodleQuickForm $mform): void { $mform->addElement( 'text', 'max_completion_tokens', get_string('settings_max_completion_tokens', 'aiprovider_openai'), ); $mform->setType('max_completion_tokens', PARAM_INT); $mform->addHelpButton('max_completion_tokens', 'settings_max_completion_tokens', 'aiprovider_openai'); } #[\Override] public function model_type(): array { return [self::MODEL_TYPE_TEXT]; } } PKY\Zxt t classes/aimodel/gpt4o.phpnu[. namespace aiprovider_openai\aimodel; use core_ai\aimodel\base; use MoodleQuickForm; /** * GPT-4o AI model. * * @package aiprovider_openai * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class gpt4o extends base implements openai_base { #[\Override] public function get_model_name(): string { return 'gpt-4o'; } #[\Override] public function get_model_display_name(): string { return 'GPT-4o'; } #[\Override] public function has_model_settings(): bool { return true; } #[\Override] public function add_model_settings(MoodleQuickForm $mform): void { $mform->addElement( 'text', 'top_p', get_string('settings_top_p', 'aiprovider_openai'), ); $mform->setType('top_p', PARAM_FLOAT); $mform->addHelpButton('top_p', 'settings_top_p', 'aiprovider_openai'); $mform->addElement( 'text', 'max_completion_tokens', get_string('settings_max_completion_tokens', 'aiprovider_openai'), ); $mform->setType('max_completion_tokens', PARAM_INT); $mform->addHelpButton('max_completion_tokens', 'settings_max_completion_tokens', 'aiprovider_openai'); $mform->addElement( 'text', 'frequency_penalty', get_string('settings_frequency_penalty', 'aiprovider_openai'), ); // This is a raw value because it can be a float from -2.0 to 2.0. $mform->setType('frequency_penalty', PARAM_RAW); $mform->addHelpButton('frequency_penalty', 'settings_frequency_penalty', 'aiprovider_openai'); $mform->addElement( 'text', 'presence_penalty', get_string('settings_presence_penalty', 'aiprovider_openai'), ); // This is a raw value because it can be a float from -2.0 to 2.0. $mform->setType('presence_penalty', PARAM_RAW); $mform->addHelpButton('presence_penalty', 'settings_presence_penalty', 'aiprovider_openai'); } #[\Override] public function model_type(): array { return [self::MODEL_TYPE_TEXT]; } } PKY\ l"classes/process_summarise_text.phpnu[. namespace aiprovider_openai; /** * Class process text summarisation. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_summarise_text extends process_generate_text { } PKY\2nclasses/hook_listener.phpnu[. namespace aiprovider_openai; use aiprovider_openai\model\base; use core_ai\hook\after_ai_action_settings_form_hook; use core_ai\hook\after_ai_provider_form_hook; /** * Hook listener for Open AI provider. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class hook_listener { /** * Hook listener for the Open AI instance setup form. * * @param after_ai_provider_form_hook $hook The hook to add to the AI instance setup. */ public static function set_form_definition_for_aiprovider_openai(after_ai_provider_form_hook $hook): void { if ($hook->plugin !== 'aiprovider_openai') { return; } $mform = $hook->mform; // Required setting to store OpenAI API key. $mform->addElement( 'passwordunmask', 'apikey', get_string('apikey', 'aiprovider_openai'), ['size' => 75], ); $mform->addHelpButton('apikey', 'apikey', 'aiprovider_openai'); $mform->addRule('apikey', get_string('required'), 'required', null, 'client'); // Setting to store OpenAI organization ID. $mform->addElement( 'text', 'orgid', get_string('orgid', 'aiprovider_openai'), ['size' => 25], ); $mform->setType('orgid', PARAM_TEXT); $mform->addHelpButton('orgid', 'orgid', 'aiprovider_openai'); } /** * Hook listener for the Open AI action settings form. * * @param after_ai_action_settings_form_hook $hook The hook to add to config action settings. */ public static function set_model_form_definition_for_aiprovider_openai(after_ai_action_settings_form_hook $hook): void { if ($hook->plugin !== 'aiprovider_openai') { return; } $mform = $hook->mform; if (isset($mform->_elementIndex['modeltemplate'])) { $model = $mform->getElementValue('modeltemplate'); if (is_array($model)) { $model = $model[0]; } if ($model == 'custom') { $mform->addElement('header', 'modelsettingsheader', get_string('settings', 'aiprovider_openai')); $settingshelp = \html_writer::tag('p', get_string('settings_help', 'aiprovider_openai')); $mform->addElement('html', $settingshelp); $mform->addElement( 'textarea', 'modelextraparams', get_string('extraparams', 'aiprovider_openai'), ['rows' => 5, 'cols' => 20], ); $mform->setType('modelextraparams', PARAM_TEXT); $mform->addElement('static', 'modelextraparams_help', null, get_string('extraparams_help', 'aiprovider_openai')); } else { $targetmodel = helper::get_model_class($model); if ($targetmodel) { if ($targetmodel->has_model_settings()) { $mform->addElement('header', 'modelsettingsheader', get_string('settings', 'aiprovider_openai')); $settingshelp = \html_writer::tag('p', get_string('settings_help', 'aiprovider_openai')); $mform->addElement('html', $settingshelp); $targetmodel->add_model_settings($mform); } } } } } } PKY\9[Cclasses/helper.phpnu[. namespace aiprovider_openai; use core_ai\aimodel\base; /** * Helper class for the OpenAI provider. * * @package aiprovider_openai * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Get all model classes. * * @return array Array of model classes. */ public static function get_model_classes(): array { $models = []; $modelclasses = \core_component::get_component_classes_in_namespace('aiprovider_openai', 'aimodel'); foreach ($modelclasses as $class => $path) { if (!class_exists($class) || !is_a($class, base::class, true)) { throw new \coding_exception("Model class not valid: {$class}"); } $models[] = $class; } return $models; } /** * Get model class by name. * * @param string $modelname Model name. * @return base|null */ public static function get_model_class(string $modelname): ?base { foreach (static::get_model_classes() as $classname) { $model = new $classname(); if ($model->get_model_name() === $modelname) { return $model; } } return null; } } PKY\n classes/abstract_processor.phpnu[. namespace aiprovider_openai; use core\http_client; use core_ai\process_base; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Uri; use GuzzleHttp\RequestOptions; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; /** * Class process text generation. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class abstract_processor extends process_base { /** * Get the endpoint URI. * * @return UriInterface */ protected function get_endpoint(): UriInterface { return new Uri($this->provider->actionconfig[$this->action::class]['settings']['endpoint']); } /** * Get the name of the model to use. * * @return string */ protected function get_model(): string { return $this->provider->actionconfig[$this->action::class]['settings']['model']; } /** * Get the model settings. * * @return array */ protected function get_model_settings(): array { $settings = $this->provider->actionconfig[$this->action::class]['settings']; if (!empty($settings['modelextraparams'])) { // Custom model settings. $params = json_decode($settings['modelextraparams'], true); foreach ($params as $key => $param) { $settings[$key] = $param; } } // Unset unnecessary settings. unset( $settings['model'], $settings['endpoint'], $settings['systeminstruction'], $settings['providerid'], $settings['modelextraparams'], ); return $settings; } /** * Get the system instructions. * * @return string */ protected function get_system_instruction(): string { return $this->action::get_system_instruction(); } /** * Create the request object to send to the OpenAI API. * * This object contains all the required parameters for the request. * * * * @param string $userid The user id. * @return RequestInterface The request object to send to the OpenAI API. */ abstract protected function create_request_object( string $userid, ): RequestInterface; /** * Handle a successful response from the external AI api. * * @param ResponseInterface $response The response object. * @return array The response. */ abstract protected function handle_api_success(ResponseInterface $response): array; #[\Override] protected function query_ai_api(): array { $request = $this->create_request_object( userid: $this->provider->generate_userid($this->action->get_configuration('userid')), ); $request = $this->provider->add_authentication_headers($request); $client = \core\di::get(http_client::class); try { // Call the external AI service. $response = $client->send($request, [ 'base_uri' => $this->get_endpoint(), RequestOptions::HTTP_ERRORS => false, ]); } catch (RequestException $e) { // Handle any exceptions. return [ 'success' => false, 'errorcode' => $e->getCode(), 'errormessage' => $e->getMessage(), ]; } // Double-check the response codes, in case of a non 200 that didn't throw an error. $status = $response->getStatusCode(); if ($status === 200) { return $this->handle_api_success($response); } else { return $this->handle_api_error($response); } } /** * Handle an error from the external AI api. * * @param ResponseInterface $response The response object. * @return array The error response. */ protected function handle_api_error(ResponseInterface $response): array { $responsearr = [ 'success' => false, 'errorcode' => $response->getStatusCode(), ]; $status = $response->getStatusCode(); if ($status >= 500 && $status < 600) { $responsearr['errormessage'] = $response->getReasonPhrase(); } else { $bodyobj = json_decode($response->getBody()->getContents()); $responsearr['errormessage'] = $bodyobj->error->message; } return $responsearr; } } PKY\r0__classes/provider.phpnu[. namespace aiprovider_openai; use core_ai\form\action_settings_form; use Psr\Http\Message\RequestInterface; /** * Class provider. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider extends \core_ai\provider { /** * Get the list of actions that this provider supports. * * @return array An array of action class names. */ public static function get_action_list(): array { return [ \core_ai\aiactions\generate_text::class, \core_ai\aiactions\generate_image::class, \core_ai\aiactions\summarise_text::class, \core_ai\aiactions\explain_text::class, ]; } #[\Override] public function add_authentication_headers(RequestInterface $request): RequestInterface { if (isset($this->config['orgid'])) { return $request ->withAddedHeader('Authorization', "Bearer {$this->config['apikey']}") ->withAddedHeader('OpenAI-Organization', $this->config['orgid']); } else { return $request->withAddedHeader('Authorization', "Bearer {$this->config['apikey']}"); } } #[\Override] public static function get_action_settings( string $action, array $customdata = [], ): action_settings_form|bool { $actionname = substr($action, (strrpos($action, '\\') + 1)); $customdata['actionname'] = $actionname; $customdata['action'] = $action; if ($actionname === 'generate_text' || $actionname === 'summarise_text' || $actionname === 'explain_text') { return new form\action_generate_text_form(customdata: $customdata); } else if ($actionname === 'generate_image') { return new form\action_generate_image_form(customdata: $customdata); } return false; } #[\Override] public static function get_action_setting_defaults(string $action): array { $actionname = substr($action, (strrpos($action, '\\') + 1)); $customdata = [ 'actionname' => $actionname, 'action' => $action, 'providername' => 'aiprovider_openai', ]; if ($actionname === 'generate_text' || $actionname === 'summarise_text' || $actionname === 'explain_text') { $mform = new form\action_generate_text_form(customdata: $customdata); return $mform->get_defaults(); } else if ($actionname === 'generate_image') { $mform = new form\action_generate_image_form(customdata: $customdata); return $mform->get_defaults(); } return []; } /** * Check this provider has the minimal configuration to work. * * @return bool Return true if configured. */ public function is_provider_configured(): bool { return !empty($this->config['apikey']); } } PKY\cˑ classes/process_explain_text.phpnu[. namespace aiprovider_openai; /** * Class process text explanation. * * @package aiprovider_openai * @copyright 2024 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_explain_text extends process_generate_text { } PKY\ &classes/test/testcase_helper_trait.phpnu[. namespace aiprovider_openai\test; /** * Trait for test cases. * * @package aiprovider_openai * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait testcase_helper_trait { /** * Create the provider object. * * @param string $actionclass The action class to use. * @param array $actionconfig The action configuration to use. */ public function create_provider( string $actionclass, array $actionconfig = [], ): \core_ai\provider { $manager = \core\di::get(\core_ai\manager::class); $config = [ 'apikey' => '123', 'enableuserratelimit' => true, 'userratelimit' => 1, 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $defaultactionconfig = [ $actionclass => [ 'settings' => [ 'model' => 'gpt-4o', 'endpoint' => "https://api.openai.com/v1/chat/completions", ], ], ]; foreach ($actionconfig as $key => $value) { $defaultactionconfig[$actionclass]['settings'][$key] = $value; } $provider = $manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', config: $config, actionconfig: $defaultactionconfig, ); return $provider; } } PKY\$L; !classes/process_generate_text.phpnu[. namespace aiprovider_openai; use GuzzleHttp\Psr7\Request; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; /** * Class process text generation. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process_generate_text extends abstract_processor { #[\Override] protected function get_system_instruction(): string { return $this->provider->actionconfig[$this->action::class]['settings']['systeminstruction']; } #[\Override] protected function create_request_object(string $userid): RequestInterface { // Create the user object. $userobj = new \stdClass(); $userobj->role = 'user'; $userobj->content = $this->action->get_configuration('prompttext'); // Create the request object. $requestobj = new \stdClass(); $requestobj->model = $this->get_model(); $requestobj->user = $userid; // If there is a system string available, use it. $systeminstruction = $this->get_system_instruction(); if (!empty($systeminstruction)) { $systemobj = new \stdClass(); $systemobj->role = 'system'; $systemobj->content = $systeminstruction; $requestobj->messages = [$systemobj, $userobj]; } else { $requestobj->messages = [$userobj]; } // Append the extra model settings. $modelsettings = $this->get_model_settings(); foreach ($modelsettings as $setting => $value) { $requestobj->$setting = $value; } return new Request( method: 'POST', uri: '', headers: [ 'Content-Type' => 'application/json', ], body: json_encode($requestobj), ); } /** * Handle a successful response from the external AI api. * * @param ResponseInterface $response The response object. * @return array The response. */ protected function handle_api_success(ResponseInterface $response): array { $responsebody = $response->getBody(); $bodyobj = json_decode($responsebody->getContents()); return [ 'success' => true, 'id' => $bodyobj->id, 'fingerprint' => $bodyobj->system_fingerprint, 'generatedcontent' => $bodyobj->choices[0]->message->content, 'finishreason' => $bodyobj->choices[0]->finish_reason, 'prompttokens' => $bodyobj->usage->prompt_tokens, 'completiontokens' => $bodyobj->usage->completion_tokens, 'model' => $bodyobj->model ?? $this->get_model(), // Fallback to config model. ]; } } PKY\ۻ{((amd/src/modelchooser.jsnu[// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * AI provider model selection handler. * * @module aiprovider_openai/modelchooser * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const Selectors = { fields: { selector: '[data-modelchooser-field="selector"]', updateButton: '[data-modelchooser-field="updateButton"]', }, }; /** * Initialise the AI provider chooser. */ export const init = () => { const modelSelector = document.querySelector(Selectors.fields.selector); if (modelSelector) { modelSelector.addEventListener('change', e => { modelSelector.options[e.target.selectedIndex].selected = true; const form = e.target.closest('form'); const updateButton = form.querySelector(Selectors.fields.updateButton); updateButton.click(); }); } }; PKY\_iKK!amd/build/modelchooser.min.js.mapnu[{"version":3,"file":"modelchooser.min.js","sources":["../src/modelchooser.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/ //\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * AI provider model selection handler.\n *\n * @module aiprovider_openai/modelchooser\n * @copyright 2025 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst Selectors = {\n fields: {\n selector: '[data-modelchooser-field=\"selector\"]',\n updateButton: '[data-modelchooser-field=\"updateButton\"]',\n },\n};\n\n/**\n * Initialise the AI provider chooser.\n */\nexport const init = () => {\n const modelSelector = document.querySelector(Selectors.fields.selector);\n if (modelSelector) {\n modelSelector.addEventListener('change', e => {\n modelSelector.options[e.target.selectedIndex].selected = true;\n const form = e.target.closest('form');\n const updateButton = form.querySelector(Selectors.fields.updateButton);\n updateButton.click();\n });\n }\n};\n"],"names":["Selectors","selector","updateButton","modelSelector","document","querySelector","addEventListener","e","options","target","selectedIndex","selected","closest","click"],"mappings":";;;;;;;;MAsBMA,iBACM,CACJC,SAAU,uCACVC,aAAc,0DAOF,WACVC,cAAgBC,SAASC,cAAcL,iBAAiBC,UAC1DE,eACAA,cAAcG,iBAAiB,UAAUC,IACrCJ,cAAcK,QAAQD,EAAEE,OAAOC,eAAeC,UAAW,EAC5CJ,EAAEE,OAAOG,QAAQ,QACJP,cAAcL,iBAAiBE,cAC5CW"}PKY\PHUUamd/build/modelchooser.min.jsnu[define("aiprovider_openai/modelchooser",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * AI provider model selection handler. * * @module aiprovider_openai/modelchooser * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const Selectors_fields={selector:'[data-modelchooser-field="selector"]',updateButton:'[data-modelchooser-field="updateButton"]'};_exports.init=()=>{const modelSelector=document.querySelector(Selectors_fields.selector);modelSelector&&modelSelector.addEventListener("change",(e=>{modelSelector.options[e.target.selectedIndex].selected=!0;e.target.closest("form").querySelector(Selectors_fields.updateButton).click()}))}})); //# sourceMappingURL=modelchooser.min.js.mapPKY\u (tests/fixtures/text_request_success.jsonnu[{ "id":"chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo", "object":"chat.completion", "created":1721168885, "model":"gpt-4o-2024-05-13", "choices":[ { "index":0, "message":{ "role":"assistant", "content":"Sure, here is some sample text in various styles to suit different testing needs:\n\n### Formal Business Email\nSubject: Proposal for Quarterly Marketing Strategy Meeting\n\nDear Ms. Johnson,\n\nI hope this message finds you well.\n\nI am writing to propose a meeting to discuss our marketing strategy for the upcoming quarter. Given our recent performance metrics, I believe it is essential to realign our objectives and explore new opportunities for growth.\n\nCould we schedule a meeting next Tuesday at 2 PM? Please let me know your availability or suggest an alternative time that suits you.\n\nThank you for your attention to this matter. I look forward to your response.\n\nBest regards,\n\nJohn Smith \nMarketing Manager \nABC Corporation \njohn.smith@abccorp.com \n(123) 456-7890\n\n---\n\n### Casual Text Message\nHey Emma,\n\nJust a heads up, a few of us are planning to get together this Friday evening at Joe's Place. Would love for you to join us! Let me know if you're free.\n\nCheers, \nAlex\n\n---\n\n### Technical Documentation\n**Function: calculateAverage**\n\n**Description:** \nThis function takes an array of numerical values as input and returns the average of those values.\n\n**Parameters:** \n- `numbers` (Array of Float): The input array containing numerical values.\n\n**Returns:** \n- `Float`: The average of the input values.\n\n**Example:**\n```python\ndef calculateAverage(numbers):\n if not numbers:\n return 0\n return sum(numbers) / len(numbers)\n\n# Example usage\ndata = [10, 20, 30, 40, 50]\naverage = calculateAverage(data)\nprint(f\"The average is: {average}\")\n```\n\n---\n\n### Narrative Paragraph\nThe sun dipped below the horizon, casting a warm golden glow over the rolling hills. Sarah stood still, taking in the breathtaking view, her heart filled with a mix of serenity and longing. As the first stars began to twinkle in the twilight sky, she knew that this moment, fleeting and beautiful, would stay with her forever—the end of one journey and the quiet beginning of another.\n\n---\n\n### Product Description\n**Portable Bluetooth Speaker**\n\nExperience top-quality sound on the go with our Portable Bluetooth Speaker. Lightweight and compact, this device offers crystal clear audio and robust bass. Its long-lasting battery ensures up to 12 hours of uninterrupted music, making it perfect for outdoor adventures, parties, and travel. With easy Bluetooth connectivity, you can pair it with any device in seconds and enjoy your favorite tunes anywhere, anytime.\n\n**Features:**\n- Up to 12 hours of playtime\n- Superior sound quality with deep bass\n- Lightweight and portable design\n- Quick and easy Bluetooth pairing\n- Durable, sleek exterior\n\n---\n\nFeel free to adjust the text according to your specific testing needs!" }, "logprobs":null, "finish_reason":"stop" } ], "usage":{ "prompt_tokens":11, "completion_tokens":568, "total_tokens":579 }, "system_fingerprint":"fp_c4e5b6fa31" } PKY\]>6)tests/fixtures/image_request_success.jsonnu[{ "created": 1719140500, "data": [ { "revised_prompt": "An image that represents the concept of a 'test'. It could be a variety of things, such as a piece of paper with multiple-choice questions, a scientist in a lab conducting experiments, or a student at a desk studying for an upcoming exam. This image should use vivid colors and clear, detailed imagery to clearly represent the concept of 'test'.", "url": "https:\/\/oaidalleapiprodscus.blob.core.windows.net\/private\/org-EdXlYn9JmBUAo1tZ1QeRcOvg\/user-8GYNL27bMyzS3WcLtkUwREgd\/img-uPMRNi0R9lxPPJYOf4NZUmMY.png?st=2024-06-23T10%3A01%3A40Z&se=2024-06-23T12%3A01%3A40Z&sp=r&sv=2023-11-03&sr=b&rscd=inline&rsct=image\/png&skoid=6aaadede-4fb3-4698-a8f6-684d7786b067&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-06-23T00%3A13%3A04Z&ske=2024-06-24T00%3A13%3A04Z&sks=b&skv=2023-11-03&sig=bmZqUqQ2QLrmQFSGxTRQoKRVf4C6VyYCZ0aA4Y5%2BGCs%3D" } ] } PKY\:Dr44tests/fixtures/test.jpgnu[JFIFHHPhotoshop 3.08BIM(ICC_PROFILEappl scnrRGB XYZ acspAPPLappl-appl rXYZgXYZbXYZ0wtptDchadX,rTRCgTRCbTRCdesc=cprtAdscmXYZ tK>XYZ Zs&XYZ (W3XYZ Rsf32 B&lcurv3descCamera RGB ProfileCamera RGB Profilemluc enUS$esES,LdaDK4deDE,fiFI(frFU<itIT,rnlNL$noNO xptBR(JsvSE*jaJPkoKR2zhTW2zhCNKameran RGB-profiiliRGB-profil fr Kamera000 RGB 000000exOMvj_ RGB r_icϏPerfil RGB para CmaraRGB-kameraprofilRGB-Profil fr Kamerasvg: RGB cϏeNRGB-beskrivelse til KameraRGB-profiel CameratT| RGB \ |Perfil RGB de CmeraProfilo RGB FotocameraCamera RGB ProfileProfil RVB de l appareil-phototextCopyright 2003 Apple Computer Inc., all rights reserved.\ExifMM*  z(2i SONYDSC-H5HH2006:12:15 21:51:49"'0221  4 <D  L0100Ƞ  <% 2006:12:15 21:51:492006:12:15 21:51:49HCC }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?J0׌xǶAq_IݸhRzu^R|ϥ)E%}.]u]VMtˑ׮9Zk]_rjvO[StݖY7޷q$s\x\]Rz4OwNkk7fMI]]mC ~On WJRz=/߫oKmIFچQۧ^Wzw{G}UZl.g=+$Xtgӟߧ){]_6s^6oet䝝ᄋIO7~ΙGs뜞ו*z[qwzZ2k]'Vws-(<cϹ{Y$cmz5N7ݵgګ^S>\rx_댞5UgJھZ]N[~]/u{nxQ~x3n6Vufݬ~n^ݽnVx;rH<)5&嫌J͵q;iM^߫~Rz}=={+9M]K9iַ^n~oM=:) #>ӓۮO=knEtww5wV-4m~"C0z>g'%n].ZwvJWYkͶKZN/Ǟn\ɥ]wv+߫Oc}fV}SݢEsƢR+{՗~۱Wzn֫}^}7[_{3>x{þ&R<9xOmN٢I-'X4r i(yc IfGNsřSb`2bhJIF(u+TiFq9Zo1~l Gm|^ JǞ rJΊV48JJk7 *Oq4*atbԏ%Ju5JӍHEҦK[ >k>$.iͳl_ky~SIB QQJiKsu$߽]}O&o>uK] \ҮL+fQ C;c;Dn!H秈%.βIQM嘊jF']Ct{:ΕXҜ)%VNfW!|G.*L:<;bppY0W)ar):GVGO~~97;웺i7^׽ϯmWeݷ~r{܏ƪ/Y7~GFMߥmWg}ѧtֽׯ|`T..TUzK>8۳}RQqv9z~#3̽ӿWZHWZmm7Im[2yqׯ'>_^fٻWi^kn}w{-k{G=>\>-O;%v+zٴtzg֮s_+*4Zgmvܒ97'2{gw%+EO:H=_ҵoմZ4IDޝɡO|8OLg5&MnZoNm+[v}dܬvٷM]s$3g5$fTkivq묷׫O}UOyui.TgwrA]ުev֪tu7m;TZowT3^NMKZ4Z^VoevovMm8;q^FK 9ʡ9p2t_<]ƃ>g<%uzrdRr)~py7<-GRt b)QQOu)[>Glg=3_R~(%.Wmꖻk[hOZa2T3y'뎾e{^ۻ髶z?i}O[֯ox{1[I{'>#ڪ j:ͥ׃jHxi8&_bjK֎ox+ź|VYhiBpyQYkR,6tƜjܘ{6S_'79a2YÙL]Gsnu ͒s'ϯq뎆߼ݮvnɷf{kdw[+8i{Y]%;%}:׮zgWNVKg-'o[7q-n릋G]UMxsZr宊m-l[66`@ aSpp&6w[t^te:|U'JmFrfY7 bNk0Ζj0a!NxE>m#VEFmIҭ8TI#~ğ? ~[:G<1k^+x}>:Md4;[?,wn㲎Kq0\Lp`>ar<%:ASp:6Jq;+'d9Jt ucgEZ1yTRU+VUӼ;i^][ZxoKlu ^y 5Ksw5̎m dU!XQOd1l$|{}jpJ~zʔWS%%QZZ7|.&9VJ3Z!N5SR|mZ|WZxK[|a֡B狋 PZ_UR7&/<|{ˬO|%˻ML$\iv5 [ oi5;%E ^%`p\.G *3 N2u9ISVu$՜cExm᷇*?ԮJjvIBT^rr.iӡ-Bxs"bs̿88iьߕ7x'7ygipxD-Qk|7=GHAXyt'jcjJ kBrx7f?%Z\׷&ו,O*RB3Ԧ8Jj\N2Nx[YJ^K|Q4ءKY|1yZC3M$,ţw;xTwG[aڒIꨫrƜe~[;֩Rb^eXFc^SJͻ:jfJR Ks۷?=vZ%;woVW\muw^zTls=98&+Je{ZWf}:TZoz&sO_N{sVi[oT[}oE|B $qׯ$_T'd\tnV9o]u(Ȝ~v?nqQdVu>n߮Zj{zSrI3tCVһ4Ēkgu妶޻_KtGc&xg<^T]}VMidkNdZ}mi'M[#ߟϷA듼ey(ZOD";]ݮwս{3{r{όN{i֖v׮ɾw{roٹ^v5`c=220zO|yϧW{h螶I'g}ꮷI+.N rǷ?wj]_K>+v[Tߣ׫Xط^;r:'193霜n/K]ikm-/Қn~׮HO׶8?r;s4I<_mZoњ0}gǯ8y6oۯ3j׻սۏ{joUkւ:H|nwM={vto}oM4BPn.YTI۽_{KY4{]7wmn-};կ{j+W׺ݿF~~Ǿ@xn.F#IKxv.ncGu$mYjK*gJ  r59^#NvJf rOjQabjuToſkJ4oo?O~h)xukγ2IL`.xUBdm|*"URמ#Ӫ5:MJtU7<,۟RUJtUXRw^h(_c𖥩x?ç԰KЙ/o#>KG<^@ &TbXE3(bqP*uSVOޤڝԧ')p. ,˪dZN35U\P-RTzmGԧ)Po?^ O&se/y•l. =FH䌲lX)ԥON>dJZ+I]%քpbS2$*2dcpW)CjSUNHJ_)/|I}f:Rׅ\[=J643D]-˼]H.ZQ=?>b㱳氫 u|JR8RvT԰s:yJP/q>&ao 'dl8PUTO&'YA(ыjZ޹xzf[ZlF>&V@f㽷KI u8wpYJU'ZWT+TחNYJ0<*]o:&iS*~մM'Qһ\iuw[\g|H]v'o>r26ؤ94 4.,TinE*)lDSe&NQ3ZJR#p*|Jʜ);]UT+oEM~|m7ƞ) u˭>kbɥi3dm{w1_Ξ3d9v KkNqMF))^? ɸPêq*2#QjR|wIQYiBqȏlp1m׫vֽwWk=z{^<׾G^s'SwwNٽn;ۮkmCX sluߎhfVW6XMZz-}ZO_Ϲ8~r8皻m-J~-uf֪]-˦x$gӷ'T8ٸz;e'w}D+On{~y LzG{5woVO":T.(:cze]W7w[+%k4~ofvûN8<}s涋Y>s;ڋq]O_zmuO^ǁ^i;?zQ뙫ʹҺ\k[Wߚn~Wޫ\}9# u㩮I=mz5ӺڻMZukݽtKdӲ郎~3=lkeQKOUgӪ^f Ӄ~{jT%dVմwmr'{N]Z.@}G|wN+U{&uhu}^+m}=zG~7-:7t^_Vni[]Z[GuiTC>~(uM'AwkwXZ#ⳆI 6 #iU/|%^5xp \Y C4̳,4֯2\\~1ahՒLUxTisFJU#JQ=K xJ[S0YvW:Qe0V AV{ZI9ƟRdy~~#|dįχum'ƞ0\؝n-JmRhn\ ۳_WopF?V78ɰRq&ccNX\=\^59BK FMG_W5xge9eGjPȲܺjrQ<^2?sYBT?zHO==J=sTj~kh k1]i FHgȩ{Bo|=\= ZjiVU%8*jqnӜR+vY,CBj0SQP+ 8E*I^W=^; x:2w.B_ ɴeFU;}68OC)0שÞCc0Xl21f"NL®*3^rTzRdş<\Cqx,;.EdE*1ҡ8B:&[R%A|#gZχ>zm>)^GB{Yo LF:5(jԌ(Iڝ8˗].2G̅n6nV e14y!|9ө<-h{TaQo~ԣ=UJ92OE-O0.HCQӭz\m PkQ J-EP_|BsƝ}|J jLzy0-<4Z}wq\?gRqn*0`v#Stjnlno%XSsӥ˫hqiŸXJܖ; ?xU +^]&-NUӮ-4ZWrٕӭ .<5<hFU(SehԩWRITNQZm.^_1ƞ38L,7ibsU၅<]?mBdF.ubQ(Ν8Sk?%<=!kN!mWp)B6Ҋoh{yn?"`j\kvv?@J=z{W]zg$gf?;<ϚV+]WgEuͫUg'Qq;9+NTWiwo}tmkk=_E 8fZr(⣯Emuenaikh"9YcJw~imx+[tm٧\Νs98y)MGF׾kͽk$ڳk{w2fOS~C 팴ZWٵo[f=7}V4\ûNz{ Qnҍ-NJR[;5Jɴj僚hZr6@mQn8^W>Xt/nNM4zjݝn{{-;vi/;ji׿UZJi}7뻑t}9V(]unW]WokݛKMoہGN_Ǟ2MmyVu*;پ~^ɚуǨz{z,m]}tM+^dkmmk-;-n_zuQ=pzzVM-vܕwd޷^Gqm[꼴=?^ޭjjXuڣ8C(YP7cvW&Hq nsS8-d*帚fj:ʞ/-NoGcx/wq3Ͱi9a:8Ye+*< ђ-w=|^Z[3Hm-&Ia|aMnkOKlo(+u/ A,MD+oϣ,wfj\Wr3ݥ̥e/ŅGNq }<'§kkV޸7TqHe+O"Ws x{S1am8FNzUo|SI;.NWK8W!VU2\PshQXMFK:c?W/,i%[/K_Je]6ohBm3MԦnZY)6 a3!8f]B/3δp֔iW:pҚpº=ڃj p<&LaRk-/c 5j1URjQC5?^Q$5Z[}nO[$&xYm.#%ِ#m<\nǽV%R:7*:}9ƥK[ZPZYtiWJxR% ~8QQj ijݮʿֿxϏuVo2džu|{HbѴ-ئi`Hig3|?;ve᧖19NqSXZ5=&3 JJ/iRi9wo c62yKSXK .*hN)*qI]A9Y]0C[Sv\\Wq,͖?4*GT2< <5 .rݔȧ'dڒI.g+bkIS%)UJVqvQmI>Uꟶŝ;_|E߄t0既x_M=gqv-YZ%v0"%Gqaٜ*nؼVy&ؚ:r:pqU*Q})Ÿns~i!Vx 6&y~?uΥYPaaΥiRԕ9Jyf׌2źw5@dh|MKJW{ KDPcTϸ:pYq^UߝR^uKW{-)p pCd9F)EIR_ Nwԯ9ރ??ikPmuM^+{.'9LRq.Xܜ ~c_x,JUZ5z۩TrQwm$l r;!V )*8j80>o87ͤ윹ck~oư[ļ gj ^3VܛII3Oۻ^W?t*Z^{n޷~}znzt>KGTi4bwھ}DNN{~4׍z;>^׻wѳN5ggmV`A翯׽Ҿm]m4ֳ{Nv#+dGSWz,uV얝vm67oN]֯{}{Rx I?*\dҎJfݞO[M5{jշlc$;Ǟ;RZjItMwĵѯ=_U]Ii:OQsz{gv]㢿5յuսQ+kYe{-ݴ՝mBu['{ŸZk]ۭw7ufm8 v=ǿnt=s=$I׽][w%{'k﻾Wǎ?SӞO8pNVV֩|ZZjv.6N]Vy/47m`pqL{cfVvVVwV_N]˯OO;u׾8}z\vwV~_>u*|-][[}kqnmnKUi=WpIo'v^nvVPx>s\wj)$ҺR+6i5fMD~mv;릧ſ΁yqn!6%@s_xm[O>n:iY_Z\ƍJ U(MӼ,e/ǵϑio(牵 @?\xt;ָ?i\xvt&or.o O~-?x`|2p|gY. <*5jcFΌ0n\7?$!Z? ~|OX+fY6AY"\Ga2hdXn#K aZn5'<- |#ƾ+-N]:k@R^}ޫ}lɴ ye3Mq2ĘӖ`qNB1*j5:ժ*RiՍ1Q.Y=*yb+VԄZ*JNNTZ*N3a|i}akV:: Il.AC5 īe.eƘOW p4+הo*.N5jQg.k{ g$OPFPnpܹg|M?uK8_xGJJ[Z?Er.q y$kv"kyaQץXڸyM帪z]x(r.umSZZEÞƯSi*xxT9i질w.i'eJsɻH~ʫsG5~mxxOw&mGa.:Mʺ|P6P"(xXz?XU OoWQTڪ1U)ʝIxW5UQUG,=:=)ʕ:O 9^4Zom 6{ⷷ˔/įm4Cͤ h`nRy1~,1IiRҎ3kGYV~? xl9(JKv tOt;>;5'>0t[ķW!ӯlnmN r!HoGfq)ن/B7OJW('XP?q==.Lܟ&c~w^EO#(ԯ>zRQnKZL: ma|{jj:׌tK Q,Ǜk{z&9-$ID &'U)Uj +Nq0N)~MER`K5ХVʔqJ57OQU.J [_^w{W^SuCWZl^$uV҃ah:12;ISY PiC2v9#E673nO)mC1VjFeS>TFuRQ|ǯ^.ie{UzҾMӧq׮Gr8=yx/[.[&fuu$Bm#}?:ryWmn֛{=[;6=}LǷL`>96}/t[rM.M|ҿm/g8vn.EմagTնM >p2Nszӊwm{i~ȒgmV4z^nfJ?A7il|W&K4WT\{q>ܚRM2wz&~+]5$+k[~p` ^׊3K;^Wyފe5mזo8$l{^zU.vޭwzw7nNIqӿ5+6+Jmn)J^'~oqO;>'kJ;i;tݷ-E{k쓹.q#?3zVR ]lgeo[}u-}zz5]5b;O\x Dzǃ5ַQ|*nlZ2MjqfH`7%gu5%W3ԧ&]7Q啚V|oX Lh:nTùu (F|nݚ ~ƾ QMN}Z=&}[wҭ"uM>kˋxe3<5*P8Ix<:dydUqp[:9BS{ӣJ5)U{JkSQr?,,1&-ݼdJm 8z*ӋrjZN;)ihݵOh..߱#0r3q_(Q11չv8\%+^~k_|&Rm9e#)_W'J{'eAtM3c^EGO < M_kl,六 _5xY+]߭촻֚݊_^_| _ |?%WW\\4fV`qWz]} <V䋩Qʤ˫oOz~h֟ KZ왤> ^!ž#ͺN>%[K}^ Os_'3'縘br< N2*񋜰}X{ZUn$ڕJ0QRj?&rr>"&2:TqX]YhS&4rrڟ:⍎tZ [_^yڂ2lRHrBj9CZpi!`cZzj4⮣&S)7wk5%Mӧ,3UgNT7ng%vNO_Y x a&s$o*^IK!RW-WR-~Ζl+Z׵aj~Wa};g$ 5ů;a fv;,卤w*Hʦ:R_eZKYȽ2#'q.fQ–Fi>w(M򤠢*s༟:/3խ[JZ SR\%75)TSI%X'Ŀh< Zn}GԵ}Jᮦy U4cKu4q!Kğѕl,o\c!b*Unq9GF:JGXq9YU''C JJS[ *xz_,UY\)NSwjG:EѾ+_~$EnDŽ{o>,_Brh߈mXh$<Ejh)e?2d ѼN2I{RrѷZ崲̒#MSb ZI;bqYA.ZQ}?R7`y4I]O_eK壺n(;ݻ_K4l:^6~>i;^_kZ>Vw1oC?'=^[zѫۭ[Ӯz ׌\Q7;^+Vmm_7Kj n-}߳G8}i;ͻnvY^W݃z]j_zkf*L~Nj1FJM]uK[ꝝ[ u뭯Odrs'=N?LUgk6Mݾ]w뾩ݓwow{˸גzqc565[4wV+N^?wr3q˯5uݥm{ݶvu}]8k<; _{UnwQgY6dmݬktݾm}3?\/I'tޭk${_i{isv\ۯY4_.[2ZWmvV宺sN봝MM%z=Kmukgօ#㯮:ckIխܭw/@4^g=6ƬR8<TӵMw{.[ܤ;ٷn7~ksӌ{=Mc$)-UیZM-EФۺw~vv?|7XKIk0rqyW%<:xTNOoGkVʤdq4浾ot={E?|+4MJ/? ѵ;6^ 04@4_φ#T_yjꤠzT_6{虜JdArN~M˕MWBJzYo:O d\gyh>u9NP-u"Osf$,\, p2u(ԎRՠkJEIS6XRged7A^08GIՕ9Jxi:mZSu%MϚjTIqNgCb Ab`>Rvz'$JnѴ]5jkv&)VN3473ITOYE)u咳Z/e'/ݦeWW^G W)i{>cN{I#M7ŶzE+F,82lҥ')U˪(8ݾJ5a nSRKOy^/F" 2ӍJkENUV3Œ]Drj%|7g|g?WS ƞ3tCfs_.Fw柫i׺F-\qXIŇlb+ԡORIFtQnIsiv?܎0_G,~Wu,Zsνt(b0/OPJ5C.dޓ FKoŖگojmWIxSK3V1YvBI j:()JJr`Þa TA2QPT\R|39vEp Ux(SskVa5a?m=Lg,jWQjU%cۚ fx'׉OxWZuF.a_.\1,v (F!h6*VTaMkXz^qU֯VUq2F*GNITܛ# MjJ u=@+ןWMJVmGmu뵬ZW~Fݳ#q!T;3=j[;WnSj=9_j~=޷麿WWv֮M-7MMx[|2?CYIgmdZZϽyZZڽH['2rn_59YiMy edM_Gz_SENq{~GYGwtq|ϵ]";i˽wE<G$2J= ~=x<թrJI7{]o꺻[++=[GwE?O(W™4m: oQtDv>d.g|!A q1!WjTΩ>8!ҡGFPg&ײ-ɾGv?L1 ~J^ksbr}^pDB9lh3C1躏|e/iVwm]S_dq0-ZVmGs%6҉&r"9Ĩg&2],V oQ|?"^>¤(OjEJvl#С,pUX*bI3z5)S\JMՃsIma1i6V u 7F6.n$%G*YVW4Mi*+?<1wþ PVMnYԕ'4z;'gO|j~ Ǐ4 Ş+RG-oYcYa|U%#$C)x<Vڦ6Ɵ(n[OyL ֽ9VHzj5~2|Iٯ!xi:!U|(# Mʜj~\ɏo\(5'k]+Ͷb-m Ss4aeExgԥOь~*rsPqj1V?,iE9RjgWڪvӑ9JqIy+6 Ӳ7ſ x~/ӊ|]? V2!xi7IqejwRgq6at?^ӡ^*NpMFR=9Sϼ~$|5y6~fYqYЯJj1yF9d)SViL`|:xw_2MjPZ< TJHNv>$3.#*y[b8j|ю' Ztkre|Z3,s噾[Q/̰8\' K N'(JIS8Owfz7KMltN{ub9?^S2Wo[KD;^~݃V蝹ߥ^;?\*ҿ4t]>z]l[_;o} u8ޡMhO߯Qj}߿[=o-#v'OոK+y]~-mmߩ<җ,nm$kz'n{j[mH$ϡV}ϲVԶ-F익l͞^r}5 J+}Ok[ݚ9fzn__-b.\OqׁN}kUJ.Wi4~ֻjWuջi~[h\۩=;j\uwtwM,m$ޖ\׾W}{ytvOl?NJ]r+].fәGumrNd+QwVki6ַ.ms-S]֚>ڻY} |y?Ѷ벴tQgm{-fN1p{mJVvJ_յ]<5cn2ZƥKTm-Q{kuDҵ}]NƔmgz<%ʜW[I>Jm'm_}[z{3C1K=;_M]Sz'l{v޾D`־xwcVu_ Oi[x.5j ikg$W2XܻLSi'͸W8٦/eyCRTxk)B%ZM.#7s,>[S/PƬV]:_+TS2?3p/Vɩx>XE 9]bY^Mix X,/Q l4mO3)a<(St#P)Q j7QNtԥN0l7XTj՝]Ξ"jNYrqRWJZQ拌?9>;X|A#Am+7wsDs B[G|#K|q>QKf(, Bb4… ܭ^+RjG _8ƌ18lYj3 EJ/ my_+FN骉/߅֋5|<mG9tM_UMwT_zޟ]hWsLuHfhoh1\ۍq1p .{<. K SWqQu*_v ӊ+Wԭ]b3o*T _. 94%/u+O Z鷗߆ӛ]W񵷈u]RHi|A ψ,VHos#)<ʅ&'ujP)}K8a$ΝWW[K)Ud8¬MK4hXD!z0.aQJIũa>x<ҙ_fҮxO.<3iR?8\˧:n ҭmpVγY$~'8,VIZ3W]owok{u o~m;_vM{ٽS^{okiO}]_C?[z?My5&mRD/Vߓm}-?=ݒoY$Ųntzw]nץ?ϡQA=s=S>j髧fSv{Mٻ7]ov֚|%nu~[vkkvZ=[ktխ}x?F[WM8Yחg֛jՙ{u3gnQ>K'[-F3Z/[ﻵpy<׿ӞFR-ztMi[w}tk;+}U<B5wzy}l}}e7ުZo{&tI'>?ǜp/򿔖}W]ߝwmn{]MwFp϶yoL:.hdV]}5{ٍ}M+$iin[9$coӹׯa+;]NGʕmt_o$ <=vwOwmKTw}nS{[^W}uas}zssYN.ҺWM9z5gvRZګ]J*Ǽ3^$p2yWVU^p|*ZRK)νǞ5| j&~!uմ]*x5H.aצ?+Lտ#pٍ>y`kLa:Н* N8YU(PMe$f{9.qRɲq,ҾaK*f.P15*B\h(R(?n-cTxsĒOyE]z4XS xcE1RB:4M~N.UcVφb*'l%Iw,ci)SN>kbbqN'HbVe^TIs4rs/ RҼ_xTKRk :O}29нܷs(>7gEl> kO,Kt}Nl5e:*1TQM*uTh”=GxG"'yfXcn":XrC УZ5*)?=~A%=uGZ=miɴc=>ݖOhzݮ-_>}:<}3EnkjN$礷ko}5 'ۦ9[;dջi~gշ|^~^wԤx;Y4w{ڶ6t+[Zտ4PI>s'ѧ]Y;_^utۿ_k[Zuʝn?'ky?7֝SzKM^wٽfEw=Ok0\۴mKevmw9Zom;kԇ s׷o\cjwrjVݭͻ_дٵh]u{I8r8}ANҩE5n}vM>[w}[Mo}5lퟧF}u5%w'Y۷M,jE/]t-{.Mt= sgׯ~v%keJN[Gugv`ivkhWvvj[=u{n?qg8XI^J;h.g_ }5UцLS܂ U٫=}笥-v孑=]vݴwjDtOS׎=W3ZMY]-z]2W{ۣk"Ԡ{ o 4m,QH37{{ai`<+!rer~_Jp¦bƜN|ъ|ғm.V '6VXj կ^g`{j1iK97m9F_e(fOYx:׆+sZ?H^""q@tazY~xK=yR8(W0mNzTJNqp Fw>]Q G7x"u2ʝSWqgRXsR#Z2Z*7jpHNmdO:>5ާqI>PGe9/,5-rnK [9߄y ) XU}S5\𲕗R4k26ibrϞ; },NvixD#unm/"xw>1< MK~$3a1Ue_ iPnbxzP]I;~~4x3oowɭxCSӼWZnM+Zam yEҾ3FXlZ t䪺Pvn)&nT>[4UVxnʤrJ.RK[#"7!y1ut01 #yD9BtWiS唡MSkKNNm^.^e,LSIïk5QUl5u6)F_i'ykkm-yޫ]oVZze9}=*-+vݕ4nvdݷ}yА:dB:m.}>W7f߯K[w7NO\.$S oIi;W]o6;%]z-.Vz@'6z|z'%kךV['WUmmerr9_+JmڶiZk{.in=^+1mjWnܱ)IV׮O߲GoGg?z߄ .MgR&{wCoM"GU̱`rAu+*:թ9e-e^sY4?q_H a)ƏaT!?b̧/ig|,m)STz;z b_|2Cp|`e=a],X՞]`ֽ_2䖊=yKr?ۉyi WVN-)] !|"ӗx9'<K#iC.q\l],. evҌ_[s=}Zq!}%5sQ\JYF9_{;<>mkI__u9r-8~|N}9?E~+vJ2(|-s/>M{tA/׆{.#mrW?bێ~7n@:c Nk7~3~ kGcÃ><'E'w,gVw{P{vwZI *5&SmOM]kYL?L_)3Bao3q0 x @סH$fbE53 >I_"xgVUie{J]4pf?IRΛ"yz~g:V%k)E|g[ yaoϾ eǟ61q;{jNp q9'L>KJ̧)˒WT!Uj۩)I;~_5*.JRX;ܥbiSW%>ߴp}ygf)cGRAѦerCY@;0焋WIBooʒzZ}Z>34;,-or *YlwgF~ÿEj߳7)nqgz#}|*0O9[ρ9YfiO8ii9=-mO"aw m%ٵe/_߂A~m?~YA)%Aneɇ [Kx#0m )^Ŵ˖sy6# }ef+mj_q !dy&0_%R+ ^62Vana_EŦVW\~d݉CAivÝX[]u-ݣ%;K[7+#p[sI5lZqk5-%$RJmo͏kMWJtChhnMMm8Կ?.X| 74l#x=,b, ᑀ_-yu8Sʦ 6{EնIu Fs+?wu{ݽ>!j~~jxSc9REmS, +h-$.>VA*)#zS7_8v[fR%kg?(ZI'fY_% NQ}לV>|G(I]+y+ u/,Ie,IHi$rű )v H|蛞bkp+Ԟ\LeF̩eKIͮ[kϤ):2)SֆM]Bxs|mlH_P>?xP?}A'F3<_)\Tܟ[=ycջ=ӿ&éjGVeG:Y\g;+ H¤<{B\fK7eݛeUA˿~c~(kV:VW&G§>NA9Ӯz/Wd.M٫^zk&Í_it]WNO&7X㑟}19X;;ĭ4q:tW{ns2%-}Gw%41ivcu@"! #E[{ޏ>앓v ܘl)BP?5N9ԽJpR3Te62M$Zikm'ÅC})2m{,˵A T^zJ0ZWc+9%SmdKTkHe(aܝI*RS(Ԝ5bI{Wz;kΝ7Ѭz-rOE?f g we$,$liyZg{*/hyA/ze۷W(>$ۚ&qu=O/Nӫ Utۚ){B]G=7e~҅ѿ-}?XkjV{?i ?6u>oZ~ΧO ?-i Z7_m3_;G_(iZ~BkG L=}9vbuKM tJߪ]WgΒx:r/rrzc<<ոO'߯/mE/b޺6姖OdeOٶO=E׍4meYKR_*9k<c*i:34TI9]ҭ\Һ]߃MBۓT.ǥiu,L}hW,d.<϶}ggew),pX,^׃)rIMVWWMzr񰬢IEbĜY*/JWm!X>3\֨3k0,<|lX3DB!a|rC Ve;(+I9SN?{k;.*n.ғ(KU9e (ErZ{:i5%%fQĽ*=-\\y ]~#sPm* `wUN7nw+EsYZXVo IBNU$e*M-R CGχ4l+1kU|Ći$:z5{VJumZ(Nx|Vت1˳{4-9IßNxHxd|#_[Y=^X?u/.]~Zd? |g͙$,eu{N뾤SnWfݷw "u7uc֣>ؒ[4_~U{j8mj~AO}K ? Wt?>swEdX^U'!J-}??ˠׇߋI9!Ofxߞg৆ ռTH/h7~,,9M/W3Gh pZYb7g}[ƶމ]Ho9q-mwŶo[׮_Jwo'tmw#?,1A. namespace aiprovider_openai; use aiprovider_openai\test\testcase_helper_trait; use core_ai\aiactions\base; use core_ai\provider; use GuzzleHttp\Psr7\Response; /** * Test Generate text provider class for OpenAI provider methods. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \aiprovider_openai\provider * @covers \aiprovider_openai\process_generate_text * @covers \aiprovider_openai\abstract_processor */ final class process_generate_text_test extends \advanced_testcase { use testcase_helper_trait; /** @var string A successful response in JSON format. */ protected string $responsebodyjson; /** @var \core_ai\manager */ private $manager; /** @var provider The provider that will process the action. */ protected provider $provider; /** @var base The action to process. */ protected base $action; /** * Set up the test. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Load a response body from a file. $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_openai', 'text_request_success.json')); $this->manager = \core\di::get(\core_ai\manager::class); $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\generate_text::class, actionconfig: [ 'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'), ], ); $this->create_action(); } /** * Create the action object. * @param int $userid The user id to use in the action. */ private function create_action(int $userid = 1): void { $this->action = new \core_ai\aiactions\generate_text( contextid: 1, userid: $userid, prompttext: 'This is a test prompt', ); } /** * Test create_request_object */ public function test_create_request_object(): void { $processor = new process_generate_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('This is a test prompt', $body->messages[1]->content); $this->assertEquals('user', $body->messages[1]->role); } /** * Test create_request_object with extra model settings. */ public function test_create_request_object_with_model_settings(): void { $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\generate_text::class, actionconfig: [ 'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'), 'temperature' => '0.5', 'max_completion_tokens' => '100', ], ); $processor = new process_generate_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('gpt-4o', $body->model); $this->assertEquals('0.5', $body->temperature); $this->assertEquals('100', $body->max_completion_tokens); $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\generate_text::class, actionconfig: [ 'model' => 'my-custom-gpt', 'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'), 'modelextraparams' => '{"temperature": 0.5,"max_completion_tokens": 100}', ], ); $processor = new process_generate_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('my-custom-gpt', $body->model); $this->assertEquals('0.5', $body->temperature); $this->assertEquals('100', $body->max_completion_tokens); } /** * Test the API error response handler method. */ public function test_handle_api_error(): void { $responses = [ 500 => new Response(500, ['Content-Type' => 'application/json']), 503 => new Response(503, ['Content-Type' => 'application/json']), 401 => new Response( 401, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Invalid Authentication']]), ), 404 => new Response( 404, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'You must be a member of an organization to use the API']]), ), 429 => new Response( 429, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Rate limit reached for requests']]), ), ]; $processor = new process_generate_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_error'); foreach ($responses as $status => $response) { $result = $method->invoke($processor, $response); $this->assertEquals($status, $result['errorcode']); if ($status == 500) { $this->assertEquals('Internal Server Error', $result['errormessage']); } else if ($status == 503) { $this->assertEquals('Service Unavailable', $result['errormessage']); } else { $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']); } } } /** * Test the API success response handler method. */ public function test_handle_api_success(): void { $response = new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, ); // We're testing a private method, so we need to setup reflector magic. $processor = new process_generate_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_success'); $result = $method->invoke($processor, $response); $this->assertTrue($result['success']); $this->assertEquals('chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']); $this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']); $this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']); $this->assertEquals('stop', $result['finishreason']); $this->assertEquals('11', $result['prompttokens']); $this->assertEquals('568', $result['completiontokens']); $this->assertEquals('gpt-4o-2024-05-13', $result['model']); } /** * Test query_ai_api for a successful call. */ public function test_query_ai_api_success(): void { // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'query_ai_api'); $result = $method->invoke($processor); $this->assertTrue($result['success']); $this->assertEquals('chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']); $this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']); $this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']); $this->assertEquals('stop', $result['finishreason']); $this->assertEquals('11', $result['prompttokens']); $this->assertEquals('568', $result['completiontokens']); $this->assertEquals('gpt-4o-2024-05-13', $result['model']); } /** * Test prepare_response success. */ public function test_prepare_response_success(): void { $processor = new process_generate_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => true, 'id' => 'chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', 'fingerprint' => 'fp_c4e5b6fa31', 'generatedcontent' => 'Sure, here is some sample text', 'finishreason' => 'stop', 'prompttokens' => '11', 'completiontokens' => '568', 'model' => 'gpt-4o', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('generate_text', $result->get_actionname()); $this->assertEquals($response['success'], $result->get_success()); $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']); $this->assertEquals($response['model'], $result->get_response_data()['model']); } /** * Test prepare_response error. */ public function test_prepare_response_error(): void { $processor = new process_generate_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => false, 'errorcode' => 500, 'errormessage' => 'Internal server error.', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('generate_text', $result->get_actionname()); $this->assertEquals($response['errorcode'], $result->get_errorcode()); $this->assertEquals($response['errormessage'], $result->get_errormessage()); } /** * Test process method. */ public function test_process(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('generate_text', $result->get_actionname()); } /** * Test process method with error. */ public function test_process_error(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from OpenAI. $mock->append(new Response( 401, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Invalid Authentication']]), )); $processor = new process_generate_text($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('generate_text', $result->get_actionname()); $this->assertEquals(401, $result->get_errorcode()); $this->assertEquals('Invalid Authentication', $result->get_errormessage()); } /** * Test process method with user rate limiter. */ public function test_process_with_user_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the user rate limiter. $config = [ 'apikey' => '123', 'enableuserratelimit' => true, 'userratelimit' => 1, ]; $provider = $this->manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', config: $config, actionconfig: [ \core_ai\aiactions\generate_text::class => [ 'settings' => [ 'model' => 'gpt-4o', 'endpoint' => "https://api.openai.com/v1/chat/completions", 'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'), ], ], ], ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Case 1: User rate limit has not been reached. $this->create_action($user1->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($this->provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: User rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('User rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: User rate limit has not been reached for a different user. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 4: Time window has passed, user rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->provider = $this->create_provider(\core_ai\aiactions\generate_text::class); $this->create_action($user1->id); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } /** * Test process method with global rate limiter. */ public function test_process_with_global_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the global rate limiter. $config = [ 'apikey' => '123', 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $provider = $this->manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', config: $config, actionconfig: [ \core_ai\aiactions\generate_text::class => [ 'settings' => [ 'model' => 'gpt-4o', 'endpoint' => "https://api.openai.com/v1/chat/completions", 'systeminstruction' => get_string('action_generate_text_instruction', 'core_ai'), ], ], ], ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Case 1: Global rate limit has not been reached. $this->create_action($user1->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: Global rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('Global rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: Global rate limit has been reached for a different user too. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertFalse($result->get_success()); // Case 4: Time window has passed, global rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->provider = $this->create_provider(\core_ai\aiactions\generate_text::class); $this->create_action($user1->id); $processor = new process_generate_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } } PKY\ll%tests/process_generate_image_test.phpnu[. namespace aiprovider_openai; use aiprovider_openai\test\testcase_helper_trait; use core_ai\aiactions\base; use core_ai\provider; use GuzzleHttp\Psr7\Response; /** * Test response_base OpenAI provider methods. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \aiprovider_openai\provider * @covers \aiprovider_openai\process_generate_image * @covers \aiprovider_openai\abstract_processor */ final class process_generate_image_test extends \advanced_testcase { use testcase_helper_trait; /** @var string A successful response in JSON format. */ protected string $responsebodyjson; /** @var \core_ai\manager */ private $manager; /** @var provider The provider that will process the action. */ protected provider $provider; /** @var base The action to process. */ protected base $action; /** * Set up the test. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Load a response body from a file. $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_openai', 'image_request_success.json')); $this->manager = \core\di::get(\core_ai\manager::class); $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\generate_image::class, actionconfig: [ 'model' => 'dall-e-3', ], ); $this->create_action(); } /** * Create the action object. * @param int $userid The user id to use in the action. */ private function create_action(int $userid = 1): void { $this->action = new \core_ai\aiactions\generate_image( contextid: 1, userid: $userid, prompttext: 'This is a test prompt', quality: 'hd', aspectratio: 'square', numimages: 1, style: 'vivid', ); } /** * Test calculate_size. */ public function test_calculate_size(): void { $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'calculate_size'); $ratio = 'square'; $size = $method->invoke($processor, $ratio); $this->assertEquals('1024x1024', $size); $ratio = 'portrait'; $size = $method->invoke($processor, $ratio); $this->assertEquals('1024x1792', $size); $ratio = 'landscape'; $size = $method->invoke($processor, $ratio); $this->assertEquals('1792x1024', $size); } /** * Test create_request_object */ public function test_create_request_object(): void { $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $requestdata = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('This is a test prompt', $requestdata->prompt); $this->assertEquals('dall-e-3', $requestdata->model); $this->assertEquals('1', $requestdata->n); $this->assertEquals('hd', $requestdata->quality); $this->assertEquals('url', $requestdata->response_format); $this->assertEquals('1024x1024', $requestdata->size); } /** * Test create_request_object with extra model settings. */ public function test_create_request_object_with_model_settings(): void { $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\generate_image::class, actionconfig: [ 'model' => 'dall-e-3', 'temperature' => '0.5', 'max_completion_tokens' => '100', ], ); $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('dall-e-3', $body->model); $this->assertEquals('0.5', $body->temperature); $this->assertEquals('100', $body->max_completion_tokens); $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\generate_image::class, actionconfig: [ 'model' => 'my-custom-gpt', 'modelextraparams' => '{"temperature": 0.5,"max_completion_tokens": 100}', ], ); $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('my-custom-gpt', $body->model); $this->assertEquals('0.5', $body->temperature); $this->assertEquals('100', $body->max_completion_tokens); } /** * Test the API error response handler method. */ public function test_handle_api_error(): void { $responses = [ 500 => new Response(500, ['Content-Type' => 'application/json']), 503 => new Response(503, ['Content-Type' => 'application/json']), 401 => new Response(401, ['Content-Type' => 'application/json'], '{"error": {"message": "Invalid Authentication"}}'), 404 => new Response(404, ['Content-Type' => 'application/json'], '{"error": {"message": "You must be a member of an organization to use the API"}}'), 429 => new Response(429, ['Content-Type' => 'application/json'], '{"error": {"message": "Rate limit reached for requests"}}'), ]; $processor = new process_generate_image($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_error'); foreach ($responses as $status => $response) { $result = $method->invoke($processor, $response); $this->assertEquals($status, $result['errorcode']); if ($status == 500) { $this->assertEquals('Internal Server Error', $result['errormessage']); } else if ($status == 503) { $this->assertEquals('Service Unavailable', $result['errormessage']); } else { $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']); } } } /** * Test the API success response handler method. */ public function test_handle_api_success(): void { $response = new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, ); // We're testing a private method, so we need to setup reflector magic. $processor = new process_generate_image($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_success'); $result = $method->invoke($processor, $response); $this->stringContains('An image that represents the concept of a \'test\'.', $result['revisedprompt']); $this->stringContains('oaidalleapiprodscus.blob.core.windows.net', $result['sourceurl']); $this->assertEquals('dall-e-3', $result['model']); } /** * Test query_ai_api for a successful call. */ public function test_query_ai_api_success(): void { // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen( self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r', )), )); $this->setAdminUser(); $processor = new process_generate_image($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'query_ai_api'); $result = $method->invoke($processor); $this->stringContains('An image that represents the concept of a \'test\'.', $result['revisedprompt']); $this->stringContains('oaidalleapiprodscus.blob.core.windows.net', $result['sourceurl']); $this->assertEquals('dall-e-3', $result['model']); } /** * Test prepare_response success. */ public function test_prepare_response_success(): void { $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => true, 'revisedprompt' => 'An image that represents the concept of a \'test\'.', 'imageurl' => 'oaidalleapiprodscus.blob.core.windows.net', 'model' => 'dall-e-3', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('generate_image', $result->get_actionname()); $this->assertEquals($response['success'], $result->get_success()); $this->assertEquals($response['revisedprompt'], $result->get_response_data()['revisedprompt']); $this->assertEquals($response['model'], $result->get_response_data()['model']); } /** * Test prepare_response error. */ public function test_prepare_response_error(): void { $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => false, 'errorcode' => 500, 'errormessage' => 'Internal server error.', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('generate_image', $result->get_actionname()); $this->assertEquals($response['errorcode'], $result->get_errorcode()); $this->assertEquals($response['errormessage'], $result->get_errormessage()); } /** * Test url_to_file. */ public function test_url_to_file(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); $processor = new process_generate_image($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'url_to_file'); $contextid = 1; $url = $this->getExternalTestFileUrl('/test.jpg', false); $filenobj = $method->invoke($processor, $contextid, $url); $this->assertEquals('test.jpg', $filenobj->get_filename()); } /** * Test process. */ public function test_process(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); $url = 'https://example.com/test.jpg'; // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); // Create a request object. $contextid = 1; $userid = 1; $prompttext = 'This is a test prompt'; $aspectratio = 'square'; $quality = 'hd'; $numimages = 1; $style = 'vivid'; $this->action = new \core_ai\aiactions\generate_image( contextid: $contextid, userid: $userid, prompttext: $prompttext, quality: $quality, aspectratio: $aspectratio, numimages: $numimages, style: $style, ); $processor = new process_generate_image($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('generate_image', $result->get_actionname()); $this->assertEquals('An image that represents the concept of a \'test\'.', $result->get_response_data()['revisedprompt']); $this->assertEquals($url, $result->get_response_data()['sourceurl']); } /** * Test process method with error. */ public function test_process_error(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from OpenAI. $mock->append(new Response( 401, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Invalid Authentication']]), )); $processor = new process_generate_image($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('generate_image', $result->get_actionname()); $this->assertEquals(401, $result->get_errorcode()); $this->assertEquals('Invalid Authentication', $result->get_errormessage()); } /** * Test process method with user rate limiter. */ public function test_process_with_user_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the user rate limiter. $config = [ 'apikey' => '123', 'enableuserratelimit' => true, 'userratelimit' => 1, ]; $provider = $this->manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', config: $config, actionconfig: [ \core_ai\aiactions\generate_image::class => [ 'settings' => [ 'model' => 'dall-e-3', 'endpoint' => "https://api.openai.com/v1/chat/completions", ], ], ], ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); $url = 'https://example.com/test.jpg'; // Case 1: User rate limit has not been reached. $this->create_action($user1->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: User rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); $this->create_action($user1->id); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('User rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: User rate limit has not been reached for a different user. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); $processor = new process_generate_image($this->provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 4: Time window has passed, user rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); $this->create_action($user1->id); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } /** * Test process method with global rate limiter. */ public function test_process_with_global_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the global rate limiter. $config = [ 'apikey' => '123', 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $provider = $this->manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', config: $config, actionconfig: [ \core_ai\aiactions\generate_image::class => [ 'settings' => [ 'model' => 'dall-e-3', 'endpoint' => "https://api.openai.com/v1/chat/completions", ], ], ], ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); $url = 'https://example.com/test.jpg'; // Case 1: Global rate limit has not been reached. $this->create_action($user1->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: Global rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); $this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class); $this->create_action($user1->id); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('Global rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: Global rate limit has been reached for a different user too. // Log in user2. $this->setUser($user2); $this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class); $this->create_action($user2->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertFalse($result->get_success()); // Case 4: Time window has passed, global rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], json_encode([ 'created' => 1719140500, 'data' => [ (object) [ 'revised_prompt' => 'An image that represents the concept of a \'test\'.', 'url' => $url, ], ], ]), )); // The image downloaded from the server successfully. $mock->append(new Response( 200, ['Content-Type' => 'image/jpeg'], \GuzzleHttp\Psr7\Utils::streamFor(fopen(self::get_fixture_path('aiprovider_openai', 'test.jpg'), 'r')), )); $this->provider = $this->create_provider(\core_ai\aiactions\generate_image::class); $this->create_action($user1->id); $processor = new process_generate_image($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } } PKY\C_PP%tests/process_summarise_text_test.phpnu[. namespace aiprovider_openai; use aiprovider_openai\test\testcase_helper_trait; use core_ai\aiactions\base; use core_ai\provider; use GuzzleHttp\Psr7\Response; /** * Test Generate text provider class for OpenAI provider methods. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \aiprovider_openai\provider * @covers \aiprovider_openai\process_summarise_text * @covers \aiprovider_openai\abstract_processor */ final class process_summarise_text_test extends \advanced_testcase { use testcase_helper_trait; /** @var string A successful response in JSON format. */ protected string $responsebodyjson; /** @var \core_ai\manager */ private $manager; /** @var provider The provider that will process the action. */ protected provider $provider; /** @var base The action to process. */ protected base $action; /** * Set up the test. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Load a response body from a file. $this->responsebodyjson = file_get_contents(self::get_fixture_path('aiprovider_openai', 'text_request_success.json')); $this->manager = \core\di::get(\core_ai\manager::class); $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\summarise_text::class, actionconfig: [ 'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'), ], ); $this->create_action(); } /** * Create the action object. * @param int $userid The user id to use in the action. */ private function create_action(int $userid = 1): void { $this->action = new \core_ai\aiactions\summarise_text( contextid: 1, userid: $userid, prompttext: 'This is a test prompt', ); } /** * Test create_request_object */ public function test_create_request_object(): void { $processor = new process_summarise_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('system', $body->messages[0]->role); $this->assertEquals(get_string('action_summarise_text_instruction', 'core_ai'), $body->messages[0]->content); $this->assertEquals('This is a test prompt', $body->messages[1]->content); $this->assertEquals('user', $body->messages[1]->role); } /** * Test create_request_object with extra model settings. */ public function test_create_request_object_with_model_settings(): void { $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\summarise_text::class, actionconfig: [ 'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'), 'temperature' => '0.5', 'max_completion_tokens' => '100', ], ); $processor = new process_summarise_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('gpt-4o', $body->model); $this->assertEquals('0.5', $body->temperature); $this->assertEquals('100', $body->max_completion_tokens); $this->provider = $this->create_provider( actionclass: \core_ai\aiactions\summarise_text::class, actionconfig: [ 'model' => 'my-custom-gpt', 'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'), 'modelextraparams' => '{"temperature": 0.5,"max_completion_tokens": 100}', ], ); $processor = new process_summarise_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'create_request_object'); $request = $method->invoke($processor, 1); $body = (object) json_decode($request->getBody()->getContents()); $this->assertEquals('my-custom-gpt', $body->model); $this->assertEquals('0.5', $body->temperature); $this->assertEquals('100', $body->max_completion_tokens); } /** * Test the API error response handler method. * */ public function test_handle_api_error(): void { $responses = [ 500 => new Response(500, ['Content-Type' => 'application/json']), 503 => new Response(503, ['Content-Type' => 'application/json']), 401 => new Response(401, ['Content-Type' => 'application/json'], '{"error": {"message": "Invalid Authentication"}}'), 404 => new Response(404, ['Content-Type' => 'application/json'], '{"error": {"message": "You must be a member of an organization to use the API"}}'), 429 => new Response(429, ['Content-Type' => 'application/json'], '{"error": {"message": "Rate limit reached for requests"}}'), ]; $processor = new process_summarise_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_error'); foreach ($responses as $status => $response) { $result = $method->invoke($processor, $response); $this->assertEquals($status, $result['errorcode']); if ($status == 500) { $this->assertEquals('Internal Server Error', $result['errormessage']); } else if ($status == 503) { $this->assertEquals('Service Unavailable', $result['errormessage']); } else { $this->assertStringContainsString($response->getBody()->getContents(), $result['errormessage']); } } } /** * Test the API success response handler method. */ public function test_handle_api_success(): void { $response = new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, ); // We're testing a private method, so we need to set up reflector magic. $processor = new process_summarise_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'handle_api_success'); $result = $method->invoke($processor, $response); $this->assertTrue($result['success']); $this->assertEquals('chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']); $this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']); $this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']); $this->assertEquals('stop', $result['finishreason']); $this->assertEquals('11', $result['prompttokens']); $this->assertEquals('568', $result['completiontokens']); $this->assertEquals('gpt-4o-2024-05-13', $result['model']); } /** * Test query_ai_api for a successful call. */ public function test_query_ai_api_success(): void { // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($this->provider, $this->action); $method = new \ReflectionMethod($processor, 'query_ai_api'); $result = $method->invoke($processor); $this->assertTrue($result['success']); $this->assertEquals('chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', $result['id']); $this->assertEquals('fp_c4e5b6fa31', $result['fingerprint']); $this->assertStringContainsString('Sure, here is some sample text', $result['generatedcontent']); $this->assertEquals('stop', $result['finishreason']); $this->assertEquals('11', $result['prompttokens']); $this->assertEquals('568', $result['completiontokens']); $this->assertEquals('gpt-4o-2024-05-13', $result['model']); } /** * Test prepare_response success. */ public function test_prepare_response_success(): void { $processor = new process_summarise_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => true, 'id' => 'chatcmpl-9lkwPWOIiQEvI3nfcGofJcmS5lPYo', 'fingerprint' => 'fp_c4e5b6fa31', 'generatedcontent' => 'Sure, here is some sample text', 'finishreason' => 'stop', 'prompttokens' => '11', 'completiontokens' => '568', 'model' => 'gpt-4o', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('summarise_text', $result->get_actionname()); $this->assertEquals($response['success'], $result->get_success()); $this->assertEquals($response['generatedcontent'], $result->get_response_data()['generatedcontent']); $this->assertEquals($response['model'], $result->get_response_data()['model']); } /** * Test prepare_response error. */ public function test_prepare_response_error(): void { $processor = new process_summarise_text($this->provider, $this->action); // We're working with a private method here, so we need to use reflection. $method = new \ReflectionMethod($processor, 'prepare_response'); $response = [ 'success' => false, 'errorcode' => 500, 'errormessage' => 'Internal server error.', ]; $result = $method->invoke($processor, $response); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('summarise_text', $result->get_actionname()); $this->assertEquals($response['errorcode'], $result->get_errorcode()); $this->assertEquals($response['errormessage'], $result->get_errormessage()); } /** * Test process method. */ public function test_process(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertTrue($result->get_success()); $this->assertEquals('summarise_text', $result->get_actionname()); } /** * Test process method with error. */ public function test_process_error(): void { // Log in user. $this->setUser($this->getDataGenerator()->create_user()); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // The response from OpenAI. $mock->append(new Response( 401, ['Content-Type' => 'application/json'], json_encode(['error' => ['message' => 'Invalid Authentication']]), )); $processor = new process_summarise_text($this->provider, $this->action); $result = $processor->process(); $this->assertInstanceOf(\core_ai\aiactions\responses\response_base::class, $result); $this->assertFalse($result->get_success()); $this->assertEquals('summarise_text', $result->get_actionname()); $this->assertEquals(401, $result->get_errorcode()); $this->assertEquals('Invalid Authentication', $result->get_errormessage()); } /** * Test process method with user rate limiter. */ public function test_process_with_user_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the user rate limiter. $config = [ 'apikey' => '123', 'enableuserratelimit' => true, 'userratelimit' => 1, ]; $provider = $this->manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', config: $config, actionconfig: [ \core_ai\aiactions\summarise_text::class => [ 'settings' => [ 'model' => 'gpt-4o', 'endpoint' => "https://api.openai.com/v1/chat/completions", 'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'), ], ], ], ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Case 1: User rate limit has not been reached. $this->create_action($user1->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: User rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('User rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: User rate limit has not been reached for a different user. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 4: Time window has passed, user rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } /** * Test process method with global rate limiter. */ public function test_process_with_global_rate_limiter(): void { // Create users. $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); // Log in user1. $this->setUser($user1); // Mock clock. $clock = $this->mock_clock_with_frozen(); // Set the global rate limiter. $config = [ 'apikey' => '123', 'enableglobalratelimit' => true, 'globalratelimit' => 1, ]; $provider = $this->manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', config: $config, actionconfig: [ \core_ai\aiactions\summarise_text::class => [ 'settings' => [ 'model' => 'gpt-4o', 'endpoint' => "https://api.openai.com/v1/chat/completions", 'systeminstruction' => get_string('action_summarise_text_instruction', 'core_ai'), ], ], ], ); // Mock the http client to return a successful response. ['mock' => $mock] = $this->get_mocked_http_client(); // Case 1: Global rate limit has not been reached. $this->create_action($user1->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); // Case 2: Global rate limit has been reached. $clock->bump(HOURSECS - 10); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertEquals(429, $result->get_errorcode()); $this->assertEquals('Global rate limit exceeded', $result->get_errormessage()); $this->assertFalse($result->get_success()); // Case 3: Global rate limit has been reached for a different user too. // Log in user2. $this->setUser($user2); $this->create_action($user2->id); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertFalse($result->get_success()); // Case 4: Time window has passed, global rate limit should be reset. $clock->bump(11); // Log in user1. $this->setUser($user1); // The response from OpenAI. $mock->append(new Response( 200, ['Content-Type' => 'application/json'], $this->responsebodyjson, )); $this->create_action($user1->id); $processor = new process_summarise_text($provider, $this->action); $result = $processor->process(); $this->assertTrue($result->get_success()); } } PKY\tests/provider_test.phpnu[. namespace aiprovider_openai; /** * Test OpenAI provider methods. * * @package aiprovider_openai * @copyright 2024 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \aiprovider_openai\provider */ final class provider_test extends \advanced_testcase { /** @var \core_ai\manager */ private $manager; /** @var \core_ai\provider */ private $provider; /** * Overriding setUp() function to always reset after tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); // Create the provider instance. $this->manager = \core\di::get(\core_ai\manager::class); $this->provider = $this->manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', ); } /** * Test get_action_list */ public function test_get_action_list(): void { $actionlist = $this->provider->get_action_list(); $this->assertIsArray($actionlist); $this->assertCount(4, $actionlist); $this->assertContains(\core_ai\aiactions\generate_text::class, $actionlist); $this->assertContains(\core_ai\aiactions\generate_image::class, $actionlist); $this->assertContains(\core_ai\aiactions\summarise_text::class, $actionlist); $this->assertContains(\core_ai\aiactions\explain_text::class, $actionlist); } /** * Test generate_userid. */ public function test_generate_userid(): void { $userid = $this->provider->generate_userid(1); // Assert that the generated userid is a string of proper length. $this->assertIsString($userid); $this->assertEquals(64, strlen($userid)); } /** * Test is_request_allowed. */ public function test_is_request_allowed(): void { // Create the provider instance. $config = [ 'enableuserratelimit' => true, 'userratelimit' => 3, 'enableglobalratelimit' => true, 'globalratelimit' => 5, ]; $provider = $this->manager->create_provider_instance( classname: '\aiprovider_openai\provider', name: 'dummy', config: $config, ); $contextid = 1; $userid = 1; $prompttext = 'This is a test prompt'; $aspectratio = 'square'; $quality = 'hd'; $numimages = 1; $style = 'vivid'; $action = new \core_ai\aiactions\generate_image( contextid: $contextid, userid: $userid, prompttext: $prompttext, quality: $quality, aspectratio: $aspectratio, numimages: $numimages, style: $style, ); // Make 3 requests, all should be allowed. for ($i = 0; $i < 3; $i++) { $this->assertTrue($provider->is_request_allowed($action)); } // The 4th request for the same user should be denied. $result = $provider->is_request_allowed($action); $this->assertFalse($result['success']); $this->assertEquals('User rate limit exceeded', $result['errormessage']); // Change user id to make a request for a different user, should pass (4 requests for global rate). $action = new \core_ai\aiactions\generate_image( contextid: $contextid, userid: 2, prompttext: $prompttext, quality: $quality, aspectratio: $aspectratio, numimages: $numimages, style: $style, ); $this->assertTrue($provider->is_request_allowed($action)); // Make a 5th request for the global rate limit, it should be allowed. $this->assertTrue($provider->is_request_allowed($action)); // The 6th request should be denied. $result = $provider->is_request_allowed($action); $this->assertFalse($result['success']); $this->assertEquals('Global rate limit exceeded', $result['errormessage']); } /** * Test is_provider_configured. */ public function test_is_provider_configured(): void { // No configured values. $this->assertFalse($this->provider->is_provider_configured()); // Properly configured values. $updatedprovider = $this->manager->update_provider_instance( provider: $this->provider, config: ['apikey' => '123'], ); $this->assertTrue($updatedprovider->is_provider_configured()); } } PKY\]] db/hooks.phpnu[PKY\]GDDlang/en/aiprovider_openai.phpnu[PKY\:*"lang/en/deprecated.txtnu[PKY\sA/DD E%version.phpnu[PKY\( +)classes/form/action_generate_image_form.phpnu[PKY\=)k3classes/form/action_form.phpnu[PKY\lX *Qclasses/form/action_generate_text_form.phpnu[PKY\ ^classes/privacy/provider.phpnu[PKY\Π"Niclasses/process_generate_image.phpnu[PKY\[classes/aimodel/dalle3.phpnu[PKY\ nclasses/aimodel/openai_base.phpnu[PKY\cclasses/aimodel/o1.phpnu[PKY\Zxt t classes/aimodel/gpt4o.phpnu[PKY\ l"Þclasses/process_summarise_text.phpnu[PKY\2nclasses/hook_listener.phpnu[PKY\9[Cijclasses/helper.phpnu[PKY\n һclasses/abstract_processor.phpnu[PKY\r0__classes/provider.phpnu[PKY\cˑ fclasses/process_explain_text.phpnu[PKY\ &classes/test/testcase_helper_trait.phpnu[PKY\$L; !classes/process_generate_text.phpnu[PKY\ۻ{((amd/src/modelchooser.jsnu[PKY\_iKK!amd/build/modelchooser.min.js.mapnu[PKY\PHUU amd/build/modelchooser.min.jsnu[PKY\u (, tests/fixtures/text_request_success.jsonnu[PKY\]>6)6tests/fixtures/image_request_success.jsonnu[PKY\:Dr44.tests/fixtures/test.jpgnu[PKY\A5Q5Q$tests/process_generate_text_test.phpnu[PKY\ll%2 tests/process_generate_image_test.phpnu[PKY\C_PP%vtests/process_summarise_text_test.phpnu[PKY\tests/provider_test.phpnu[PKI u