Viewing File: /usr/local/cpanel/whostmgr/docroot/cgi/softaculous/lib/ai/core/class_ai_client.php
<?php
if(!defined('SOFTACULOUS')){
die('Hacking Attempt');
}
class AIClient {
private $provider;
private $api_key;
private $model;
private $provider_type;
private $base_url;
private $provider_config;
public function __construct($provider, $api_key, $model, $provider_config = array()){
$this->provider = $provider;
$this->api_key = $api_key;
$this->model = $model;
$this->provider_type = $provider->get_id();
$this->base_url = $provider->get_api_endpoint();
$this->provider_config = $provider_config;
}
public function chat($messages, $tools = array(), $options = array()){
if($this->provider_type === 'anthropic'){
return $this->anthropic_chat($messages, $tools, $options);
}elseif($this->provider_type === 'google'){
return $this->google_chat($messages, $tools, $options);
}elseif($this->provider_type === 'azure'){
return $this->azure_chat($messages, $tools, $options);
}
if($this->provider_type === 'openai' && $this->is_responses_only_model()){
return $this->openai_responses_chat($messages, $tools, $options);
}
return $this->openai_chat($messages, $tools, $options);
}
public function chat_stream($messages, $tools, $on_event, $options = array()){
if($this->provider_type === 'anthropic'){
return $this->anthropic_stream($messages, $tools, $on_event, $options);
}elseif($this->provider_type === 'google'){
return $this->google_stream($messages, $tools, $on_event, $options);
}elseif($this->provider_type === 'azure'){
return $this->azure_stream($messages, $tools, $on_event, $options);
}
if($this->provider_type === 'openai' && $this->is_responses_only_model()){
return $this->openai_responses_stream($messages, $tools, $on_event, $options);
}
return $this->openai_stream($messages, $tools, $on_event, $options);
}
private $abort_check_file = '';
private $abort_check_time = 0;
public function set_abort_check_file($filepath){
$this->abort_check_file = $filepath;
}
private function should_abort(){
if(empty($this->abort_check_file)) return false;
$now = time();
if($now - $this->abort_check_time < 1) return false;
$this->abort_check_time = $now;
if(file_exists($this->abort_check_file)) return true;
if(function_exists('connection_aborted') && connection_aborted()) return true;
return false;
}
public function test_connection(){
$messages = array(array('role' => 'user', 'content' => 'Say "OK" in one word.'));
try{
$result = $this->chat($messages, array(), array('max_tokens' => 10));
if(!empty($result['error'])){
return array('success' => false, 'error' => $result['error']);
}
return array('success' => true, 'message' => __('Connection successful'));
}catch(\Exception $e){
return array('success' => false, 'error' => $e->getMessage());
}
}
// ---- OpenAI Compatible ----
private function openai_headers(){
$h = array(
'Content-Type: application/json'
);
if(!empty($this->api_key)){
$h[] = 'Authorization: Bearer ' . $this->api_key;
}
if($this->provider_type === 'opencode_zen' || $this->provider_type === 'opencode_zen_premium' || $this->provider_type === 'openrouter'){
$h[] = 'HTTP-Referer: https://opencode.ai/';
$h[] = 'X-Title: Softaculous AI';
}
if(!empty($this->provider_config['extra_headers'])){
foreach($this->provider_config['extra_headers'] as $eh){
$h[] = $eh;
}
}
return $h;
}
private function openai_chat($messages, $tools, $options){
$url = rtrim($this->base_url, '/') . '/chat/completions';
$headers = $this->openai_headers();
$payload = $this->openai_build_payload($messages, $tools, $options);
$payload['stream'] = false;
$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
if(!empty($response['error'])){
if($this->provider_type === 'openai' && $this->is_responses_endpoint_error($response['error'])){
return $this->openai_responses_chat($messages, $tools, $options);
}
return $response;
}
return $this->openai_parse_response($response['body']);
}
private function is_responses_endpoint_error($msg){
$msg = strtolower((string)$msg);
if(strpos($msg, 'v1/responses') !== false){
return true;
}
if(strpos($msg, 'not supported in the v1/chat/completions endpoint') !== false){
return true;
}
return false;
}
private function openai_stream($messages, $tools, $on_event, $options){
$url = rtrim($this->base_url, '/') . '/chat/completions';
$headers = $this->openai_headers();
$payload = $this->openai_build_payload($messages, $tools, $options);
$payload['stream'] = true;
$accumulated = array('text' => '', 'reasoning' => '', 'tool_calls' => array(), 'usage' => array());
$tool_call_buffers = array();
$sse_buffer = '';
try{
$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$tool_call_buffers, &$sse_buffer){
$sse_buffer .= $chunk;
while(($pos = strpos($sse_buffer, "\n")) !== false){
$line = substr($sse_buffer, 0, $pos);
$sse_buffer = substr($sse_buffer, $pos + 1);
$line = trim($line);
if(strpos($line, 'data: ') !== 0) continue;
$data_str = substr($line, 6);
if(trim($data_str) === '[DONE]') return;
$data = @json_decode($data_str, true);
if(!$data) continue;
if(!empty($data['choices'][0])){
$choice = $data['choices'][0];
$delta = (!empty($choice['delta']) ? $choice['delta'] : array());
if(!empty($delta['content'])){
$accumulated['text'] .= $delta['content'];
$on_event(array('type' => 'text_delta', 'text' => $delta['content']));
}
if(!empty($delta['reasoning_content'])){
$accumulated['reasoning'] .= $delta['reasoning_content'];
$on_event(array('type' => 'reasoning_delta', 'text' => $delta['reasoning_content']));
}
if(!empty($delta['tool_calls'])){
foreach($delta['tool_calls'] as $tc){
$idx = isset($tc['index']) ? $tc['index'] : 0;
if(!isset($tool_call_buffers[$idx])){
$tool_call_buffers[$idx] = array(
'id' => isset($tc['id']) ? $tc['id'] : '',
'name' => isset($tc['function']['name']) ? $tc['function']['name'] : '',
'arguments' => ''
);
}
if(!empty($tc['id'])) $tool_call_buffers[$idx]['id'] = $tc['id'];
if(!empty($tc['function']['name'])) $tool_call_buffers[$idx]['name'] = $tc['function']['name'];
if(!empty($tc['function']['arguments'])) $tool_call_buffers[$idx]['arguments'] .= $tc['function']['arguments'];
}
}
if(!empty($choice['finish_reason'])){
if($choice['finish_reason'] === 'tool_calls' || $choice['finish_reason'] === 'function_call'){
}
}
}
if(!empty($data['usage'])){
$accumulated['usage'] = $data['usage'];
// Normalize OpenAI cached tokens from streaming
if(!empty($data['usage']['prompt_tokens_details']['cached_tokens'])){
$accumulated['usage']['cached_tokens'] = $data['usage']['prompt_tokens_details']['cached_tokens'];
}
}
}
}, isset($options['timeout']) ? $options['timeout'] : 120);
}catch(\RuntimeException $e){
if($this->provider_type === 'openai' && $this->is_responses_endpoint_error($e->getMessage())){
return $this->openai_responses_stream($messages, $tools, $on_event, $options);
}
throw $e;
}
if(!empty($tool_call_buffers)){
foreach($tool_call_buffers as $i => $tc){
$args = @json_decode($tc['arguments'], true);
if($args === null) $args = array();
$tool_call = array(
'id' => $tc['id'] ? $tc['id'] : 'call_'.$i,
'name' => $tc['name'],
'input' => $args
);
$accumulated['tool_calls'][] = $tool_call;
$on_event(array('type' => 'tool_call', 'id' => $tool_call['id'], 'name' => $tool_call['name'], 'input' => $args));
}
}
$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));
return array(
'content' => $accumulated['text'],
'tool_calls' => $accumulated['tool_calls'],
'usage' => $accumulated['usage'],
'parts' => $this->build_parts($accumulated)
);
}
private function is_reasoning_model(){
$model = strtolower((string)$this->model);
if($model === '') return false;
if(function_exists('ai_get_models_dev_cache')){
$cache = ai_get_models_dev_cache();
if(is_array($cache)){
if(isset($cache['openai/' . $this->model]['reasoning']) && $cache['openai/' . $this->model]['reasoning']){
return true;
}
if(isset($cache[$this->model]['reasoning']) && $cache[$this->model]['reasoning']){
return true;
}
}
}
$patterns = array('gpt-5', 'gpt-6', 'o1', 'o2', 'o3', 'o4', 'o5', 'codex');
foreach($patterns as $p){
if(strpos($model, $p) === 0){
return true;
}
}
return false;
}
private function is_responses_only_model(){
$model = strtolower((string)$this->model);
if($model === '') return false;
if(strpos($model, 'codex') !== false){
return true;
}
return false;
}
private function openai_build_payload($messages, $tools, $options){
$payload = array(
'model' => $this->model,
'messages' => $messages
);
if(!empty($tools)){
$payload['tools'] = array_map(function($t){
return array(
'type' => 'function',
'function' => array(
'name' => $t['name'],
'description' => $t['description'],
'parameters' => $t['parameters']
)
);
}, $tools);
}
$reasoning = $this->is_reasoning_model();
if($reasoning){
$effort = !empty($options['reasoning_effort']) ? $options['reasoning_effort'] : 'medium';
$payload['reasoning_effort'] = $effort;
if(!empty($options['max_tokens'])){
$payload['max_completion_tokens'] = intval($options['max_tokens']);
}
}else{
if(!empty($options['temperature'])){
$payload['temperature'] = floatval($options['temperature']);
}
if(!empty($options['max_tokens'])){
$payload['max_tokens'] = intval($options['max_tokens']);
}
}
// OpenAI: automatic prefix caching (no explicit markers needed)
// Just need to parse cached_tokens from usage response
return $payload;
}
private function openai_parse_response($body){
$data = @json_decode($body, true);
if(!$data){
return array('error' => __('Invalid JSON response from API'));
}
if(!empty($data['error'])){
$msg = is_array($data['error']) ? (isset($data['error']['message']) ? $data['error']['message'] : json_encode($data['error'])) : $data['error'];
return array('error' => $msg);
}
if(empty($data['choices'])) $choice = null; else $choice = $data['choices'][0];
if(!$choice) return array('error' => __('No response choices'));
if(empty($choice['message']['content'])) $text = ''; else $text = $choice['message']['content'];
$reasoning = '';
if(!empty($choice['message']['reasoning_content'])) $reasoning = $choice['message']['reasoning_content'];
$tool_calls = array();
if(!empty($choice['message']['tool_calls'])){
foreach($choice['message']['tool_calls'] as $tc){
$args = @json_decode(isset($tc['function']['arguments']) ? $tc['function']['arguments'] : '{}', true);
if($args === null) $args = array();
$tool_calls[] = array(
'id' => isset($tc['id']) ? $tc['id'] : '',
'name' => isset($tc['function']['name']) ? $tc['function']['name'] : '',
'input' => $args
);
}
}
$accumulated = array('text' => $text, 'reasoning' => $reasoning, 'tool_calls' => $tool_calls, 'usage' => isset($data['usage']) ? $data['usage'] : array());
// Normalize OpenAI cached tokens
if(!empty($data['usage']['prompt_tokens_details']['cached_tokens'])){
$accumulated['usage']['cached_tokens'] = $data['usage']['prompt_tokens_details']['cached_tokens'];
}
return array(
'content' => $text,
'tool_calls' => $tool_calls,
'usage' => $accumulated['usage'],
'parts' => $this->build_parts($accumulated)
);
}
// ---- OpenAI Responses API (v1/responses) ----
// Used for Codex models and any model OpenAI moves to the Responses-only endpoint.
// Also used as a fallback when chat/completions returns the "use v1/responses" error.
private function openai_responses_build_payload($messages, $tools, $options){
$input = array();
foreach($messages as $msg){
$role = isset($msg['role']) ? $msg['role'] : 'user';
if($role === 'system'){
$input[] = array(
'role' => 'system',
'content' => isset($msg['content']) ? $msg['content'] : ''
);
}elseif($role === 'user'){
$content = isset($msg['content']) ? $msg['content'] : '';
if(is_array($content)) $content = json_encode($content);
$input[] = array('role' => 'user', 'content' => $content);
}elseif($role === 'assistant'){
$assistant_content = array();
if(!empty($msg['content'])){
$content = $msg['content'];
if(is_array($content)) $content = json_encode($content);
if($content !== '' && $content !== null){
$assistant_content[] = array('type' => 'output_text', 'text' => $content);
}
}
if(!empty($msg['tool_calls'])){
foreach($msg['tool_calls'] as $tc){
$args_str = isset($tc['function']['arguments']) ? $tc['function']['arguments'] : '{}';
$input[] = array(
'type' => 'function_call',
'call_id' => isset($tc['id']) ? $tc['id'] : '',
'name' => isset($tc['function']['name']) ? $tc['function']['name'] : '',
'arguments' => $args_str
);
}
}
if(!empty($assistant_content)){
$input[] = array('role' => 'assistant', 'content' => $assistant_content);
}
}elseif($role === 'tool' || $role === 'tool_result'){
$content = isset($msg['content']) ? $msg['content'] : '';
if(is_array($content)) $content = json_encode($content);
$input[] = array(
'type' => 'function_call_output',
'call_id' => isset($msg['tool_call_id']) ? $msg['tool_call_id'] : '',
'output' => $content
);
}
}
$payload = array(
'model' => $this->model,
'input' => $input
);
if(!empty($tools)){
$payload['tools'] = array_map(function($t){
return array(
'type' => 'function',
'name' => $t['name'],
'description' => $t['description'],
'parameters' => $t['parameters']
);
}, $tools);
}
if($this->is_reasoning_model()){
$effort = !empty($options['reasoning_effort']) ? $options['reasoning_effort'] : 'medium';
$payload['reasoning'] = array('effort' => $effort);
if(!empty($options['max_tokens'])){
$payload['max_output_tokens'] = intval($options['max_tokens']);
}
}else{
if(!empty($options['temperature'])){
$payload['temperature'] = floatval($options['temperature']);
}
if(!empty($options['max_tokens'])){
$payload['max_output_tokens'] = intval($options['max_tokens']);
}
}
return $payload;
}
private function openai_responses_chat($messages, $tools, $options){
$url = rtrim($this->base_url, '/') . '/responses';
$headers = $this->openai_headers();
$payload = $this->openai_responses_build_payload($messages, $tools, $options);
$payload['stream'] = false;
$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
if(!empty($response['error'])){
return $response;
}
return $this->openai_responses_parse_response($response['body']);
}
private function openai_responses_parse_response($body){
$data = @json_decode($body, true);
if(!$data){
return array('error' => __('Invalid JSON response from API'));
}
if(!empty($data['error'])){
$msg = is_array($data['error']) ? (isset($data['error']['message']) ? $data['error']['message'] : json_encode($data['error'])) : $data['error'];
return array('error' => $msg);
}
$text = '';
$reasoning = '';
$tool_calls = array();
$output = isset($data['output']) && is_array($data['output']) ? $data['output'] : array();
foreach($output as $item){
$itype = isset($item['type']) ? $item['type'] : '';
if($itype === 'message'){
$content = isset($item['content']) && is_array($item['content']) ? $item['content'] : array();
foreach($content as $c){
$ctype = isset($c['type']) ? $c['type'] : '';
if(($ctype === 'output_text' || $ctype === 'text') && !empty($c['text'])){
$text .= $c['text'];
}elseif($ctype === 'reasoning' && !empty($c['text'])){
$reasoning .= $c['text'];
}
}
}elseif($itype === 'function_call'){
$args = @json_decode(isset($item['arguments']) ? $item['arguments'] : '{}', true);
if($args === null) $args = array();
$tool_calls[] = array(
'id' => isset($item['call_id']) ? $item['call_id'] : (isset($item['id']) ? $item['id'] : ''),
'name' => isset($item['name']) ? $item['name'] : '',
'input' => $args
);
}elseif($itype === 'reasoning'){
$summary = isset($item['summary']) && is_array($item['summary']) ? $item['summary'] : array();
foreach($summary as $s){
if(!empty($s['text'])) $reasoning .= $s['text'];
}
}
}
$accumulated = array('text' => $text, 'reasoning' => $reasoning, 'tool_calls' => $tool_calls, 'usage' => isset($data['usage']) ? $data['usage'] : array());
if(!empty($data['usage']['input_tokens_details']['cached_tokens'])){
$accumulated['usage']['cached_tokens'] = $data['usage']['input_tokens_details']['cached_tokens'];
}
return array(
'content' => $text,
'tool_calls' => $tool_calls,
'usage' => $accumulated['usage'],
'parts' => $this->build_parts($accumulated)
);
}
private function openai_responses_stream($messages, $tools, $on_event, $options){
$url = rtrim($this->base_url, '/') . '/responses';
$headers = $this->openai_headers();
$payload = $this->openai_responses_build_payload($messages, $tools, $options);
$payload['stream'] = true;
$accumulated = array('text' => '', 'reasoning' => '', 'tool_calls' => array(), 'usage' => array());
$pending_tool_calls = array();
$sse_buffer = '';
$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$pending_tool_calls, &$sse_buffer){
$sse_buffer .= $chunk;
while(($pos = strpos($sse_buffer, "\n")) !== false){
$line = substr($sse_buffer, 0, $pos);
$sse_buffer = substr($sse_buffer, $pos + 1);
$line = trim($line);
if(strpos($line, 'data: ') !== 0) continue;
$data_str = substr($line, 6);
if(trim($data_str) === '[DONE]') return;
$event = @json_decode($data_str, true);
if(!$event) continue;
$etype = isset($event['type']) ? $event['type'] : '';
if($etype === 'response.output_text.delta'){
if(!empty($event['delta'])){
$accumulated['text'] .= $event['delta'];
$on_event(array('type' => 'text_delta', 'text' => $event['delta']));
}
}elseif($etype === 'response.reasoning_summary_text.delta'){
if(!empty($event['delta'])){
$accumulated['reasoning'] .= $event['delta'];
$on_event(array('type' => 'reasoning_delta', 'text' => $event['delta']));
}
}elseif($etype === 'response.output_item.added'){
$item = isset($event['item']) ? $event['item'] : array();
if((isset($item['type']) ? $item['type'] : '') === 'function_call'){
$idx = count($pending_tool_calls);
$pending_tool_calls[$idx] = array(
'call_id' => isset($item['call_id']) ? $item['call_id'] : '',
'id' => isset($item['id']) ? $item['id'] : '',
'name' => isset($item['name']) ? $item['name'] : '',
'arguments' => ''
);
}
}elseif($etype === 'response.function_call_arguments.delta'){
if(!empty($event['delta']) && !empty($pending_tool_calls)){
$last_idx = count($pending_tool_calls) - 1;
if($last_idx >= 0){
$pending_tool_calls[$last_idx]['arguments'] .= $event['delta'];
}
}
}elseif($etype === 'response.output_item.done'){
$item = isset($event['item']) ? $event['item'] : array();
if((isset($item['type']) ? $item['type'] : '') === 'function_call'){
$args = @json_decode(isset($item['arguments']) ? $item['arguments'] : '{}', true);
if($args === null) $args = array();
$tc = array(
'id' => isset($item['call_id']) ? $item['call_id'] : (isset($item['id']) ? $item['id'] : ''),
'name' => isset($item['name']) ? $item['name'] : '',
'input' => $args
);
$accumulated['tool_calls'][] = $tc;
$on_event(array('type' => 'tool_call', 'id' => $tc['id'], 'name' => $tc['name'], 'input' => $args));
}
}elseif($etype === 'response.completed' || $etype === 'response.done'){
if(!empty($event['response']['usage'])){
$accumulated['usage'] = $event['response']['usage'];
if(!empty($event['response']['usage']['input_tokens_details']['cached_tokens'])){
$accumulated['usage']['cached_tokens'] = $event['response']['usage']['input_tokens_details']['cached_tokens'];
}
}elseif(!empty($event['usage'])){
$accumulated['usage'] = $event['usage'];
}
}elseif($etype === 'error' || $etype === 'response.failed'){
$err_msg = isset($event['error']['message']) ? $event['error']['message'] : (isset($event['message']) ? $event['message'] : __('Unknown error'));
$on_event(array('type' => 'error', 'message' => $err_msg));
}
}
}, isset($options['timeout']) ? $options['timeout'] : 120);
$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));
return array(
'content' => $accumulated['text'],
'tool_calls' => $accumulated['tool_calls'],
'usage' => $accumulated['usage'],
'parts' => $this->build_parts($accumulated)
);
}
private function anthropic_chat($messages, $tools, $options){
$url = 'https://api.anthropic.com/v1/messages';
$headers = array(
'Content-Type: application/json',
'x-api-key: ' . $this->api_key,
'anthropic-version: 2023-06-01',
'Accept: application/json'
);
$payload = $this->anthropic_build_payload($messages, $tools, $options);
$payload['stream'] = false;
$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
if(!empty($response['error'])) return $response;
return $this->anthropic_parse_response($response['body']);
}
private function anthropic_stream($messages, $tools, $on_event, $options){
$url = 'https://api.anthropic.com/v1/messages';
$headers = array(
'Content-Type: application/json',
'x-api-key: ' . $this->api_key,
'anthropic-version: 2023-06-01',
'Accept: text/event-stream'
);
$payload = $this->anthropic_build_payload($messages, $tools, $options);
$payload['stream'] = true;
$accumulated = array('text' => '', 'reasoning' => '', 'tool_calls' => array(), 'usage' => array());
$current_tool = null;
$current_tool_args = '';
$sse_buffer = '';
$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$current_tool, &$current_tool_args, &$sse_buffer){
$sse_buffer .= $chunk;
while(($pos = strpos($sse_buffer, "\n")) !== false){
$line = substr($sse_buffer, 0, $pos);
$sse_buffer = substr($sse_buffer, $pos + 1);
$line = trim($line);
if(strpos($line, 'data: ') !== 0) continue;
$data_str = substr($line, 6);
$event = @json_decode($data_str, true);
if(!$event) continue;
$type = isset($event['type']) ? $event['type'] : '';
if($type === 'content_block_delta'){
$delta = isset($event['delta']) ? $event['delta'] : array();
$delta_type = isset($delta['type']) ? $delta['type'] : '';
if($delta_type === 'text_delta' && !empty($delta['text'])){
$accumulated['text'] .= $delta['text'];
$on_event(array('type' => 'text_delta', 'text' => $delta['text']));
}elseif($delta_type === 'thinking_delta' && !empty($delta['thinking'])){
$accumulated['reasoning'] .= $delta['thinking'];
$on_event(array('type' => 'reasoning_delta', 'text' => $delta['thinking']));
}elseif($delta_type === 'input_json_delta' && !empty($delta['partial_json'])){
$current_tool_args .= $delta['partial_json'];
}
}elseif($type === 'content_block_start'){
$block = isset($event['content_block']) ? $event['content_block'] : array();
if((isset($block['type']) ? $block['type'] : '') === 'tool_use'){
$current_tool = array(
'id' => isset($block['id']) ? $block['id'] : '',
'name' => isset($block['name']) ? $block['name'] : '',
'input' => array()
);
$current_tool_args = '';
}
}elseif($type === 'content_block_stop'){
if($current_tool){
$args = @json_decode($current_tool_args, true);
if($args === null) $args = array();
$current_tool['input'] = $args;
$accumulated['tool_calls'][] = $current_tool;
$on_event(array('type' => 'tool_call', 'id' => $current_tool['id'], 'name' => $current_tool['name'], 'input' => $args));
$current_tool = null;
$current_tool_args = '';
}
}elseif($type === 'message_delta'){
if(!empty($event['usage'])){
$accumulated['usage']['output_tokens'] = isset($event['usage']['output_tokens']) ? $event['usage']['output_tokens'] : 0;
}
}elseif($type === 'message_start'){
if(!empty($event['message']['usage'])){
$accumulated['usage']['input_tokens'] = isset($event['message']['usage']['input_tokens']) ? $event['message']['usage']['input_tokens'] : 0;
if(!empty($event['message']['usage']['cache_creation_input_tokens'])) $accumulated['usage']['cache_creation_input_tokens'] = $event['message']['usage']['cache_creation_input_tokens'];
if(!empty($event['message']['usage']['cache_read_input_tokens'])) $accumulated['usage']['cache_read_input_tokens'] = $event['message']['usage']['cache_read_input_tokens'];
}
}elseif($type === 'message_stop'){
// done
}elseif($type === 'error'){
$err_msg = isset($event['error']['message']) ? $event['error']['message'] : __('Unknown error');
$on_event(array('type' => 'error', 'message' => $err_msg));
}
}
}, isset($options['timeout']) ? $options['timeout'] : 120);
$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));
return array(
'content' => $accumulated['text'],
'tool_calls' => $accumulated['tool_calls'],
'usage' => $accumulated['usage'],
'parts' => $this->build_parts($accumulated)
);
}
private function anthropic_build_payload($messages, $tools, $options){
$system_msg = '';
$filtered = array();
foreach($messages as $msg){
if($msg['role'] === 'system'){
$system_msg .= $msg['content'] . "\n";
}else{
$filtered[] = $msg;
}
}
$payload = array(
'model' => $this->model,
'messages' => $filtered,
'max_tokens' => intval(isset($options['max_tokens']) ? $options['max_tokens'] : 8192)
);
// Anthropic prompt caching: cache system prompt with 1h TTL
if(!empty($system_msg)){
$payload['system'] = array(
array(
'type' => 'text',
'text' => trim($system_msg),
'cache_control' => array('type' => 'ephemeral', 'ttl' => '1h')
)
);
}
// Cache tools (last tool gets cache breakpoint)
if(!empty($tools)){
$tool_arr = array();
$tool_count = count($tools);
foreach($tools as $i => $t){
$tool = array(
'name' => $t['name'],
'description' => $t['description'],
'input_schema' => $t['parameters']
);
// Cache the last tool definition
if($i === $tool_count - 1){
$tool['cache_control'] = array('type' => 'ephemeral', 'ttl' => '1h');
}
$tool_arr[] = $tool;
}
$payload['tools'] = $tool_arr;
}
if(!empty($options['temperature'])){
$payload['temperature'] = floatval($options['temperature']);
}
// Cache last 2 messages for conversation continuity
$msg_count = count($filtered);
if($msg_count >= 2){
for($i = max(0, $msg_count - 2); $i < $msg_count; $i++){
if(!isset($filtered[$i]['content'])) continue;
// Convert string content to array format with cache_control
if(is_string($filtered[$i]['content'])){
$filtered[$i]['content'] = array(
array(
'type' => 'text',
'text' => $filtered[$i]['content'],
'cache_control' => array('type' => 'ephemeral')
)
);
}elseif(is_array($filtered[$i]['content'])){
// Add cache_control to last content block
$last_block = end($filtered[$i]['content']);
if($last_block){
$last_block['cache_control'] = array('type' => 'ephemeral');
$filtered[$i]['content'][count($filtered[$i]['content']) - 1] = $last_block;
}
}
}
$payload['messages'] = $filtered;
}
return $payload;
}
private function anthropic_parse_response($body){
$data = @json_decode($body, true);
if(!$data) return array('error' => __('Invalid JSON response'));
if(!empty($data['error'])) return array('error' => isset($data['error']['message']) ? $data['error']['message'] : json_encode($data['error']));
$text = '';
$tool_calls = array();
foreach(isset($data['content']) ? $data['content'] : array() as $block){
if((isset($block['type']) ? $block['type'] : '') === 'text'){
$text .= isset($block['text']) ? $block['text'] : '';
}elseif((isset($block['type']) ? $block['type'] : '') === 'tool_use'){
$tool_calls[] = array(
'id' => isset($block['id']) ? $block['id'] : '',
'name' => isset($block['name']) ? $block['name'] : '',
'input' => isset($block['input']) ? $block['input'] : array()
);
}
}
$accumulated = array('text' => $text, 'tool_calls' => $tool_calls, 'usage' => isset($data['usage']) ? $data['usage'] : array());
return array(
'content' => $text,
'tool_calls' => $tool_calls,
'usage' => isset($data['usage']) ? $data['usage'] : array(),
'parts' => $this->build_parts($accumulated)
);
}
private function google_chat($messages, $tools, $options){
$key = $this->api_key;
$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $this->model . ':generateContent?key=' . $key;
$headers = array('Content-Type: application/json');
$payload = $this->google_build_payload($messages, $tools, $options);
$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
if(!empty($response['error'])) return $response;
return $this->google_parse_response($response['body']);
}
private function google_stream($messages, $tools, $on_event, $options){
$key = $this->api_key;
$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $this->model . ':streamGenerateContent?alt=sse&key=' . $key;
$headers = array('Content-Type: application/json');
$payload = $this->google_build_payload($messages, $tools, $options);
$accumulated = array('text' => '', 'tool_calls' => array(), 'usage' => array());
$sse_buffer = '';
$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$sse_buffer){
$sse_buffer .= $chunk;
while(($pos = strpos($sse_buffer, "\n")) !== false){
$line = substr($sse_buffer, 0, $pos);
$sse_buffer = substr($sse_buffer, $pos + 1);
$line = trim($line);
if(strpos($line, 'data: ') !== 0) continue;
$data_str = substr($line, 6);
$data = @json_decode($data_str, true);
if(!$data) continue;
if(!empty($data['candidates'][0]['content']['parts'])){
foreach($data['candidates'][0]['content']['parts'] as $part){
if(!empty($part['text'])){
$accumulated['text'] .= $part['text'];
$on_event(array('type' => 'text_delta', 'text' => $part['text']));
}
if(!empty($part['functionCall'])){
$tc = array(
'id' => 'gcall_'.count($accumulated['tool_calls']),
'name' => isset($part['functionCall']['name']) ? $part['functionCall']['name'] : '',
'input' => isset($part['functionCall']['args']) ? $part['functionCall']['args'] : array()
);
$accumulated['tool_calls'][] = $tc;
$on_event(array('type' => 'tool_call', 'id' => $tc['id'], 'name' => $tc['name'], 'input' => $tc['input']));
}
}
}
if(!empty($data['usageMetadata'])){
$accumulated['usage'] = $data['usageMetadata'];
// Normalize Gemini cached tokens
if(!empty($data['usageMetadata']['cachedContentTokenCount'])){
$accumulated['usage']['cached_tokens'] = $data['usageMetadata']['cachedContentTokenCount'];
}
}
}
}, isset($options['timeout']) ? $options['timeout'] : 120);
$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));
return array(
'content' => $accumulated['text'],
'tool_calls' => $accumulated['tool_calls'],
'usage' => $accumulated['usage'],
'parts' => $this->build_parts($accumulated)
);
}
private function google_build_payload($messages, $tools, $options){
$contents = array();
$system_instruction = '';
// Build a tool_call_id => tool_name map from preceding assistant
// tool_calls. Google removed the 'function' role; tool results must
// now be sent as a user turn with a functionResponse part whose
// name must match the prior functionCall name, so we resolve it
// from the assistant tool_use that produced this result.
$tool_name_by_id = array();
foreach($messages as $m){
if(!isset($m['role']) || $m['role'] !== 'assistant') continue;
if(empty($m['tool_calls'])) continue;
foreach($m['tool_calls'] as $tc){
$tid = isset($tc['id']) ? $tc['id'] : '';
$tname = isset($tc['function']['name']) ? $tc['function']['name'] : (isset($tc['name']) ? $tc['name'] : '');
if($tid !== '' && $tname !== '') $tool_name_by_id[$tid] = $tname;
}
}
foreach($messages as $msg){
$role = isset($msg['role']) ? $msg['role'] : 'user';
if($role === 'system'){
$system_instruction .= $msg['content'] . "\n";
continue;
}
if($role === 'tool_result' || $role === 'tool'){
if(!empty($msg['content'])){
$tcid = isset($msg['tool_call_id']) ? $msg['tool_call_id'] : '';
$tool_name = isset($tool_name_by_id[$tcid]) ? $tool_name_by_id[$tcid] : '';
if($tool_name === ''){
// Google requires a non-empty functionResponse.name; use a
// stable synthetic name when the id can't be matched.
$tool_name = 'tool_' . abs(crc32((string)$tcid ?: 'unknown'));
}
$result_text = is_string($msg['content']) ? $msg['content'] : json_encode($msg['content']);
$contents[] = array(
'role' => 'user',
'parts' => array(array(
'functionResponse' => array(
'name' => $tool_name,
'response' => array(
'name' => $tool_name,
'content' => $result_text
)
)
))
);
}
continue;
}
if($role === 'assistant'){
// Emit functionCall parts for any tool_calls so Gemini sees the
// call that subsequent functionResponse parts answer.
$g_parts = array();
$content = isset($msg['content']) ? $msg['content'] : '';
if(is_array($content)) $content = json_encode($content);
if($content !== '' && $content !== null){
$g_parts[] = array('text' => $content);
}
if(!empty($msg['tool_calls'])){
foreach($msg['tool_calls'] as $tc){
$tname = isset($tc['function']['name']) ? $tc['function']['name'] : (isset($tc['name']) ? $tc['name'] : '');
$args_str = isset($tc['function']['arguments']) ? $tc['function']['arguments'] : '{}';
$args_arr = json_decode($args_str, true);
if(!is_array($args_arr)) $args_arr = array();
$g_parts[] = array('functionCall' => array('name' => $tname, 'args' => $args_arr));
}
}
if(!empty($g_parts)){
$contents[] = array('role' => 'model', 'parts' => $g_parts);
}
continue;
}
// Plain user message
$content = isset($msg['content']) ? $msg['content'] : '';
if(is_array($content)) $content = json_encode($content);
$contents[] = array(
'role' => 'user',
'parts' => array(array('text' => $content))
);
}
$payload = array('contents' => $contents);
if(!empty($system_instruction)){
$payload['systemInstruction'] = array('parts' => array(array('text' => trim($system_instruction))));
}
$gen_config = array();
if(!empty($options['temperature'])) $gen_config['temperature'] = floatval($options['temperature']);
if(!empty($options['max_tokens'])) $gen_config['maxOutputTokens'] = intval($options['max_tokens']);
if(!empty($gen_config)) $payload['generationConfig'] = $gen_config;
if(!empty($tools)){
$payload['tools'] = array(array('functionDeclarations' => array_map(function($t){
return array(
'name' => $t['name'],
'description' => $t['description'],
'parameters' => $t['parameters']
);
}, $tools)));
}
return $payload;
}
private function google_parse_response($body){
$data = @json_decode($body, true);
if(!$data) return array('error' => __('Invalid JSON response'));
if(!empty($data['error'])) return array('error' => isset($data['error']['message']) ? $data['error']['message'] : json_encode($data['error']));
$text = '';
$tool_calls = array();
$parts = isset($data['candidates'][0]['content']['parts']) ? $data['candidates'][0]['content']['parts'] : array();
foreach($parts as $part){
if(!empty($part['text'])) $text .= $part['text'];
if(!empty($part['functionCall'])){
$tool_calls[] = array(
'id' => 'gcall_'.count($tool_calls),
'name' => isset($part['functionCall']['name']) ? $part['functionCall']['name'] : '',
'input' => isset($part['functionCall']['args']) ? $part['functionCall']['args'] : array()
);
}
}
$usage = isset($data['usageMetadata']) ? $data['usageMetadata'] : array();
// Normalize Gemini cached tokens
if(!empty($data['usageMetadata']['cachedContentTokenCount'])){
$usage['cached_tokens'] = $data['usageMetadata']['cachedContentTokenCount'];
}
$accumulated = array('text' => $text, 'tool_calls' => $tool_calls, 'usage' => $usage);
return array(
'content' => $text,
'tool_calls' => $tool_calls,
'usage' => $usage,
'parts' => $this->build_parts($accumulated)
);
}
private function azure_chat($messages, $tools, $options){
$config = isset($this->provider_config) ? $this->provider_config : array();
$resource = isset($config['resource']) ? $config['resource'] : '{resource}';
$deployment = isset($config['deployment']) ? $config['deployment'] : $this->model;
$url = "https://{$resource}.openai.azure.com/openai/deployments/{$deployment}/chat/completions?api-version=2024-02-01";
$headers = array(
'Content-Type: application/json',
'api-key: ' . $this->api_key
);
$payload = $this->openai_build_payload($messages, $tools, $options);
$payload['stream'] = false;
$response = $this->curl_post($url, $headers, $payload, isset($options['timeout']) ? $options['timeout'] : 120);
if(!empty($response['error'])) return $response;
return $this->openai_parse_response($response['body']);
}
private function azure_stream($messages, $tools, $on_event, $options){
$config = isset($this->provider_config) ? $this->provider_config : array();
$resource = isset($config['resource']) ? $config['resource'] : '{resource}';
$deployment = isset($config['deployment']) ? $config['deployment'] : $this->model;
$url = "https://{$resource}.openai.azure.com/openai/deployments/{$deployment}/chat/completions?api-version=2024-02-01";
$headers = array(
'Content-Type: application/json',
'api-key: ' . $this->api_key
);
$payload = $this->openai_build_payload($messages, $tools, $options);
$payload['stream'] = true;
return $this->openai_stream_impl($url, $headers, $payload, $on_event, $options);
}
private function openai_stream_impl($url, $headers, $payload, $on_event, $options){
$accumulated = array('text' => '', 'reasoning' => '', 'tool_calls' => array(), 'usage' => array());
$tool_call_buffers = array();
$sse_buffer = '';
$this->curl_post_stream($url, $headers, $payload, function($chunk) use ($on_event, &$accumulated, &$tool_call_buffers, &$sse_buffer){
$sse_buffer .= $chunk;
while(($pos = strpos($sse_buffer, "\n")) !== false){
$line = substr($sse_buffer, 0, $pos);
$sse_buffer = substr($sse_buffer, $pos + 1);
$line = trim($line);
if(strpos($line, 'data: ') !== 0) continue;
$data_str = substr($line, 6);
if(trim($data_str) === '[DONE]') return;
$data = @json_decode($data_str, true);
if(!$data) continue;
if(!empty($data['choices'][0])){
$choice = $data['choices'][0];
$delta = isset($choice['delta']) ? $choice['delta'] : array();
if(!empty($delta['content'])){
$accumulated['text'] .= $delta['content'];
$on_event(array('type' => 'text_delta', 'text' => $delta['content']));
}
if(!empty($delta['reasoning_content'])){
$accumulated['reasoning'] .= $delta['reasoning_content'];
$on_event(array('type' => 'reasoning_delta', 'text' => $delta['reasoning_content']));
}
if(!empty($delta['tool_calls'])){
foreach($delta['tool_calls'] as $tc){
$idx = isset($tc['index']) ? $tc['index'] : 0;
if(!isset($tool_call_buffers[$idx])){
$tool_call_buffers[$idx] = array('id' => '', 'name' => '', 'arguments' => '');
}
if(!empty($tc['id'])) $tool_call_buffers[$idx]['id'] = $tc['id'];
if(!empty($tc['function']['name'])) $tool_call_buffers[$idx]['name'] = $tc['function']['name'];
if(!empty($tc['function']['arguments'])) $tool_call_buffers[$idx]['arguments'] .= $tc['function']['arguments'];
}
}
}
if(!empty($data['usage'])) $accumulated['usage'] = $data['usage'];
}
}, isset($options['timeout']) ? $options['timeout'] : 120);
if(!empty($tool_call_buffers)){
foreach($tool_call_buffers as $i => $tc){
$args = @json_decode($tc['arguments'], true);
if($args === null) $args = array();
$tool_call = array('id' => $tc['id'] ? $tc['id'] : 'call_'.$i, 'name' => $tc['name'], 'input' => $args);
$accumulated['tool_calls'][] = $tool_call;
$on_event(array('type' => 'tool_call', 'id' => $tool_call['id'], 'name' => $tool_call['name'], 'input' => $args));
}
}
$on_event(array('type' => 'done', 'usage' => $accumulated['usage']));
return array(
'content' => $accumulated['text'],
'tool_calls' => $accumulated['tool_calls'],
'usage' => $accumulated['usage'],
'parts' => $this->build_parts($accumulated)
);
}
private function build_parts($accumulated){
$parts = array();
if(!empty($accumulated['reasoning'])){
$parts[] = array('type' => 'reasoning', 'text' => $accumulated['reasoning']);
}
if(!empty($accumulated['text'])){
$parts[] = array('type' => 'text', 'text' => $accumulated['text']);
}
foreach($accumulated['tool_calls'] as $tc){
$parts[] = array('type' => 'tool_use', 'id' => $tc['id'], 'name' => $tc['name'], 'input' => $tc['input']);
}
return $parts;
}
private function curl_post($url, $headers, $payload, $timeout = 120){
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_CONNECTTIMEOUT => 10
));
$body = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if($body === false){
return array('error' => __('cURL error: $0', array($curl_error)));
}
if($http_code >= 400){
$err_data = @json_decode($body, true);
if(!empty($err_data['error']['message'])){
return array('error' => $err_data['error']['message']);
}
if(!empty($err_data['error'])){
$e = $err_data['error'];
$msg = is_string($e) ? $e : (isset($e['message']) ? $e['message'] : json_encode($e));
return array('error' => $msg);
}
return array('error' => __('HTTP $0: ', array($http_code)) . mb_substr($body, 0, 500));
}
return array('body' => $body);
}
private function curl_post_stream($url, $headers, $payload, $on_chunk, $timeout = 120){
$ch = curl_init($url);
$aborted = false;
$http_code = 0;
$error_body = '';
$in_error = false;
curl_setopt_array($ch, array(
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => false,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_LOW_SPEED_TIME => 60,
CURLOPT_LOW_SPEED_LIMIT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_WRITEFUNCTION => function($ch, $data) use ($on_chunk, &$aborted, &$http_code, &$error_body, &$in_error){
if($this->should_abort()){
$aborted = true;
return -1;
}
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($code > 0){
$http_code = $code;
if($code >= 400){
$in_error = true;
}
}
if($in_error){
$error_body .= $data;
return strlen($data);
}
$on_chunk($data);
return strlen($data);
}
));
curl_exec($ch);
$curl_error = curl_error($ch);
if($http_code === 0 && $curl_error !== ''){
curl_close($ch);
throw new \RuntimeException(__('cURL error: $0', array($curl_error)));
}
curl_close($ch);
if($aborted){
throw new \RuntimeException(__('Generation aborted by user'));
}
if($http_code >= 400){
$err_data = @json_decode($error_body, true);
$msg = '';
if(!empty($err_data['error']['message'])){
$msg = is_array($err_data['error']['message']) ? json_encode($err_data['error']['message']) : $err_data['error']['message'];
}elseif(!empty($err_data['error'])){
$e = $err_data['error'];
$msg = is_string($e) ? $e : (isset($e['message']) ? $e['message'] : json_encode($e));
}elseif(!empty($err_data['message'])){
$msg = $err_data['message'];
}
if($msg === ''){
$msg = 'HTTP ' . $http_code . ': ' . mb_substr($error_body, 0, 500);
}
throw new \RuntimeException($msg);
}
}
}
Back to Directory