feat: add dynamic Robot configuration choices
This commit is contained in:
@@ -13,6 +13,7 @@ const LOCAL_ROOT = '/api/works/ai-hardware';
|
||||
const UPSTREAM_ROOT = '/api/ai-hardware';
|
||||
const REVISION_ETAG = /^(?:W\/)?"(0|[1-9]\d*)"$/;
|
||||
const LOCAL_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,35}$/;
|
||||
const CATALOG_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$/;
|
||||
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;
|
||||
|
||||
@@ -119,6 +120,52 @@ function projectOverview(value: unknown): Record<string, unknown> | null {
|
||||
return { status: value.status, agents, devices };
|
||||
}
|
||||
|
||||
const CATALOG_MODEL_TYPES = new Set([
|
||||
'VAD', 'ASR', 'LLM', 'VLLM', 'Intent', 'Memory', 'TTS',
|
||||
]);
|
||||
|
||||
function projectCatalog(value: unknown): Record<string, unknown> | null {
|
||||
if (!isRecord(value)
|
||||
|| value.schema_version !== 1
|
||||
|| !Array.isArray(value.models)
|
||||
|| value.models.length > 256
|
||||
|| !Array.isArray(value.voices)
|
||||
|| value.voices.length > 512) return null;
|
||||
const models: Record<string, unknown>[] = [];
|
||||
for (const item of value.models) {
|
||||
if (!isRecord(item)
|
||||
|| typeof item.model_type !== 'string'
|
||||
|| !CATALOG_MODEL_TYPES.has(item.model_type)
|
||||
|| typeof item.model_id !== 'string' || item.model_id.length < 1 || item.model_id.length > 255
|
||||
|| typeof item.model_name !== 'string' || item.model_name.length < 1 || item.model_name.length > 128
|
||||
|| (item.supports_function_call !== null && typeof item.supports_function_call !== 'boolean')) return null;
|
||||
models.push({
|
||||
model_type: item.model_type,
|
||||
model_id: item.model_id,
|
||||
model_name: item.model_name,
|
||||
supports_function_call: item.supports_function_call,
|
||||
});
|
||||
}
|
||||
const voices: Record<string, unknown>[] = [];
|
||||
for (const item of value.voices) {
|
||||
if (!isRecord(item)
|
||||
|| typeof item.tts_model_id !== 'string' || item.tts_model_id.length < 1 || item.tts_model_id.length > 255
|
||||
|| typeof item.voice_id !== 'string' || item.voice_id.length < 1 || item.voice_id.length > 255
|
||||
|| typeof item.voice_name !== 'string' || item.voice_name.length < 1 || item.voice_name.length > 128
|
||||
|| !Array.isArray(item.languages) || item.languages.length > 16
|
||||
|| item.languages.some((language) => typeof language !== 'string' || language.length < 1 || language.length > 50)
|
||||
|| typeof item.is_clone !== 'boolean') return null;
|
||||
voices.push({
|
||||
tts_model_id: item.tts_model_id,
|
||||
voice_id: item.voice_id,
|
||||
voice_name: item.voice_name,
|
||||
languages: item.languages,
|
||||
is_clone: item.is_clone,
|
||||
});
|
||||
}
|
||||
return { schema_version: 1, models, voices };
|
||||
}
|
||||
|
||||
async function readBoundedJson(req: IncomingMessage): Promise<Record<string, unknown>> {
|
||||
const declared = Number(req.headers['content-length']);
|
||||
if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES) {
|
||||
@@ -323,6 +370,7 @@ const SAFE_UPSTREAM_ERRORS: Record<string, string> = {
|
||||
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_catalog_unavailable: 'AI hardware configuration options are unavailable',
|
||||
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',
|
||||
@@ -384,6 +432,10 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
|
||||
_ctx: HostApiContext,
|
||||
): Promise<boolean> {
|
||||
if (url.pathname !== LOCAL_ROOT && !url.pathname.startsWith(`${LOCAL_ROOT}/`)) return false;
|
||||
if (url.pathname === `${LOCAL_ROOT}/catalog`) {
|
||||
res.setHeader('Cache-Control', 'private, no-store');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
}
|
||||
|
||||
let operationId: string | undefined;
|
||||
try {
|
||||
@@ -399,6 +451,21 @@ export function createAiHardwareRouteHandler(dependencies: AiHardwareRouteDepend
|
||||
upstreamPath = UPSTREAM_ROOT;
|
||||
project = projectOverview;
|
||||
expectedStatus = 200;
|
||||
} else if (method === 'GET' && url.pathname === `${LOCAL_ROOT}/catalog`) {
|
||||
const keys = [...url.searchParams.keys()];
|
||||
const values = url.searchParams.getAll('tts_model_id');
|
||||
if (keys.some((key) => key !== 'tts_model_id') || values.length > 1) {
|
||||
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
|
||||
}
|
||||
const ttsModelId = values[0];
|
||||
if (ttsModelId !== undefined && !CATALOG_ID.test(ttsModelId)) {
|
||||
throw new SafeRouteError(400, 'AI_HARDWARE_INVALID_REQUEST', 'Invalid AI hardware request');
|
||||
}
|
||||
upstreamPath = `${UPSTREAM_ROOT}/catalog${
|
||||
ttsModelId === undefined ? '' : `?tts_model_id=${encodeURIComponent(ttsModelId)}`
|
||||
}`;
|
||||
project = projectCatalog;
|
||||
expectedStatus = 200;
|
||||
} else if (method === 'POST' && url.pathname === `${LOCAL_ROOT}/credential-recovery`) {
|
||||
const input = await readBoundedJson(req);
|
||||
ensureExactKeys(input, new Set(['client_operation_id']));
|
||||
|
||||
Reference in New Issue
Block a user