PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
betterdocs / includes / AI / Providers / GeminiProvider.php

GeminiProvider.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.2, at includes/AI/Providers/GeminiProvider.php

169 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\AI\Providers;
4
5 /**
6 * Google Gemini provider (Generative Language API).
7 *
8 * Wire format differs from OpenAI: the system prompt goes in
9 * `system_instruction`, the conversation in `contents[].parts[].text` with
10 * roles user/model, and limits live under `generationConfig`. Auth is the
11 * `x-goog-api-key` header.
12 *
13 * @since 4.4.0
14 */
15 class GeminiProvider extends BaseProvider {
16
17 public function id() {
18 return 'gemini';
19 }
20
21 public function label() {
22 return 'Google Gemini';
23 }
24
25 /**
26 * @return string API base, no trailing slash.
27 */
28 protected function base_url() {
29 return 'https://generativelanguage.googleapis.com/v1beta';
30 }
31
32 /**
33 * Split normalized messages into a Gemini system_instruction string and a
34 * contents array with user/model roles.
35 *
36 * @param array $messages
37 * @return array array( string $system, array $contents )
38 */
39 protected function map_messages( $messages ) {
40 $system = array();
41 $contents = array();
42
43 foreach ( $messages as $message ) {
44 $role = isset( $message['role'] ) ? $message['role'] : 'user';
45 $content = isset( $message['content'] ) ? (string) $message['content'] : '';
46
47 if ( 'system' === $role ) {
48 $system[] = $content;
49 continue;
50 }
51
52 $contents[] = array(
53 'role' => ( 'assistant' === $role ) ? 'model' : 'user',
54 'parts' => array( array( 'text' => $content ) ),
55 );
56 }
57
58 return array( implode( "\n\n", $system ), $contents );
59 }
60
61 /**
62 * {@inheritDoc}
63 */
64 public function chat( $messages, $options = array() ) {
65 if ( empty( $this->api_key ) ) {
66 return new \WP_Error( 'no_api_key', sprintf( __( '%s API key is not configured.', 'betterdocs' ), $this->label() ) );
67 }
68
69 $model = $this->resolve_model( $options );
70 $context = isset( $options['context'] ) ? $options['context'] : null;
71 $max_tokens = $this->floor_tokens( isset( $options['max_tokens'] ) ? $options['max_tokens'] : 2500, $model, $context );
72
73 list( $system, $contents ) = $this->map_messages( $messages );
74
75 $payload = array(
76 'contents' => $contents,
77 'generationConfig' => array( 'maxOutputTokens' => (int) $max_tokens ),
78 );
79 if ( '' !== $system ) {
80 $payload['system_instruction'] = array( 'parts' => array( array( 'text' => $system ) ) );
81 }
82 if ( isset( $options['temperature'] ) && null !== $options['temperature'] ) {
83 $payload['generationConfig']['temperature'] = (float) $options['temperature'];
84 }
85
86 $url = $this->base_url() . '/models/' . rawurlencode( $model ) . ':generateContent';
87 $headers = array( 'Content-Type' => 'application/json', 'x-goog-api-key' => $this->api_key );
88 $timeout = isset( $options['timeout'] ) ? $options['timeout'] : 50;
89
90 $status = null;
91 $data = $this->post_json( $url, $headers, $payload, $timeout, $status );
92 if ( is_wp_error( $data ) ) {
93 return $data;
94 }
95
96 if ( ! empty( $data['error'] ) ) {
97 $raw = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Unknown API error.', 'betterdocs' );
98 // Classify by HTTP status so a retired model (404) is not reported as a
99 // quota error (429) — the Gemini API returns the raw text either way.
100 return new \WP_Error( 'provider_error', $this->classify_http_error( $status, $raw, $model ) );
101 }
102
103 $content = $this->extract_text( $data );
104 if ( '' === $content ) {
105 return new \WP_Error( 'no_content', sprintf( __( 'No content received from %s.', 'betterdocs' ), $this->label() ) );
106 }
107
108 $usage = $this->normalize_usage(
109 isset( $data['usageMetadata'] ) && is_array( $data['usageMetadata'] ) ? $data['usageMetadata'] : array(),
110 array( 'prompt' => 'promptTokenCount', 'completion' => 'candidatesTokenCount', 'total' => 'totalTokenCount' )
111 );
112
113 return $this->success(
114 $content,
115 $model,
116 $usage,
117 isset( $data['candidates'][0]['finishReason'] ) ? $data['candidates'][0]['finishReason'] : null
118 );
119 }
120
121 /**
122 * Concatenate all text parts of the first candidate.
123 *
124 * @param array $data
125 * @return string
126 */
127 protected function extract_text( $data ) {
128 if ( empty( $data['candidates'][0]['content']['parts'] ) || ! is_array( $data['candidates'][0]['content']['parts'] ) ) {
129 return '';
130 }
131 $text = '';
132 foreach ( $data['candidates'][0]['content']['parts'] as $part ) {
133 if ( isset( $part['text'] ) ) {
134 $text .= $part['text'];
135 }
136 }
137 return $text;
138 }
139
140 /**
141 * {@inheritDoc}
142 */
143 public function validate_key( $api_key = '' ) {
144 $api_key = $api_key !== '' ? $api_key : $this->api_key;
145
146 if ( empty( $api_key ) ) {
147 return array( 'valid' => false, 'message' => __( 'Please insert your API key to use AI features.', 'betterdocs' ) );
148 }
149
150 $response = wp_remote_get( $this->base_url() . '/models', array(
151 'headers' => array( 'x-goog-api-key' => $api_key ),
152 'timeout' => 15,
153 ) );
154
155 if ( is_wp_error( $response ) ) {
156 return array( 'valid' => false, 'message' => $response->get_error_message() );
157 }
158
159 $code = (int) wp_remote_retrieve_response_code( $response );
160 if ( 200 === $code ) {
161 return array( 'valid' => true, 'message' => __( 'Valid API Key', 'betterdocs' ) );
162 }
163
164 $body = json_decode( wp_remote_retrieve_body( $response ), true );
165 $message = isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Invalid API Key', 'betterdocs' );
166 return array( 'valid' => false, 'message' => $message );
167 }
168 }
169