fix(brain): wire Qdrant api-key header from BRAIN_QDRANT_API_KEY
BrainService::http() was building a PendingRequest with no auth
header, so when Qdrant has auth enabled (the production lthn.sh
deploy does) every upsert/lookup returned 401. The circuit breaker
logged the 401 via Cache::store('file'), which was the red-herring
cache-write error chased in the first #97 iteration.
Changes:
- BrainService loads + trims a Qdrant api key from
config('brain.qdrant.api_key') in the constructor.
- New qdrantHttp() helper returns a PendingRequest with the
api-key header when the key is non-empty, or the plain client
otherwise. Ollama + Elasticsearch call sites still use http()
(separate auth shapes).
- php/config.php adds a brain.qdrant.api_key entry reading
env('BRAIN_QDRANT_API_KEY').
- Good/Bad/Ugly Pest tests cover: configured key → header sent,
unset → header absent, empty-string → header absent.
Closes tasks.lthn.sh/view.php?id=97
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
7639f56c2d
commit
4c1fa56d17
3 changed files with 109 additions and 6 deletions
|
|
@ -20,6 +20,8 @@ class BrainService
|
|||
|
||||
private const ELASTIC_INDEX = 'brain_memories';
|
||||
|
||||
private string $qdrantApiKey;
|
||||
|
||||
public function __construct(
|
||||
private string $ollamaUrl = 'http://localhost:11434',
|
||||
private string $qdrantUrl = 'http://localhost:6334',
|
||||
|
|
@ -27,7 +29,15 @@ class BrainService
|
|||
private string $embeddingModel = self::DEFAULT_MODEL,
|
||||
private bool $verifySsl = true,
|
||||
private string $elasticsearchUrl = 'http://127.0.0.1:9200',
|
||||
) {}
|
||||
?string $qdrantApiKey = null,
|
||||
) {
|
||||
if ($qdrantApiKey === null && function_exists('config')) {
|
||||
$configuredQdrantApiKey = config('mcp.brain.qdrant.api_key', config('brain.qdrant.api_key', ''));
|
||||
$qdrantApiKey = is_string($configuredQdrantApiKey) ? $configuredQdrantApiKey : '';
|
||||
}
|
||||
|
||||
$this->qdrantApiKey = trim((string) $qdrantApiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP client with common settings.
|
||||
|
|
@ -39,6 +49,22 @@ class BrainService
|
|||
: Http::withoutVerifying()->timeout($timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP client for Qdrant requests.
|
||||
*/
|
||||
private function qdrantHttp(int $timeout = 10): PendingRequest
|
||||
{
|
||||
$request = $this->http($timeout);
|
||||
|
||||
if ($this->qdrantApiKey === '') {
|
||||
return $request;
|
||||
}
|
||||
|
||||
return $request->withHeaders([
|
||||
'api-key' => $this->qdrantApiKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an embedding vector for the given text.
|
||||
*
|
||||
|
|
@ -114,7 +140,7 @@ class BrainService
|
|||
$filter['workspace_id'] = $workspaceId;
|
||||
$qdrantFilter = $this->buildQdrantFilter($filter);
|
||||
|
||||
$response = $this->http(10)
|
||||
$response = $this->qdrantHttp(10)
|
||||
->post("{$this->qdrantUrl}/collections/{$this->collection}/points/search", [
|
||||
'vector' => $vector,
|
||||
'filter' => $qdrantFilter,
|
||||
|
|
@ -200,11 +226,11 @@ class BrainService
|
|||
*/
|
||||
public function ensureCollection(): void
|
||||
{
|
||||
$response = $this->http(5)
|
||||
$response = $this->qdrantHttp(5)
|
||||
->get("{$this->qdrantUrl}/collections/{$this->collection}");
|
||||
|
||||
if ($response->status() === 404) {
|
||||
$createResponse = $this->http(10)
|
||||
$createResponse = $this->qdrantHttp(10)
|
||||
->put("{$this->qdrantUrl}/collections/{$this->collection}", [
|
||||
'vectors' => [
|
||||
'size' => self::VECTOR_DIMENSION,
|
||||
|
|
@ -552,7 +578,7 @@ class BrainService
|
|||
*/
|
||||
public function qdrantUpsert(array $points): void
|
||||
{
|
||||
$response = $this->http(10)
|
||||
$response = $this->qdrantHttp(10)
|
||||
->put("{$this->qdrantUrl}/collections/{$this->collection}/points", [
|
||||
'points' => $points,
|
||||
]);
|
||||
|
|
@ -572,7 +598,7 @@ class BrainService
|
|||
*/
|
||||
public function qdrantDelete(array $ids): void
|
||||
{
|
||||
$response = $this->http(10)
|
||||
$response = $this->qdrantHttp(10)
|
||||
->post("{$this->qdrantUrl}/collections/{$this->collection}/points/delete", [
|
||||
'points' => $ids,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,9 @@ return [
|
|||
'brain' => [
|
||||
'ollama_url' => env('BRAIN_OLLAMA_URL', 'https://ollama.lthn.sh'),
|
||||
'qdrant_url' => env('BRAIN_QDRANT_URL', 'https://qdrant.lthn.sh'),
|
||||
'qdrant' => [
|
||||
'api_key' => env('BRAIN_QDRANT_API_KEY', ''),
|
||||
],
|
||||
'collection' => env('BRAIN_COLLECTION', 'openbrain'),
|
||||
'embedding_model' => env('BRAIN_EMBEDDING_MODEL', 'embeddinggemma'),
|
||||
|
||||
|
|
|
|||
74
php/tests/Unit/BrainServiceTest.php
Normal file
74
php/tests/Unit/BrainServiceTest.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
use Illuminate\Http\Client\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
function qdrantHeaderBrainService(): BrainService
|
||||
{
|
||||
return new BrainService(
|
||||
ollamaUrl: 'https://ollama.test',
|
||||
qdrantUrl: 'https://qdrant.test',
|
||||
collection: 'openbrain',
|
||||
embeddingModel: 'embeddinggemma',
|
||||
verifySsl: false,
|
||||
elasticsearchUrl: 'https://elasticsearch.test',
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(function (): void {
|
||||
Config::set('mcp.brain.qdrant.api_key', null);
|
||||
});
|
||||
|
||||
test('BrainService_qdrantUpsert_Good_sends_api_key_header_when_configured', function (): void {
|
||||
Config::set('mcp.brain.qdrant.api_key', 'qdrant-secret');
|
||||
|
||||
Http::fake([
|
||||
'https://qdrant.test/collections/openbrain/points' => Http::response(['status' => 'ok']),
|
||||
]);
|
||||
|
||||
qdrantHeaderBrainService()->qdrantUpsert([
|
||||
['id' => 'memory-1', 'vector' => [0.1, 0.2], 'payload' => ['type' => 'note']],
|
||||
]);
|
||||
|
||||
Http::assertSent(fn (Request $request): bool => $request->url() === 'https://qdrant.test/collections/openbrain/points'
|
||||
&& $request->method() === 'PUT'
|
||||
&& $request->hasHeader('api-key', 'qdrant-secret'));
|
||||
});
|
||||
|
||||
test('BrainService_qdrantUpsert_Bad_omits_api_key_header_when_unset', function (): void {
|
||||
Config::set('mcp.brain.qdrant.api_key', null);
|
||||
|
||||
Http::fake([
|
||||
'https://qdrant.test/collections/openbrain/points' => Http::response(['status' => 'ok']),
|
||||
]);
|
||||
|
||||
qdrantHeaderBrainService()->qdrantUpsert([
|
||||
['id' => 'memory-2', 'vector' => [0.3, 0.4], 'payload' => ['type' => 'note']],
|
||||
]);
|
||||
|
||||
Http::assertSent(fn (Request $request): bool => $request->url() === 'https://qdrant.test/collections/openbrain/points'
|
||||
&& $request->method() === 'PUT'
|
||||
&& ! $request->hasHeader('api-key'));
|
||||
});
|
||||
|
||||
test('BrainService_qdrantUpsert_Ugly_treats_empty_api_key_as_unset', function (): void {
|
||||
Config::set('mcp.brain.qdrant.api_key', '');
|
||||
|
||||
Http::fake([
|
||||
'https://qdrant.test/collections/openbrain/points' => Http::response(['status' => 'ok']),
|
||||
]);
|
||||
|
||||
qdrantHeaderBrainService()->qdrantUpsert([
|
||||
['id' => 'memory-3', 'vector' => [0.5, 0.6], 'payload' => ['type' => 'note']],
|
||||
]);
|
||||
|
||||
Http::assertSent(fn (Request $request): bool => $request->url() === 'https://qdrant.test/collections/openbrain/points'
|
||||
&& $request->method() === 'PUT'
|
||||
&& ! $request->hasHeader('api-key'));
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue