Problem/Motivation
Currently, the ApiClient service is designed to be very simple: the generateText() method makes a call to the Gemini API, extracts only the text portion of the response, and returns it as a string.
All other valuable information from the API response, most importantly the usageMetadata object which contains the totalTokenCount, is discarded.
This makes it impossible to perform essential tasks like:
Tracking token usage for cost analysis and budgeting.
Logging API consumption per user or per action.
Building custom ECA (Events - Conditions - Actions) workflows that react to the number of tokens used.
My specific use case is to create a custom ECA Action Plugin that calls the ApiClient service and then saves the totalTokenCount to a field on a Drupal entity. This is currently not possible because the data is not exposed by the service.
Steps to reproduce
Proposed resolution
The proposed solution is to make a small, non-breaking addition to the ApiClient service to make it more extensible.
Add a private property, $lastResponse, to the ApiClient class to store the full, decoded JSON response from the most recent API call.
In the generateText() method, populate this property with the response data before returning the text string.
Add a new public getter method, getLastResponse(): ?array, that allows other services and plugins to retrieve the full response data after a call has been made.
This change maintains perfect backward compatibility for existing integrations while enabling advanced use cases for developers who need access to the full API response.
Here is the suggested code for the gemini_provider/src/ApiClient.php file, incorporating the changes needed to expose the API usage data.
The new additions are clearly marked with // <-- ADDED comments for clarity.
namespace Drupal\gemini_provider;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
/**
* Service to interact with the Gemini API.
*/
class ApiClient {
/**
* The HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected ClientInterface $httpClient;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* The logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Stores the full decoded JSON response from the last API call.
*
* @var ?array
*/
protected ?array $lastResponse = NULL; // <-- ADDED
/**
* Constructs an ApiClient object.
*
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory.
*/
public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
$this->httpClient = $http_client;
$this->configFactory = $config_factory;
$this->logger = $logger_factory->get('gemini_provider');
}
/**
* Generates text from a prompt.
*
* @param string $prompt
* The prompt to generate text from.
*
* @return string|null
* The generated text, or null on error.
*/
public function generateText(string $prompt): ?string {
$config = $this->configFactory->get('gemini_provider.settings');
$apiKey = $config->get('api_key');
$model = $config->get('model');
$url = "https://generativelanguage.googleapis.com/v1beta/models/$model:generateContent";
$options = [
'headers' => [
'Content-Type' => 'application/json',
'x-goog-api-key' => $apiKey,
],
'json' => [
'contents' => [
[
'parts' => [
[
'text' => $prompt,
],
],
],
],
],
];
try {
$response = $this->httpClient->post($url, $options);
$data = json_decode($response->getBody()->getContents(), TRUE);
$this->lastResponse = $data; // <-- ADDED
return $data['candidates'][0]['content']['parts'][0]['text'] ?? NULL;
}
catch (RequestException $e) {
$this->lastResponse = NULL; // <-- ADDED
$this->logger->error('Error generating text: @message', ['@message' => $e->getMessage()]);
}
return NULL;
}
/**
* Returns the full decoded response from the most recent API call.
*
* @return ?array
* The last response, or null if no call has been made or an error occurred.
*/
public function getLastResponse(): ?array { // <-- ADDED
return $this->lastResponse;
}
}
Remaining tasks
Review the proposed approach.
Create a patch with the changes.
Commit the patch.
User interface changes
API changes
A new, non-breaking public method will be added to the gemini_provider.api_client service:
public function getLastResponse(): ?array
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #2 | gemini_expose-response-metadata-3444555-1.patch | 1.1 KB | maxilein |
Issue fork gemini_provider-3549099
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
maxilein commentedComment #3
marcus_johansson commentedComment #4
jibla commented@maxilein - can you create a MR?
Comment #7
a.dmitriiev commentedChatOutputfrom AI Core already allows to attach token usage information. I have created a MR that attaches this information forchatoperation. Please review.Comment #8
arianraeesi commentedComment #9
jibla commentedThanks for the MR @a.dmitriiev!
The implementation looks good and I've tested it successfully - token usage is correctly mapped from Gemini's usageMetadata to AI Core's TokenUsageDto.
I found two minor issues that should be addressed:
1. Typo in .cspell-project-words.txt: Line 5 has metatata but should be metadata.
2. Unnecessary loadApiKey() method: The MR adds a new loadApiKey() method, but the parent class AiProviderClientBase already has this method with better error handling (throws AiSetupFailureException with a helpful message if the key is missing). The new override would throw a generic PHP error instead. Please remove this method to use the parent's implementation. #3560493: Improve handling when no key/missing key is configured
Once these are fixed, this is ready to merge!
Comment #10
jibla commentedComment #11
a.dmitriiev commentedI rebased the MR and the second problem is gone now. The
metatatawas also removed as the word was fixed in the code to correct one :)Please check once again
Comment #12
jibla commentedRe-reviewed the rebased MR. Both issues have been addressed:
1. Typo in `.cspell-project-words.txt` - Fixed.
2. Unnecessary `loadApiKey()` method - Removed.
Will merge this.
@a.dmitriiev thanks!
Comment #13
jibla commentedComment #15
arianraeesi commentedComment #16
jibla commented