Files
makelore/electron/api/routes/ai-hardware.ts

593 lines
26 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { HostApiContext } from '../context';
import { sendJson } from '../route-utils';
import { WORKS_SQUARE_CONFIG } from '../works-config';
import { getValidWorksSquareAccessToken } from '../../services/works-square-session';
import { proxyAwareFetch } from '../../utils/proxy-fetch';
const MAX_REQUEST_BYTES = 64 * 1024;
const MAX_RESPONSE_BYTES = 256 * 1024;
const DEFAULT_TIMEOUT_MS = 15_000;
const LOCAL_ROOT = '/api/works/ai-hardware';
const UPSTREAM_ROOT = '/api/ai-hardware';
const STRONG_REVISION = /^"(0|[1-9]\d*)"$/;
const LOCAL_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,35}$/;
const OPERATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const MAX_RETRY_AFTER_SECONDS = 2;
type TokenGetter = typeof getValidWorksSquareAccessToken;
export type AiHardwareRouteDependencies = {
fetchImpl?: typeof fetch;
getAccessToken?: TokenGetter;
apiBaseUrl?: string;
randomUuid?: () => string;
timeoutMs?: number;
};
class SafeRouteError extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
readonly retryable = false,
) {
super(message);
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function asRequiredString(value: unknown): string | null {
return typeof value === 'string' && value.trim() ? value.trim() : null;
}
function asRevision(value: unknown): number | null {
return Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : null;
}
function projectAgent(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
const id = asRequiredString(value.id);
const name = asRequiredString(value.name);
const configRevision = asRevision(value.config_revision);
return id && name && configRevision !== null
? { id, name, config_revision: configRevision }
: null;
}
function projectDevice(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
const id = asRequiredString(value.id);
const agentId = asRequiredString(value.agent_id);
const assignmentRevision = asRevision(value.assignment_revision);
return id && agentId && assignmentRevision !== null
? { id, agent_id: agentId, assignment_revision: assignmentRevision }
: null;
}
const CONFIG_STRING_FIELDS = [
'system_prompt', 'lang_code', 'language', 'asr_model_id', 'vad_model_id', 'llm_model_id',
'slm_model_id', 'vllm_model_id', 'tts_model_id', 'tts_voice_id', 'tts_language',
'mem_model_id', 'intent_model_id',
] as const;
const CONFIG_NUMBER_FIELDS = ['tts_volume', 'tts_rate', 'tts_pitch'] as const;
const CONFIG_STRING_LIMITS: Record<string, number> = {
system_prompt: 16000, lang_code: 10, language: 10, asr_model_id: 32, vad_model_id: 64,
llm_model_id: 32, slm_model_id: 255, vllm_model_id: 32, tts_model_id: 32,
tts_voice_id: 32, tts_language: 50, mem_model_id: 32, intent_model_id: 32,
};
const CLEARABLE_CONFIG_FIELDS = new Set<string>([
...CONFIG_STRING_FIELDS, ...CONFIG_NUMBER_FIELDS,
]);
const CONFIG_UPDATE_FIELDS = new Set<string>([
'agent_name', ...CONFIG_STRING_FIELDS, ...CONFIG_NUMBER_FIELDS, 'chat_history_conf', 'clear_fields',
]);
function projectAgentConfig(value: unknown): Record<string, unknown> | null {
const agent = projectAgent(value);
if (!agent || !isRecord(value)) return null;
const result: Record<string, unknown> = { ...agent };
for (const field of CONFIG_STRING_FIELDS) {
const fieldValue = value[field];
if (fieldValue !== null && typeof fieldValue !== 'string') return null;
result[field] = fieldValue;
}
for (const field of CONFIG_NUMBER_FIELDS) {
const fieldValue = value[field];
if (fieldValue !== null && (!Number.isInteger(fieldValue) || (fieldValue as number) < -100 || (fieldValue as number) > 100)) return null;
result[field] = fieldValue;
}
const chatHistory = value.chat_history_conf;
if (chatHistory !== null && (!Number.isInteger(chatHistory) || (chatHistory as number) < 0 || (chatHistory as number) > 2)) return null;
result.chat_history_conf = chatHistory;
return result;
}
function projectOverview(value: unknown): Record<string, unknown> | null {
if (!isRecord(value) || !Array.isArray(value.agents) || !Array.isArray(value.devices)) return null;
const allowedStatuses = new Set([
'unprovisioned', 'provisioning', 'active', 'credential_recovery_required', 'invalid',
]);
if (typeof value.status !== 'string' || !allowedStatuses.has(value.status)) return null;
const agents = value.agents.map(projectAgent);
const devices = value.devices.map(projectDevice);
if (agents.some((item) => item === null) || devices.some((item) => item === null)) return null;
return { status: value.status, agents, devices };
}
async function readBoundedJson(req: IncomingMessage): Promise<Record<string, unknown>> {
const declared = Number(req.headers['content-length']);
if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES) {
throw new SafeRouteError(413, 'AI_HARDWARE_REQUEST_TOO_LARGE', 'AI hardware request is too large');
}
const chunks: Buffer[] = [];
let size = 0;
for await (const chunk of req) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
size += buffer.byteLength;
if (size > MAX_REQUEST_BYTES) {
throw new SafeRouteError(413, 'AI_HARDWARE_REQUEST_TOO_LARGE', 'AI hardware request is too large');
}
chunks.push(buffer);
}
try {
const value = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown;
if (!isRecord(value)) throw new Error('not an object');
return value;
} catch {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
}
async function readBoundedResponse(response: Response): Promise<unknown> {
const declared = Number(response.headers.get('content-length'));
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES) {
await response.body?.cancel().catch(() => undefined);
throw new SafeRouteError(502, 'AI_HARDWARE_RESPONSE_TOO_LARGE', 'AI hardware service returned too much data', true);
}
if (!response.body) return null;
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let size = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > MAX_RESPONSE_BYTES) {
await reader.cancel().catch(() => undefined);
throw new SafeRouteError(502, 'AI_HARDWARE_RESPONSE_TOO_LARGE', 'AI hardware service returned too much data', true);
}
chunks.push(value);
}
const text = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString('utf8').trim();
if (!text) return null;
try {
return JSON.parse(text) as unknown;
} catch {
throw new SafeRouteError(502, 'AI_HARDWARE_INVALID_RESPONSE', 'AI hardware service returned an invalid response', true);
}
}
type TimedResponse = {
response: Response;
signal: AbortSignal;
finish: () => void;
};
async function readTimedResponse(call: TimedResponse): Promise<unknown> {
try {
return await readBoundedResponse(call.response);
} catch (error) {
if (call.signal.aborted) {
throw new SafeRouteError(504, 'AI_HARDWARE_TIMEOUT', 'AI hardware service timed out', true);
}
throw error;
} finally {
call.finish();
}
}
async function readSafeErrorPayload(call: TimedResponse): Promise<unknown> {
try {
return await readTimedResponse(call);
} catch (error) {
if (error instanceof SafeRouteError && error.code === 'AI_HARDWARE_TIMEOUT') throw error;
return null;
}
}
async function cancelTimedResponse(call: TimedResponse): Promise<void> {
try {
await call.response.body?.cancel().catch(() => undefined);
} finally {
call.finish();
}
}
function ensureExactKeys(body: Record<string, unknown>, allowed: Set<string>): void {
if (Object.keys(body).some((key) => !allowed.has(key))) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
}
function requireLocalId(value: unknown): string {
const id = asRequiredString(value);
if (!id || !LOCAL_ID.test(id)) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
return id;
}
function decodeLocalId(value: string): string {
try {
return requireLocalId(decodeURIComponent(value));
} catch (error) {
if (error instanceof SafeRouteError) throw error;
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
}
function requireRevision(body: Record<string, unknown>): number {
const revision = asRevision(body.revision);
if (revision === null) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
return revision;
}
function takeOperationId(body: Record<string, unknown>, createUuid: () => string): string {
const candidate = body.client_operation_id ?? createUuid();
if (typeof candidate !== 'string' || !OPERATION_ID.test(candidate)) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
delete body.client_operation_id;
return candidate.toLowerCase();
}
function validateConfigUpdate(body: Record<string, unknown>): Record<string, unknown> {
ensureExactKeys(body, new Set(['revision', ...CONFIG_UPDATE_FIELDS]));
requireRevision(body);
const output: Record<string, unknown> = {};
for (const [key, value] of Object.entries(body)) {
if (key === 'revision') continue;
if (key === 'agent_name') {
const name = asRequiredString(value);
if (!name || name.length > 64) throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
output[key] = name;
} else if (key === 'clear_fields') {
if (!Array.isArray(value) || value.length > 16 || new Set(value).size !== value.length || value.some((item) => typeof item !== 'string' || !CLEARABLE_CONFIG_FIELDS.has(item))) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
output[key] = value;
} else if (CONFIG_NUMBER_FIELDS.includes(key as typeof CONFIG_NUMBER_FIELDS[number])) {
if (!Number.isInteger(value) || (value as number) < -100 || (value as number) > 100) throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
output[key] = value;
} else if (key === 'chat_history_conf') {
if (!Number.isInteger(value) || (value as number) < 0 || (value as number) > 2) throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
output[key] = value;
} else {
const limit = CONFIG_STRING_LIMITS[key];
if (typeof value !== 'string' || !value.trim() || limit === undefined || value.length > limit) throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
output[key] = value;
}
}
const clears = Array.isArray(output.clear_fields) ? new Set(output.clear_fields) : new Set();
if (Object.keys(output).some((key) => key !== 'clear_fields' && clears.has(key))) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
if (!Object.keys(output).length || (Object.keys(output).length === 1 && Array.isArray(output.clear_fields) && output.clear_fields.length === 0)) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
return output;
}
function strongRevisionFrom(response: Response): number {
const etag = response.headers.get('etag');
const match = etag?.match(STRONG_REVISION);
if (!match) throw new SafeRouteError(502, 'AI_HARDWARE_INVALID_ETAG', 'AI hardware service returned an invalid revision', true);
const revision = Number(match[1]);
if (!Number.isSafeInteger(revision)) throw new SafeRouteError(502, 'AI_HARDWARE_INVALID_ETAG', 'AI hardware service returned an invalid revision', true);
return revision;
}
function isJsonContentType(response: Response): boolean {
const contentType = response.headers.get('content-type')?.toLowerCase().split(';', 1)[0].trim();
return contentType === 'application/json' || contentType === 'application/problem+json';
}
function safeErrorForStatus(status: number): { code: string; error: string; retryable: boolean } {
if (status === 401) return { code: 'AI_HARDWARE_AUTH_REQUIRED', error: 'Works Square sign-in is required', retryable: false };
if (status === 403) return { code: 'AI_HARDWARE_FORBIDDEN', error: 'AI hardware operation is not allowed', retryable: false };
if (status === 404) return { code: 'AI_HARDWARE_RESOURCE_NOT_FOUND', error: 'AI hardware resource was not found', retryable: false };
if (status === 409 || status === 412) return { code: 'AI_HARDWARE_REVISION_CONFLICT', error: 'AI hardware data changed; refresh and retry', retryable: false };
if (status === 429) return { code: 'AI_HARDWARE_RATE_LIMITED', error: 'AI hardware service is busy; retry later', retryable: true };
if (status >= 500) return { code: 'AI_HARDWARE_UNAVAILABLE', error: 'AI hardware service is temporarily unavailable', retryable: true };
return { code: 'AI_HARDWARE_REQUEST_FAILED', error: 'AI hardware request was rejected', retryable: false };
}
const SAFE_UPSTREAM_ERRORS: Record<string, string> = {
ai_hardware_idempotency_conflict: 'AI hardware request conflicts with an earlier operation',
ai_hardware_operation_in_progress: 'AI hardware operation is still in progress',
ai_hardware_device_already_bound: 'AI hardware device is already bound',
ai_hardware_revision_conflict: 'AI hardware data changed; refresh and retry',
ai_hardware_state_conflict: 'AI hardware operation conflicts with the current state',
ai_hardware_revision_required: 'A current AI hardware revision is required',
ai_hardware_activation_code_invalid: 'AI hardware activation code is invalid',
ai_hardware_request_rejected: 'AI hardware request was rejected',
ai_hardware_resource_not_found: 'AI hardware resource was not found',
ai_hardware_provider_state_conflict: 'AI hardware provider state is inconsistent',
ai_hardware_credential_recovery_required: 'AI hardware credential recovery is required',
ai_hardware_credential_unavailable: 'AI hardware credential is unavailable',
ai_hardware_credential_recovery_unavailable: 'AI hardware credential recovery is not currently available',
ai_hardware_unconfigured: 'AI hardware integration is not configured',
ai_hardware_disabled: 'AI hardware module is not enabled',
ai_hardware_idempotency_key_invalid: 'AI hardware operation identity is invalid',
ai_hardware_revision_invalid: 'AI hardware revision is invalid',
xiaozhi_hardware_protocol_error: 'AI hardware service returned an invalid response',
xiaozhi_hardware_timeout: 'AI hardware service timed out',
xiaozhi_hardware_unavailable: 'AI hardware service is temporarily unavailable',
};
function safeUpstreamError(payload: unknown, status: number): { code: string; error: string; retryable: boolean } {
const detail = isRecord(payload) && isRecord(payload.detail) ? payload.detail : null;
const code = detail && typeof detail.error_code === 'string' && SAFE_UPSTREAM_ERRORS[detail.error_code]
? detail.error_code
: null;
if (!code) return safeErrorForStatus(status);
return { code, error: SAFE_UPSTREAM_ERRORS[code], retryable: detail?.retryable === true };
}
function sendFailure(
res: ServerResponse,
localStatus: number,
status: number,
code: string,
error: string,
retryable: boolean,
retryAfterSeconds?: number,
operationId?: string,
): void {
sendJson(res, localStatus, {
success: false,
status,
code,
error,
retryable,
...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
...(operationId === undefined ? {} : { operation_id: operationId }),
});
}
function retryAfterSeconds(response: Response, cap = 86_400): number | undefined {
const raw = response.headers.get('retry-after')?.trim();
if (!raw) return undefined;
const delta = /^\d+$/.test(raw) ? Number(raw) : Math.ceil((Date.parse(raw) - Date.now()) / 1000);
if (!Number.isFinite(delta) || delta < 0) return undefined;
return Math.min(delta, cap);
}
export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDependencies = {}) {
const fetchImpl = dependencies.fetchImpl ?? proxyAwareFetch;
const getAccessToken = dependencies.getAccessToken ?? getValidWorksSquareAccessToken;
const apiBaseUrl = (dependencies.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, '');
const createUuid = dependencies.randomUuid ?? randomUUID;
const timeoutMs = dependencies.timeoutMs ?? DEFAULT_TIMEOUT_MS;
return async function handleAiHardwareRoutes(
req: IncomingMessage,
res: ServerResponse,
url: URL,
_ctx: HostApiContext,
): Promise<boolean> {
if (url.pathname !== LOCAL_ROOT && !url.pathname.startsWith(`${LOCAL_ROOT}/`)) return false;
let operationId: string | undefined;
try {
let upstreamPath: string;
let body: Record<string, unknown> | undefined;
let project: (value: unknown) => Record<string, unknown> | null;
let requireEtag = false;
let ifMatch: number | undefined;
let expectedStatus: number;
const method = req.method ?? 'GET';
if (method === 'GET' && url.pathname === LOCAL_ROOT) {
upstreamPath = UPSTREAM_ROOT;
project = projectOverview;
expectedStatus = 200;
} else if (method === 'POST' && url.pathname === `${LOCAL_ROOT}/credential-recovery`) {
const input = await readBoundedJson(req);
ensureExactKeys(input, new Set(['client_operation_id']));
operationId = takeOperationId(input, createUuid);
upstreamPath = `${UPSTREAM_ROOT}/credential-recovery`;
project = projectOverview;
expectedStatus = 200;
} else if (method === 'POST' && url.pathname === `${LOCAL_ROOT}/agents`) {
const input = await readBoundedJson(req);
ensureExactKeys(input, new Set(['agent_name', 'client_operation_id']));
operationId = takeOperationId(input, createUuid);
const agentName = asRequiredString(input.agent_name);
if (!agentName || agentName.length > 64) throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
upstreamPath = `${UPSTREAM_ROOT}/agents`;
body = { agent_name: agentName };
project = projectAgent;
expectedStatus = 201;
} else if (method === 'POST' && url.pathname === `${LOCAL_ROOT}/device-bindings`) {
const input = await readBoundedJson(req);
ensureExactKeys(input, new Set(['activation_code', 'agent_id', 'client_operation_id']));
operationId = takeOperationId(input, createUuid);
if (typeof input.activation_code !== 'string' || !/^\d{6}$/.test(input.activation_code)) {
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
}
upstreamPath = `${UPSTREAM_ROOT}/device-bindings`;
body = { activation_code: input.activation_code, agent_id: requireLocalId(input.agent_id) };
project = projectDevice;
expectedStatus = 201;
} else {
const agentMatch = url.pathname.match(/^\/api\/works\/ai-hardware\/agents\/([^/]+)$/);
const assignmentMatch = url.pathname.match(/^\/api\/works\/ai-hardware\/devices\/([^/]+)\/agent-assignment$/);
if (agentMatch && (method === 'GET' || method === 'PATCH')) {
const id = decodeLocalId(agentMatch[1]);
upstreamPath = `/api/ai-hardware/agents/${encodeURIComponent(id)}`;
project = projectAgentConfig;
requireEtag = true;
expectedStatus = 200;
if (method === 'PATCH') {
const input = await readBoundedJson(req);
operationId = takeOperationId(input, createUuid);
ifMatch = requireRevision(input);
body = validateConfigUpdate(input);
}
} else if (assignmentMatch && (method === 'GET' || method === 'PUT')) {
const id = decodeLocalId(assignmentMatch[1]);
upstreamPath = `/api/ai-hardware/devices/${encodeURIComponent(id)}/agent-assignment`;
project = projectDevice;
requireEtag = true;
expectedStatus = 200;
if (method === 'PUT') {
const input = await readBoundedJson(req);
ensureExactKeys(input, new Set(['revision', 'agent_id', 'client_operation_id']));
operationId = takeOperationId(input, createUuid);
ifMatch = requireRevision(input);
body = { agent_id: requireLocalId(input.agent_id) };
}
} else {
sendFailure(res, 404, 404, 'AI_HARDWARE_ROUTE_NOT_FOUND', 'AI hardware route was not found', false);
return true;
}
}
const token = await getAccessToken({ fetchImpl });
if (!token) {
sendFailure(res, 200, 401, 'AI_HARDWARE_AUTH_REQUIRED', 'Works Square sign-in is required', false);
return true;
}
const idempotencyKey = operationId ? `makelore-${operationId}` : undefined;
const call = async (accessToken: string): Promise<TimedResponse> => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
let finished = false;
const finish = () => {
if (finished) return;
finished = true;
clearTimeout(timer);
};
try {
const response = await fetchImpl(`${apiBaseUrl}${upstreamPath}`, {
method,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
...(ifMatch === undefined ? {} : { 'If-Match': `"${ifMatch}"` }),
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
redirect: 'manual',
});
return { response, signal: controller.signal, finish };
} catch (error) {
finish();
if (controller.signal.aborted) {
throw new SafeRouteError(504, 'AI_HARDWARE_TIMEOUT', 'AI hardware service timed out', true);
}
throw error;
}
};
let currentToken = token;
let activeCall = await call(currentToken);
let response = activeCall.response;
if (response.status === 401) {
await cancelTimedResponse(activeCall);
const refreshed = await getAccessToken({ fetchImpl, forceRefresh: true });
if (refreshed) {
currentToken = refreshed;
activeCall = await call(currentToken);
response = activeCall.response;
}
}
let cachedErrorPayload: unknown;
if (response.status === 409 && operationId) {
const retryDelay = retryAfterSeconds(response, MAX_RETRY_AFTER_SECONDS);
if (retryDelay !== undefined) {
cachedErrorPayload = isJsonContentType(response) ? await readSafeErrorPayload(activeCall) : null;
const safe = safeUpstreamError(cachedErrorPayload, response.status);
if (safe.code === 'ai_hardware_operation_in_progress') {
await new Promise<void>((resolve) => setTimeout(resolve, retryDelay * 1000));
activeCall = await call(currentToken);
response = activeCall.response;
cachedErrorPayload = undefined;
}
}
}
if (!response.ok) {
const status = response.status;
if (status === 404 && upstreamPath === UPSTREAM_ROOT) {
await cancelTimedResponse(activeCall);
sendFailure(res, 200, 404, 'AI_HARDWARE_DISABLED', 'AI hardware module is not enabled', false);
return true;
}
const retryAfter = retryAfterSeconds(response);
const payload = cachedErrorPayload ?? (isJsonContentType(response)
? await readSafeErrorPayload(activeCall)
: (await cancelTimedResponse(activeCall), null));
const safe = safeUpstreamError(payload, status);
sendFailure(
res,
200,
status,
safe.code,
safe.error,
safe.retryable,
retryAfter,
operationId,
);
return true;
}
if (response.status !== expectedStatus || !isJsonContentType(response)) {
await cancelTimedResponse(activeCall);
throw new SafeRouteError(502, 'AI_HARDWARE_INVALID_RESPONSE', 'AI hardware service returned an invalid response', true);
}
let revision: number | undefined;
try {
revision = requireEtag ? strongRevisionFrom(response) : undefined;
} catch (error) {
await cancelTimedResponse(activeCall);
throw error;
}
const projected = project(await readTimedResponse(activeCall));
if (!projected) throw new SafeRouteError(502, 'AI_HARDWARE_INVALID_RESPONSE', 'AI hardware service returned an invalid response', true);
if (revision !== undefined) {
const dtoRevision = 'config_revision' in projected
? projected.config_revision
: projected.assignment_revision;
if (dtoRevision !== revision) {
throw new SafeRouteError(502, 'AI_HARDWARE_INVALID_RESPONSE', 'AI hardware service returned an invalid response', true);
}
}
sendJson(res, 200, { success: true, data: projected, ...(revision === undefined ? {} : { revision }) });
return true;
} catch (error) {
if (error instanceof SafeRouteError) {
sendFailure(res, 200, error.status, error.code, error.message, error.retryable, undefined, operationId);
} else if (error instanceof Error && error.name === 'AbortError') {
sendFailure(res, 200, 504, 'AI_HARDWARE_TIMEOUT', 'AI hardware service timed out', true, undefined, operationId);
} else {
sendFailure(res, 200, 502, 'AI_HARDWARE_UNAVAILABLE', 'AI hardware service is temporarily unavailable', true, undefined, operationId);
}
return true;
}
};
}
export const handleAiHardwareRoutes = createAiHardwareRouteHandler();