The learning process was failing to communicate with the gateway because: 1. Gateway was sending 'Strict-Transport-Security' header on HTTP responses 2. Node.js fetch respects HSTS and upgrades subsequent requests to HTTPS 3. Gateway only has HTTP listener (localhost:3103), no HTTPS 4. Result: SSL 'packet length too long' error on second request attempt Solution: Modified registerHSTSMiddleware to only send HSTS header when the connection is already secure (HTTPS or x-forwarded-proto: https). HTTP connections will not get the HSTS header, preventing the forced upgrade.
98 lines
2.6 KiB
TypeScript
98 lines
2.6 KiB
TypeScript
/**
|
|
* Internal HTTP client for calling the LLM Gateway API.
|
|
* Used by learning jobs to run internal inference calls.
|
|
*/
|
|
|
|
import { logger } from './observability/logger.js';
|
|
|
|
const GATEWAY_URL = process.env['GATEWAY_URL'] ?? 'http://localhost:3100';
|
|
const INTERNAL_SECRET = process.env['INTERNAL_SECRET'] ?? 'internal-learning-secret';
|
|
|
|
export interface GatewayCallOptions {
|
|
taskType: string;
|
|
input: string;
|
|
userContext?: string;
|
|
caller?: string;
|
|
}
|
|
|
|
export interface GatewayCallResult {
|
|
output: string;
|
|
confidence: number;
|
|
model: string;
|
|
latencyMs: number;
|
|
}
|
|
|
|
export async function callGateway(opts: GatewayCallOptions): Promise<GatewayCallResult> {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 60_000);
|
|
|
|
try {
|
|
const response = await fetch(`${GATEWAY_URL}/v1/completion`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Caller': opts.caller ?? 'internal',
|
|
'X-Internal-Secret': INTERNAL_SECRET,
|
|
'Cache-Control': 'no-store',
|
|
},
|
|
body: JSON.stringify({
|
|
task_type: opts.taskType,
|
|
input: opts.input,
|
|
user_context: opts.userContext ?? '',
|
|
}),
|
|
signal: controller.signal,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const body = await response.text();
|
|
throw new Error(`Gateway returned ${response.status}: ${body.slice(0, 200)}`);
|
|
}
|
|
|
|
const data = (await response.json()) as {
|
|
output: string;
|
|
confidence: number;
|
|
model: string;
|
|
latency_ms: number;
|
|
};
|
|
|
|
return {
|
|
output: data.output,
|
|
confidence: data.confidence,
|
|
model: data.model,
|
|
latencyMs: data.latency_ms,
|
|
};
|
|
} catch (err) {
|
|
logger.error({ err, taskType: opts.taskType }, 'Gateway call failed');
|
|
throw err;
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
export async function postInternal(path: string, body: unknown): Promise<void> {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
|
|
try {
|
|
const response = await fetch(`${GATEWAY_URL}${path}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Internal-Secret': INTERNAL_SECRET,
|
|
'Cache-Control': 'no-store',
|
|
},
|
|
body: JSON.stringify(body),
|
|
signal: controller.signal,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
logger.warn({ path, status: response.status, text: text.slice(0, 200) }, 'Internal POST non-OK');
|
|
}
|
|
} catch (err) {
|
|
logger.error({ err, path }, 'Internal POST failed');
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|