Files
zooqueen 694061e40b a free plan opens on the model it can run
A conversation opens on the first model its endpoint offers, and that was a
priced rung. An account on the free plan holds no balance by design — it is
free, and buys its own credit — so its first message reached the wallet gate
where an answer belonged.

The plan is read from the one service that decides it (cloud /v1/allowance,
which names the plan and the day's free calls) and the free route leads the
catalog for a free caller. Reordered, never trimmed: the priced rungs stay
listed, because seeing what a plan buys is the offer. A plan that cannot be
read is not a free plan, so no failure of that read gives the free lane away.

Consent follows the route rather than the caller. The free models cost nothing
and share data in exchange; that is as true of a free plan defaulting to them
as of a guest pinned to them.

A spent day of free calls now renders as the limit it is. The gateway names it
allowance_spent beside the same 402 an empty wallet gets, and unmapped it read
as "something went wrong on our side" — an outage, at the moment an account is
most ready to buy. It says when the count returns and offers the plan that
lifts it, and no longer offers a switch to the route that just refused.
2026-08-16 01:56:48 -07:00

55 lines
1.8 KiB
JavaScript

const { resolveTenantBearer } = require('@hanzochat/api');
/**
* The plan a caller is on, asked of the service that decides it.
*
* Cloud's `GET /v1/allowance` names the plan a caller resolves to and what it
* lets them do without paying: commerce derives the name from their
* subscriptions, and an account that has bought nothing derives `free`. Chat
* reads that answer instead of forming a second opinion from a balance or a
* subscription row — two answers to "is this person on the free tier" is how a
* free account came to be offered a model it cannot pay for.
*
* The caller's own IAM bearer is forwarded, so the plan that comes back belongs
* to the person who asked: the same identity the gateway bills.
*/
/** The plan an account holds until it buys one. */
const FREE = 'free';
/** How long to wait. An in-cluster hop, and a page is worth more than a slow answer. */
const TIMEOUT = 3000;
/**
* The caller's plan name, or null when it could not be read.
*
* NULL IS NOT FREE. A caller whose plan did not answer keeps whatever a paying
* caller gets, so no failure of this read can hand the free lane to someone
* cloud never named.
*
* @param {import('express').Request} req
* @returns {Promise<string|null>}
*/
async function planOf(req) {
const bearer = resolveTenantBearer(req);
const cloud = (process.env.HANZO_CLOUD_URL || '').replace(/\/+$/, '');
if (!bearer || !cloud) {
return null;
}
try {
const resp = await fetch(`${cloud}/v1/allowance`, {
headers: { Authorization: `Bearer ${bearer}` },
signal: AbortSignal.timeout(TIMEOUT),
});
if (!resp.ok) {
return null;
}
const { plan } = await resp.json();
return typeof plan === 'string' && plan ? plan : null;
} catch {
return null;
}
}
module.exports = { planOf, FREE };