mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
hanzoai/ai folded every route it serves under /v1/ai (HIP-0139 §3.1): a
capability answers at one address, and /v1/rag and /v1/router were two more
top-level addresses belonging to the same app. The old spellings die when cloud
picks up the module — there is no alias — so the calls move with them.
RagClient now posts /v1/ai/rag/{embed,query,query-multiple,delete} and reads
/v1/ai/rag/context; RoutingDefaults reads /v1/ai/router/defaults. Nothing else
changes: same bodies, same forwarded IAM bearer, same soft-fail.
The reward signal is NOT moved. Its URL is built inside @hanzo/ai's sendFeedback
(node_modules/@hanzo/ai/dist), not here, so client/src/utils/rewardSignal.ts and
its spec still name /v1/feedback and will keep doing so until that package
publishes the fold. Moving the assertion here would only make the spec disagree
with what the SDK actually posts.
74 lines
2.7 KiB
JavaScript
74 lines
2.7 KiB
JavaScript
const { logger } = require('@hanzochat/data-schemas');
|
|
const { resolveTenantBearer } = require('@hanzochat/api');
|
|
|
|
/**
|
|
* Server-driven auto-routing defaults for the caller's org. Proxies cloud's
|
|
* `GET /v1/ai/router/defaults` (authenticated, org-scoped) so ops can enable
|
|
* routing per-default in production from admin.hanzo.ai. The user's hanzo.id
|
|
* bearer is resolved server-side and forwarded on-behalf-of — cloud validates it
|
|
* and scopes to the caller's own org (same trust model as CloudAgentsClient); the
|
|
* token never reaches the browser.
|
|
*
|
|
* Fail-soft is the whole contract: on a missing bearer, an older cloud-api (404),
|
|
* a network error, a non-ok wrapper, or a malformed body, we answer 200 with
|
|
* `{ available: false }` so the client behaves exactly as today (local preference
|
|
* only) with no crash and no spinner block.
|
|
*/
|
|
|
|
/** HTTP timeout (ms) for the metadata read. Fast endpoint; keep it snappy. */
|
|
const TIMEOUT = Number(process.env.ROUTING_DEFAULTS_TIMEOUT) || 5000;
|
|
|
|
/** The "unknown/absent" answer — the client maps this to today's local-only behavior. */
|
|
const ABSENT = { available: false };
|
|
|
|
/** Resolve cloud's base host the same way the rest of chat's proxies do. */
|
|
function cloudEndpoint() {
|
|
const explicit = (process.env.HANZO_CLOUD_URL || '').trim();
|
|
if (explicit) {
|
|
return explicit.replace(/\/+$/, '');
|
|
}
|
|
const base = (process.env.OPENAI_BASE_URL || '').trim();
|
|
if (base) {
|
|
return base.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
|
|
}
|
|
return '';
|
|
}
|
|
|
|
async function routingDefaultsController(req, res) {
|
|
const endpoint = cloudEndpoint();
|
|
const bearer = resolveTenantBearer(req);
|
|
if (!endpoint || !bearer) {
|
|
return res.status(200).json(ABSENT);
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
|
|
try {
|
|
const resp = await fetch(`${endpoint}/v1/ai/router/defaults`, {
|
|
method: 'GET',
|
|
headers: { Authorization: `Bearer ${bearer}` },
|
|
signal: controller.signal,
|
|
});
|
|
if (!resp.ok) {
|
|
return res.status(200).json(ABSENT);
|
|
}
|
|
const body = await resp.json();
|
|
const data = body?.status === 'ok' ? body?.data : undefined;
|
|
if (!data || typeof data !== 'object') {
|
|
return res.status(200).json(ABSENT);
|
|
}
|
|
return res.status(200).json({
|
|
available: true,
|
|
auto_routing_active: data.auto_routing_active === true,
|
|
default_session_routing: data.default_session_routing === true,
|
|
});
|
|
} catch (err) {
|
|
logger.debug('[routingDefaults] fail-soft', { message: err?.message });
|
|
return res.status(200).json(ABSENT);
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
}
|
|
|
|
module.exports = routingDefaultsController;
|