Skip to main content
Cross-site redirect lets several Hivenet Router deployments share one public hostname even when each runs different models. Each deployment is a site. When a request arrives at a site that has no healthy agent for the requested model, the router answers 307 Temporary Redirect to a sibling site that serves it. The client repeats the request there. Cross-site redirect is off by default. With no site ID configured, the router behaves exactly as without the feature.
Hivenet Router does not discover sibling sites or build the sibling map. An external map writer must push the map to every router through POST /admin/sibling-map, and push it again after a router restarts.

How a request is handled

Every inference endpoint can redirect: POST /v1/chat/completions, POST /v1/messages and POST /v1/messages/count_tokens by the map’s chat placements, POST /v1/embeddings by its embedding placements and POST /v1/rerank by its rerank placements. After the API key is authenticated, the model alias is resolved and the model is checked against the key’s allowlist:
  1. If any healthy agent on this site serves the model, the request is served here. A redirect never replaces a working local agent.
  2. If the request has already been redirected twice, it is served here as before.
  3. Otherwise, if the sibling map lists another site inside the request’s scope that serves the model with the endpoint’s kind, the router answers 307 to it. Nothing is admitted, queued or charged to the key’s quota on this site.
  4. Otherwise, the request is handled here. When the map places the model on this site with scale_from_zero: true, it is held until the model wakes (see Hold at zero); otherwise it normally ends in model_not_found.
A request whose key may not use the model is never redirected: it gets 403 model_forbidden here. A redirect takes precedence over a policy’s fallback_provider: when a sibling serves the model, the request goes to the sibling, not to OpenAI or Anthropic.

Choosing the site

Every router computes the same choice from the map, in this order:
  1. Candidates. The sites with a placement of the model and the endpoint’s kind, inside the request’s scope, except this site and the site where the request first arrived.
  2. Home set. The two candidates that rank highest by a hash of the caller’s API-key ID, the model and the site ID. One key spreads one model over at most two sites, so its rate limits stay meaningful.
  3. Session order. The home set is ordered by a hash of the key ID and the optional X-Session-ID request header. The same key and session always reach the same site, so its prompt cache is reused. When a site leaves the map, only the sessions that were on it move.
  4. Wake here first. While the map’s hints are fresh, when every home site is cold and the map also places the model on this site, for the endpoint’s kind, with scale_from_zero: true, the request is not redirected: it is held here while the model wakes. A cold sibling is no faster, and the request stays where it landed.
  5. Hints. While the map’s hints are fresh, a target whose hint says load: full, or that is cold and cannot scale from zero, is skipped, and a warm target is preferred over a cold one. When every target would be skipped, the first one is used anyway. After hints_expire_at, hints are ignored and the session order alone decides.
Send X-Session-ID with a stable value per conversation to spread one key’s conversations over its two home sites. Without it, every request of a key for one model goes to the same site.

Scope

The scope bounds where a request may be redirected. A client asks for one with the X-Hivenet-Scope request header: Without the header, the scope is this site’s country: by default a request is only redirected within the country it landed in, and leaving it takes an explicit EU or all. Any other header value is refused with 400 request_invalid; it is never widened. When this site is missing from the map, a request without the header is not redirected. The scope travels in the redirect token, and the target serves the request only if it lies inside it. A second redirect keeps the scope of the first.

The redirect token

Clients remove the Authorization header when a redirect changes host. The 307 therefore carries a signed token in the rt query parameter, and the target authenticates the repeated request with it:
The token is an HMAC-SHA256 signature over the key ID, the request ID, the model, the scope, the origin site, the target site, the request path, the hop count, a SHA-256 hash of the request body, its issue and expiry times, and a random token ID that is new for every token. The target checks, in order:
  1. the token is well formed, its signature is valid, and its request ID (and client request ID, when present) is a UUID;
  2. it has not expired, with 5 seconds of grace for clock drift between routers;
  3. it was not issued more than 5 seconds in the future. Such a token means this router’s clock is behind, and it would otherwise stay valid longer than its lifetime;
  4. it names this site and this request path;
  5. this site is inside the token’s scope;
  6. the key it names is still enabled and unexpired, and any Authorization header the client sent belongs to that same key;
  7. the request body is byte-identical to the one the token was issued for. This is checked before any quota is spent;
  8. the token has not been used before: a token works once.
A token is marked used only when it passes every check, so a request refused on any check, such as a leaked URL sent with another body, never uses up the client’s token. Single use is tracked by the token ID, not the request ID: a client that sets its own ID and retries gets a new token each time, and another key cannot use up its token by sending the same ID. When a check fails and the request carries an Authorization header that authenticates, the token is ignored and the request is served on that key as a new request. Otherwise a refused token gets 401 unauthorized, like any other invalid credential. An accepted request is served on the token’s key. It keeps the first router’s request ID, returned in X-Request-ID, so one request can be followed across both sites’ logs, and the client’s own ID, if it sent one, returned in X-Client-Request-ID. It is routed on the token’s model, the concrete model the first router resolved, so an alias such as model: "auto" is not resolved again. The key’s quota is charged only here.
Until it is used or expires, a token works for anyone who has the full URL and the same request body. Hivenet Router redacts the token from its own access log. Keep URLs out of other logs, and keep --redirect-token-ttl as short as your slowest clients allow.

Loop guard

The hop count travels inside the token. A request that has already been redirected twice is never redirected again: it is served where it is or fails as usual. A second redirect never goes back to the site where the request first arrived. A stale or inconsistent map therefore costs at most one extra redirect, never a loop.

Requirements

  • Every router runs with the --site-id of its site, as the map names it.
  • Every site shares the same redirect signing secrets, each at least 32 bytes long.
  • API keys are dynamic keys pushed through /admin/api-keys, or the auth mode is none. The router refuses to start with cross-site redirect and static auth.yaml keys, because static keys have no key ID that a sibling could resolve.
  • Every site URL in the map is an absolute https:// origin under one of --redirect-allowed-domains. Clients follow a redirect from HTTPS to HTTP, and some keep sending the API key after a host change, so the router rejects http:// URLs and any host it does not own.
  • Clients follow 307 redirects. Most HTTP clients and the OpenAI and Anthropic SDKs do so by default. Plain httpx does not: set follow_redirects=True. A client that does not follow receives the 307 with a model_redirected error body.

Configuration

When both --redirect-secret-file and HIVENET_ROUTER_REDIRECT_SECRETS are set, the file wins. The router refuses to start when any of these settings is invalid, or when secrets are set without a site ID. To rotate the secret without refusing tokens in flight, add the new secret as a second line on every site, then move it to the first line on every site, then remove the old one once the token lifetime has passed. A token that expires before the client follows is refused with 401, and counted as reason="expired" in hivenet_redirect_rejections_total.

Pushing the sibling map

Replace the whole map with an admin request:
Push the same map to every site. A router ignores its own site when it picks a target. Any invalid field rejects the whole push with 400, and the current map is kept. The map is held in memory only: a restarted router has an empty map and redirects nothing until the next push. Read the current map with GET /admin/sibling-map. Both endpoints return 501 when cross-site redirect is disabled.

Metrics

A rising reason="expired" count means clients take longer than the token lifetime to follow the redirect. reason="clock" means this router’s clock is behind the others. reason="replay" means a token was presented twice. Compare hivenet_sibling_map_version across routers to find one that missed a push.

Limitations

  • A redirect happens only when no healthy agent on this site serves the model. Requests are not redirected when local agents are busy.
  • A token works once on each router, not once per site: each router remembers used token IDs in its own memory.
  • A client that repeats an already served request at the same redirect URL without its API key is refused with 401, because that token was used. Repeating the request at the original URL gets a new token, and a client that also sends its API key is served on it as a new request.
  • API keys have no home country or maximum scope yet: the default scope is the country of the site the request landed in, and any request may ask for EU or all.
  • The map push is protected by the admin key only. Treat that key as a map-writer secret: the map decides where clients and their API keys are sent.
  • Only the rt query parameter is redacted from the access log; other query parameters are logged as before.