mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
There were two. Ours — AI_GATEWAY_URL, addressed with the caller's IAM token, so the org that pays is read off the bearer's claim. And upstream's, addressed with LLM_GATEWAY_URL and a single shared personal key, with the product in the path. Neither of the two was working. The upstream pair is unset in production, so the 27 call sites that reach for it — signals, conversations, growth, stamp, review, warehouse enrichment, path cleaning, AI observability translation — raised "LLM_GATEWAY_URL and an API key must be configured" and had never run. And the three build_* functions that were supposed to prefer ours referenced names that do not exist in their scope (`user`, `api_key`, `bearer`), so they raised NameError before reaching either gateway. Every AI feature in this repo was down, in one of two ways, and the fallback hid which. So there is one now. The three get_* constructors keep their names, because 27 files call them, and resolve to the Hanzo gateway; the three build_* variants differ only in tagging the generation with an explicit ai_product. Both go through one resolver, and every label — ai_product, ai_stage, team_id, the trace id — is built in one place rather than by each caller. A static key is deleted rather than moved. A key in the environment is one tenant's credential doing every tenant's work: it would bill every customer's assistant to whoever owns the key. The bearer is an IAM token and nothing else — a signed-in person's own token when someone is acting, this deployment's identity when nobody is. And there is no fallback. Unset, malformed, or IAM refusing a token now raises GatewayUnavailable. Answering anyway would mean answering from outside the estate, unbilled and unattributed, which is worse than not answering. Verified against the live Django with the real SDKs: 20 assertions on identity, base URLs, labels and refusal, and all 23 calling modules import.