Tenant Cost Attribution Explained — 3 Fallback Models Behind One Chatbot API A developer outlines three fallback patterns for a marketplace SaaS chatbot that reviews code changes, emphasizing that tenant cost attribution depends on recording tenant, route, model, token usage, and fallback reason in a single usage event. The post compares direct provider clients, self-hosted gateways, and managed brokers, recommending a replayable tenant fixture to evaluate cost visibility. Short answer: for a marketplace SaaS chatbot that reviews code changes, choose the runtime pattern that records tenant, route, model, token usage, and fallback reason in one usage event; one credential and automatic fallback are useful, but neither matters if a team can't explain each tenant's bill. Start with the accounting boundary, not the provider list. A direct integration preserves maximum control, a self-hosted gateway centralizes routing while keeping operations in-house, and a managed broker transfers more of that operational burden. The decision turns on who must own normalization, failover policy, and evidence when a customer questions a charge. | Pattern | Pick this when | Cost visibility owner | Main trade-off | |---|---|---|---| | Direct provider clients | Provider-specific controls matter more than a common interface | Application team | Every adapter needs its own usage and error mapping | | Self-hosted gateway | One internal endpoint and policy layer justify operating a control plane | Platform team | The team owns upgrades, capacity, and telemetry quality | | Managed broker | A single credential and low gateway maintenance are priorities | Broker plus application team | Billing fields and routing controls depend on the contract | Treat “one key” as a deployment property, not a selection criterion. It reduces secret distribution across services, but it doesn't prove that fallback is safe or that tenant charges are reconstructable. For a marketplace, the useful question is narrower: can the runtime accept a code diff, return structured findings, and emit a complete usage record even when a request moves to a second model? A serious evaluation uses the same fixture against all three patterns. Give the fixture a tenant ID, repository ID, pull-request ID, fixed prompt version, and JSON output schema. Then force three outcomes: a normal response, a rate-limit response such as 429 , and a timeout at the application's deadline. The test passes only if the final finding remains tied to the original tenant and every attempt is visible. Don't merge attempts into one opaque total. A fallback can produce a good answer while leaving finance with a bad ledger. The desired flow is easy to say aloud: request enters; policy reads tenant limits; primary model runs; a retryable classification may select an allowed fallback; schema validation accepts or rejects the finding; one append-only usage event is written per attempt; the API returns the validated result and a correlation ID. That's the diagram-in-words. Each arrow needs an observable field. Direct clients fit when the review workflow relies on provider-specific capabilities or when only one provider is likely to remain active. The application owns the adapters, so it can preserve native response details. The catch is adapter work: auth, timeouts, usage extraction, error classes, and schema validation can drift independently. A self-hosted gateway fits when several teams need the same policy and the organization already operates shared services. LiteLLM is one open-source example of an LLM gateway and documents a proxy-oriented approach across model providers. This option can make routing rules inspectable, but someone still owns its availability, configuration review, and telemetry pipeline. A managed broker fits when minimizing control-plane operations is more important than owning every routing detail. Read the contract carefully — especially the usage fields, model identifiers, export path, regional handling, and behavior when a provider throttles. I'm not sure any generic feature checklist can settle that trade-off; a replayable tenant fixture and an exported usage sample provide better evidence. Per-tenant cost visibility fails when metering is reconstructed from application logs after the fact. Logs are optimized for diagnosis. A ledger needs stable identities, explicit units, and append-only attempts. Keep both, but don't pretend they are interchangeable. For each attempt, capture tenantId , requestId , attempt , provider , model , input and output token counts, timestamps, outcome, and fallback reason. Also capture the prompt and schema versions. Those version fields explain why two similar diffs may consume different resources after a rollout. Store monetary values only after applying the rate card that was effective at the attempt timestamp; token counts are evidence, while a mutable “current price” lookup is not. No mystery totals. Use metrics for aggregate control: attempts by model and outcome, fallback rate by tenant tier, validation failures, latency, and token units. Use traces to connect the inbound review request to each model attempt and the ledger write. Use logs for the detailed diagnostic context that doesn't belong in metric labels. Alert on ratios over a meaningful window rather than on a single fallback; one successful fallback is expected behavior, while a sustained shift can indicate throttling, a policy change, or a workload change. Tenant identity must be assigned by trusted server-side authentication. Never accept a billing tenant ID from an unverified chatbot payload. Repository and pull-request identifiers are useful dimensions for internal analysis, but avoid putting high-cardinality or sensitive values into metric labels. They belong in access-controlled events and traces. The following TypeScript keeps vendor adapters outside the policy loop. It also separates the billable usage event from the final response, which makes every attempt auditable. review represents an adapter call; its concrete URL and authentication belong inside the adapter, where they can be tested against that provider's published contract. type Finding = { file: string; line: number; severity: "low" | "medium" | "high"; message: string; }; type Usage = { inputTokens: number; outputTokens: number }; type ReviewResult = { findings: Finding ; usage: Usage; }; type RuntimeError = Error & { kind: "rate limit" | "timeout" | "invalid output" | "fatal"; }; interface ModelAdapter { provider: string; model: string; review diff: string, signal: AbortSignal : Promise