Skip to content

Commit 5e4d5bd

Browse files
fix(proxy): rewrite /v1/memory/hybrid to namespaced engine route (DAK-6906) (#218)
The engine exposes hybrid search under POST /v1/namespaces/{ns}/hybrid only. The proxy now rewrites the shorthand /v1/memory/hybrid path to the session- scoped namespaced route before forwarding, so the frontend doesn't need to know the session namespace. Uses the session namespace already computed by the body agent_id rewrite when available, falling back to sessionNamespace(resolved.id) for requests without an agent_id in the body. 47/47 proxy tests pass including 2 new regression tests for the path rewrite (DAK-6906). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b3e8966 commit 5e4d5bd

3 files changed

Lines changed: 55 additions & 2 deletions

File tree

docker/playground/proxy/allowlist.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ const ALLOW = [
4040
// the engine returned 405 (DAK-6758).
4141
compile('POST', '/v1/memory/importance'),
4242
// Hybrid Search Tuner: vector_weight slider sends POST /v1/memory/hybrid.
43-
// Engine route: POST /v1/namespaces/{ns}/hybrid — proxy passes through; 404s from
44-
// namespaced route are acceptable (playground uses /memory/search fallback) (DAK-6898).
43+
// Engine route: POST /v1/namespaces/{ns}/hybrid — server.js rewrites the path
44+
// to the session-namespaced route before forwarding (DAK-6906).
4545
compile('POST', '/v1/memory/hybrid'),
4646

4747
// --- sessions (ChatMemorySession scenario: start, store, recall, end) ---

docker/playground/proxy/proxy.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,48 @@ test('llm-compare does NOT seed again on second call (DAK-6845)', async () => {
775775
assert.equal(seedCallCount, 1, 'seed should only fire once per session');
776776
});
777777

778+
// DAK-6906: /v1/memory/hybrid must be rewritten to /v1/namespaces/{ns}/hybrid before forwarding.
779+
// The engine only exposes hybrid search under the namespaced route.
780+
test('hybrid path is rewritten to namespaced engine route (DAK-6906)', async () => {
781+
const p = await startProxy({ rateLimitPerMin: 1000 });
782+
const sessionId = 'pg_hybridtest001';
783+
const expectedNs = sessionNamespace(sessionId);
784+
785+
await request(p.port, {
786+
method: 'POST',
787+
path: '/v1/memory/hybrid',
788+
headers: { 'content-type': 'application/json', 'x-playground-session': sessionId },
789+
body: JSON.stringify({ agent_id: 'playground-demo', query: 'test', vector_weight: 0.7 }),
790+
});
791+
792+
const seen = p.upstream.captured[0];
793+
// Proxy must rewrite the path to the internal _dakera_agent_ namespaced engine route.
794+
assert.equal(seen.url, `/v1/namespaces/_dakera_agent_${expectedNs}/hybrid`, 'path must use _dakera_agent_ internal namespace key');
795+
// Body agent_id must also be rewritten to the session namespace.
796+
const body = JSON.parse(seen.body);
797+
assert.equal(body.agent_id, expectedNs, 'agent_id in body must be the session namespace');
798+
await p.close();
799+
});
800+
801+
test('hybrid path rewrite uses session namespace deterministically (DAK-6906)', async () => {
802+
const p = await startProxy({ rateLimitPerMin: 1000 });
803+
const sessionId = 'pg_hybridtest002';
804+
805+
// Two requests with the same session should target the same namespace.
806+
await request(p.port, { method: 'POST', path: '/v1/memory/hybrid',
807+
headers: { 'content-type': 'application/json', 'x-playground-session': sessionId },
808+
body: JSON.stringify({ agent_id: 'playground-demo', query: 'first' }) });
809+
await request(p.port, { method: 'POST', path: '/v1/memory/hybrid',
810+
headers: { 'content-type': 'application/json', 'x-playground-session': sessionId },
811+
body: JSON.stringify({ agent_id: 'playground-demo', query: 'second' }) });
812+
813+
const url1 = p.upstream.captured[0].url;
814+
const url2 = p.upstream.captured[1].url;
815+
assert.equal(url1, url2, 'same session always maps to same namespace path');
816+
assert.ok(url1.startsWith('/v1/namespaces/_dakera_agent_playground-demo-'), 'path must use _dakera_agent_ prefix with session namespace');
817+
await p.close();
818+
});
819+
778820
test('llm-compare endpoint accessible via HTTP proxy (integration, DAK-6845)', async () => {
779821
const p = await startProxy({ rateLimitPerMin: 1000, openRouterApiKey: 'fake-key', llmRateLimitPer10Min: 5, llmCompareTimeoutMs: 5000 });
780822
// The proxy will try to call OpenRouter (fake-key, won't succeed) and Dakera upstream.

docker/playground/proxy/server.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,17 @@ function createServer(config, store) {
446446
forwardPath = fUrl.pathname + (fUrl.search || '');
447447
}
448448
} catch (_) {}
449+
450+
// Path rewrite: POST /v1/memory/hybrid → POST /v1/namespaces/_dakera_agent_{session-ns}/hybrid (DAK-6906).
451+
// The engine stores memories under the internal namespace key _dakera_agent_{agent_id} and the
452+
// hybrid search endpoint requires this full internal key in the URL path. The proxy translates
453+
// the shorthand /v1/memory/hybrid path using the session namespace so the frontend doesn't need
454+
// to know either the session namespace or the internal _dakera_agent_ prefix.
455+
if (path === '/v1/memory/hybrid') {
456+
const ns = rewrite ? rewrite.namespace : sessionNamespace(resolved.id);
457+
forwardPath = `/v1/namespaces/_dakera_agent_${ns}/hybrid`;
458+
}
459+
449460
forward(config, req, res, forwardPath, bodyBuf, outHeaders, rewrite);
450461
});
451462
}

0 commit comments

Comments
 (0)