Skip to content

Commit ded081d

Browse files
committed
[FEATURE] Allow adding additional metadata to ai request
1 parent 33203f4 commit ded081d

14 files changed

Lines changed: 467 additions & 203 deletions

File tree

Classes/Request/AiRequestInterface.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,13 @@ public function getConfiguration(): ProviderConfiguration;
3030
* Used during fallback to swap the API key/model without reflection.
3131
*/
3232
public function withConfiguration(ProviderConfiguration $configuration): static;
33+
34+
/**
35+
* Return a copy of this request with additional metadata merged in.
36+
*
37+
* Example:
38+
* $request = $request->withMetadata(['my_extension.context' => $value]);
39+
* return $next->handle($request, $provider, $configuration);
40+
*/
41+
public function withMetadata(array $additional): static;
3342
}

Classes/Request/ConversationRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,12 @@ public function withConfiguration(ProviderConfiguration $configuration): static
4444
['configuration' => $configuration],
4545
));
4646
}
47+
48+
public function withMetadata(array $additional): static
49+
{
50+
return new static(...array_merge(
51+
get_object_vars($this),
52+
['metadata' => [...$this->metadata, ...$additional]],
53+
));
54+
}
4755
}

Classes/Request/EmbeddingRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,12 @@ public function withConfiguration(ProviderConfiguration $configuration): static
4040
['configuration' => $configuration],
4141
));
4242
}
43+
44+
public function withMetadata(array $additional): static
45+
{
46+
return new static(...array_merge(
47+
get_object_vars($this),
48+
['metadata' => [...$this->metadata, ...$additional]],
49+
));
50+
}
4351
}

Classes/Request/TextGenerationRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,12 @@ public function withConfiguration(ProviderConfiguration $configuration): static
3939
['configuration' => $configuration],
4040
));
4141
}
42+
43+
public function withMetadata(array $additional): static
44+
{
45+
return new static(...array_merge(
46+
get_object_vars($this),
47+
['metadata' => [...$this->metadata, ...$additional]],
48+
));
49+
}
4250
}

Classes/Request/ToolCallingRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,12 @@ public function withConfiguration(ProviderConfiguration $configuration): static
5454
['configuration' => $configuration],
5555
));
5656
}
57+
58+
public function withMetadata(array $additional): static
59+
{
60+
return new static(...array_merge(
61+
get_object_vars($this),
62+
['metadata' => [...$this->metadata, ...$additional]],
63+
));
64+
}
5765
}

Classes/Request/TranslationRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,12 @@ public function withConfiguration(ProviderConfiguration $configuration): static
4040
['configuration' => $configuration],
4141
));
4242
}
43+
44+
public function withMetadata(array $additional): static
45+
{
46+
return new static(...array_merge(
47+
get_object_vars($this),
48+
['metadata' => [...$this->metadata, ...$additional]],
49+
));
50+
}
4351
}

Classes/Request/VisionRequest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,12 @@ public function withConfiguration(ProviderConfiguration $configuration): static
4040
['configuration' => $configuration],
4141
));
4242
}
43+
44+
public function withMetadata(array $additional): static
45+
{
46+
return new static(...array_merge(
47+
get_object_vars($this),
48+
['metadata' => [...$this->metadata, ...$additional]],
49+
));
50+
}
4351
}

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,60 @@ class MyMiddleware implements AiMiddlewareInterface
457457
}
458458
```
459459

460+
### Enriching the request log
461+
462+
Every request DTO carries a `metadata` array that lands in the `metadata` JSON column of `tx_aim_request_log`. To attach extension-specific context, enrich it from your custom middleware via `$request->withMetadata([...])` and forward the new instance. The original request stays immutable; downstream middlewares see the merged metadata:
463+
464+
```php
465+
#[AsAiMiddleware(priority: 80)]
466+
final class MyExtensionContextMiddleware implements AiMiddlewareInterface
467+
{
468+
public function process(
469+
AiRequestInterface $request,
470+
AiProviderInterface $provider,
471+
ProviderConfiguration $configuration,
472+
AiMiddlewareHandler $next,
473+
): TextResponse {
474+
$request = $request->withMetadata([
475+
'my_ext.additional' => 'info',
476+
]);
477+
return $next->handle($request, $provider, $configuration);
478+
}
479+
}
480+
```
481+
482+
### Detailed / parallel logging
483+
484+
For richer or separate logging, register a middleware at a lower priority than `RequestLoggingMiddleware` (use a priority below `-700`). It sees the response, the resolved `$configuration`, and any metadata enriched by earlier middlewares, and is free to write wherever it likes without touching `tx_aim_request_log`:
485+
486+
```php
487+
#[AsAiMiddleware(priority: -750)]
488+
final class MyExtensionDetailedLogger implements AiMiddlewareInterface
489+
{
490+
public function __construct(private readonly MyExtensionLogRepository $repository) {}
491+
492+
public function process(
493+
AiRequestInterface $request,
494+
AiProviderInterface $provider,
495+
ProviderConfiguration $configuration,
496+
AiMiddlewareHandler $next,
497+
): TextResponse {
498+
$response = $next->handle($request, $provider, $configuration);
499+
$this->repository->record([
500+
'provider' => $configuration->providerIdentifier,
501+
'model' => $response->usage->modelUsed,
502+
'metadata' => $request->metadata,
503+
'tokens' => $response->usage->getTotalTokens(),
504+
'cost' => $response->usage->cost,
505+
// ...any custom shape you need
506+
]);
507+
return $response;
508+
}
509+
}
510+
```
511+
512+
The middleware pipeline is intentionally the only logging extension point: it gives you the request, response, configuration, and middleware context in one place, plus full control over where the data goes.
513+
460514
### Built-in Middleware
461515

462516
| Middleware | Priority | Purpose |
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
2+
3+
<form action="{f:be.uri(route:'aim_request_log')}" method="post" enctype="multipart/form-data" name="demand">
4+
<input type="hidden" name="orderField" value="{demand.orderField}">
5+
<input type="hidden" name="orderDirection" value="{demand.orderDirection}">
6+
<div class="form-row">
7+
<div class="form-group">
8+
<label for="demand-provider" class="form-label"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.provider" /></label>
9+
<select id="demand-provider" class="form-select" name="demand[provider_identifier]" data-on-change="submit">
10+
<option value=""><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.showAll" /></option>
11+
<f:for each="{providers}" as="provider">
12+
<option value="{provider}" {f:if(condition: '{provider} == {demand.providerIdentifier}', then: 'selected')}>{provider}</option>
13+
</f:for>
14+
</select>
15+
</div>
16+
<div class="form-group">
17+
<label for="demand-extension" class="form-label"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.extension" /></label>
18+
<select id="demand-extension" class="form-select" name="demand[extension_key]" data-on-change="submit">
19+
<option value=""><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.showAll" /></option>
20+
<f:for each="{extensionKeys}" as="ext">
21+
<option value="{ext}" {f:if(condition: '{ext} == {demand.extensionKey}', then: 'selected')}>{ext}</option>
22+
</f:for>
23+
</select>
24+
</div>
25+
<div class="form-group">
26+
<label for="demand-type" class="form-label"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.requestType" /></label>
27+
<select id="demand-type" class="form-select" name="demand[request_type]" data-on-change="submit">
28+
<option value=""><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.showAll" /></option>
29+
<f:for each="{requestTypes}" as="type">
30+
<option value="{type}" {f:if(condition: '{type} == {demand.requestType}', then: 'selected')}>{type}</option>
31+
</f:for>
32+
</select>
33+
</div>
34+
<div class="form-group">
35+
<label for="demand-model" class="form-label"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.model" /></label>
36+
<select id="demand-model" class="form-select" name="demand[model_used]" data-on-change="submit">
37+
<option value=""><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.showAll" /></option>
38+
<f:for each="{models}" as="model">
39+
<option value="{model}" {f:if(condition: '{model} == {demand.modelUsed}', then: 'selected')}>{model}</option>
40+
</f:for>
41+
</select>
42+
</div>
43+
<div class="form-group">
44+
<label for="demand-status" class="form-label"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.status" /></label>
45+
<select id="demand-status" class="form-select" name="demand[success]" data-on-change="submit">
46+
<option value=""><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.status.all" /></option>
47+
<option value="1" {f:if(condition: '{demand.hasSuccess} && {demand.success}', then: 'selected')}><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.status.success" /></option>
48+
<option value="0" {f:if(condition: '{demand.hasSuccess} && !{demand.success}', then: 'selected')}><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.filter.status.failed" /></option>
49+
</select>
50+
</div>
51+
<div class="form-group align-self-end">
52+
<input type="submit" value="{f:translate(key: 'LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:filter.submit')}" class="btn btn-default" />
53+
<a href="{f:be.uri(route:'aim_request_log')}" class="btn btn-link"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:filter.reset" /></a>
54+
</div>
55+
</div>
56+
</form>
57+
58+
</html>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
2+
3+
<tr>
4+
<td>{entry.crdate -> f:format.date(format: 'Y-m-d H:i:s')}</td>
5+
<td>
6+
<f:if condition="{entry.extension_key}">
7+
<f:then>
8+
<f:if condition="{extensionIcons.{entry.extension_key}}">
9+
<f:then><img src="{extensionIcons.{entry.extension_key}}" width="16" height="16" alt="" /></f:then>
10+
</f:if>
11+
{entry.extension_key}
12+
</f:then>
13+
<f:else><span class="text-body-secondary">-</span></f:else>
14+
</f:if>
15+
</td>
16+
<td>
17+
<f:if condition="{userMap.{entry.user_id}}">
18+
<f:then><span class="text-body-secondary">{userMap.{entry.user_id}}</span></f:then>
19+
<f:else><f:if condition="{entry.user_id}"><span class="text-body-tertiary">#{entry.user_id}</span></f:if></f:else>
20+
</f:if>
21+
</td>
22+
<td>{entry.request_type}</td>
23+
<td>{entry.provider_identifier}</td>
24+
<td>
25+
<span class="badge badge-secondary">{entry.model_used -> f:or(alternative: entry.model_requested)}</span>
26+
<f:if condition="{entry.model_used} && {entry.model_requested} && {entry.model_used} != {entry.model_requested}">
27+
<br /><small class="text-body-secondary">requested: {entry.model_requested}</small>
28+
</f:if>
29+
</td>
30+
<td>
31+
<strong>{entry.total_tokens -> f:format.number(decimals: 0, thousandsSeparator: ',')}</strong>
32+
<br /><small class="text-body-secondary">{entry.prompt_tokens} / {entry.completion_tokens}</small>
33+
<f:if condition="{entry.cached_tokens} > 0">
34+
<br /><small class="text-success">cached: {entry.cached_tokens}</small>
35+
</f:if>
36+
<f:if condition="{entry.reasoning_tokens} > 0">
37+
<br /><small class="text-warning">reasoning: {entry.reasoning_tokens}</small>
38+
</f:if>
39+
</td>
40+
<td>{entry.cost -> f:format.number(decimals: 6)}</td>
41+
<td>{entry.duration_ms -> f:format.number(decimals: 0, thousandsSeparator: ',')} ms</td>
42+
<td>
43+
<f:if condition="{entry.rerouted}">
44+
<f:if condition="{entry.reroute_type} == 'fallback'">
45+
<f:then><span class="badge badge-info" title="{entry.reroute_reason -> f:format.htmlspecialchars()}"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.status.fallback" /></span></f:then>
46+
<f:else><span class="badge badge-warning" title="{entry.reroute_reason -> f:format.htmlspecialchars()}"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.status.rerouted" /></span></f:else>
47+
</f:if>
48+
</f:if>
49+
<f:if condition="{entry.success}">
50+
<f:then>
51+
<span class="badge badge-success"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.status.success" /></span>
52+
</f:then>
53+
<f:else>
54+
<span class="badge badge-danger" title="{entry.error_message -> f:format.htmlspecialchars()}"><f:translate key="LLL:EXT:aim/Resources/Private/Language/locallang_module.xlf:requestLog.status.failed" /></span>
55+
</f:else>
56+
</f:if>
57+
</td>
58+
</tr>
59+
60+
</html>

0 commit comments

Comments
 (0)