<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oluwaseyi Ogunsola</title>
    <description>The latest articles on DEV Community by Oluwaseyi Ogunsola (@oluwaseyi_ogunsola_691294).</description>
    <link>https://dev.to/oluwaseyi_ogunsola_691294</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4185290%2Fde25d532-524f-4df0-bed6-55b5dfed0651.png</url>
      <title>DEV Community: Oluwaseyi Ogunsola</title>
      <link>https://dev.to/oluwaseyi_ogunsola_691294</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oluwaseyi_ogunsola_691294"/>
    <language>en</language>
    <item>
      <title>Designing agent-broker: Collision-Safe Redis Keys, Budget Integrity, and Zero-Trust Delegation</title>
      <dc:creator>Oluwaseyi Ogunsola</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:47:46 +0000</pubDate>
      <link>https://dev.to/oluwaseyi_ogunsola_691294/designing-agent-broker-collision-safe-redis-keys-budget-integrity-and-zero-trust-delegation-npl</link>
      <guid>https://dev.to/oluwaseyi_ogunsola_691294/designing-agent-broker-collision-safe-redis-keys-budget-integrity-and-zero-trust-delegation-npl</guid>
      <description>&lt;p&gt;A permission broker for concurrent AI agents must do more than check whether a request is allowed.&lt;/p&gt;

&lt;p&gt;It needs to coordinate shared budgets, enforce concurrency limits, manage reservations, and prevent delegated agents from bypassing the constraints imposed on their parent or root agent.&lt;/p&gt;

&lt;p&gt;These responsibilities become difficult when several processes operate concurrently and when external failures leave shared state in unexpected conditions.&lt;/p&gt;

&lt;p&gt;While building agent-broker, I focused on several less obvious failure modes: ambiguous composite Redis keys, budget pools that disappear before reservations are resolved, and caller-controlled identity or delegation metadata.&lt;/p&gt;

&lt;p&gt;This article examines the design decisions behind those cases.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Composite Redis Keys Need Unambiguous Boundaries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Redis keys are often constructed by concatenating several values with a delimiter.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;"concurrency:{target}:{budgetKey}"&lt;/p&gt;

&lt;p&gt;This is convenient until the values themselves contain the delimiter.&lt;/p&gt;

&lt;p&gt;Suppose two requests use these inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"target = "a:b"" and "budgetKey = "c""&lt;/li&gt;
&lt;li&gt;"target = "a"" and "budgetKey = "b:c""&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A naive delimiter-based scheme can serialize both combinations into the same string:&lt;/p&gt;

&lt;p&gt;"concurrency🅰️b:c"&lt;/p&gt;

&lt;p&gt;The application intended these to represent different logical resources, but Redis sees identical keys.&lt;/p&gt;

&lt;p&gt;In a system that tracks concurrency or budget state by key, this ambiguity can cause unrelated operations to share counters or interfere with one another.&lt;/p&gt;

&lt;p&gt;Length-prefixing the segments&lt;/p&gt;

&lt;p&gt;agent-broker addresses this problem through explicit length-prefixing.&lt;/p&gt;

&lt;p&gt;Each caller-supplied string segment is encoded as:&lt;/p&gt;

&lt;p&gt;"seg(value) = "${value.length}:${value}""&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"target = "a:b"" becomes "3🅰️b"&lt;/li&gt;
&lt;li&gt;"budgetKey = "c"" becomes "1:c"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resulting composite key is:&lt;/p&gt;

&lt;p&gt;"concurrency:3:a:b1:c"&lt;/p&gt;

&lt;p&gt;For the second input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"target = "a"" becomes "1:a"&lt;/li&gt;
&lt;li&gt;"budgetKey = "b:c"" becomes "3🅱️c"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key becomes:&lt;/p&gt;

&lt;p&gt;"concurrency:1:a3🅱️c"&lt;/p&gt;

&lt;p&gt;The segment boundaries remain distinguishable even when values contain colons.&lt;/p&gt;

&lt;p&gt;This is the central property of the approach: serialization preserves the boundaries between components rather than relying on the assumption that their contents will never contain a separator.&lt;/p&gt;

&lt;p&gt;The implementation is documented in ADR-0008.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Handling a Missing Budget Pool During Reservation Resolution&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A second problem arises when a budget pool disappears while an agent still has an outstanding reservation.&lt;/p&gt;

&lt;p&gt;agent-broker uses budget pools that do not have a TTL by design, allowing them to outlive individual agent lifetimes. However, the absence of a TTL does not make a key impossible to delete. An operator, an administrative operation, or an unexpected Redis state change can still remove it.&lt;/p&gt;

&lt;p&gt;This matters when resolving a reservation.&lt;/p&gt;

&lt;p&gt;Redis's "INCRBY" command creates a missing key with the requested increment. That behavior can be dangerous if the increment is intended to refund previously reserved budget.&lt;/p&gt;

&lt;p&gt;If the original pool has disappeared, applying a refund without checking the pool's existence can recreate a budget key whose state no longer represents the original pool.&lt;/p&gt;

&lt;p&gt;The resulting value may look valid at the Redis level while being inconsistent with the broker's intended accounting.&lt;/p&gt;

&lt;p&gt;Detecting the missing pool atomically&lt;/p&gt;

&lt;p&gt;agent-broker's reservation-resolution Lua script checks whether the budget pool exists before processing a refund.&lt;/p&gt;

&lt;p&gt;If the pool is missing, the script skips the refund instead of allowing "INCRBY" to recreate the key.&lt;/p&gt;

&lt;p&gt;The reservation still resolves its other responsibilities: concurrency slots are released and applicable expiring entries are cleaned up. This avoids leaving unrelated concurrency state stranded merely because the budget pool is missing.&lt;/p&gt;

&lt;p&gt;The response explicitly reports:&lt;/p&gt;

&lt;p&gt;"poolMissing: true"&lt;/p&gt;

&lt;p&gt;That signal gives the caller or observability layer a way to detect the unexpected state and investigate it.&lt;/p&gt;

&lt;p&gt;There is an important distinction here: the broker can detect that the pool is missing, but that observation alone does not establish whether it was deleted intentionally, evicted, or removed by another mechanism.&lt;/p&gt;

&lt;p&gt;The design prevents a missing pool from being silently recreated through the refund path while keeping the reservation lifecycle recoverable.&lt;/p&gt;

&lt;p&gt;This behavior is documented in ADR-0003.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Broker-Issued Identity and Computed Delegation Depth&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A permission system loses much of its value if callers can supply the identity or hierarchy metadata used to decide whether their requests are allowed.&lt;/p&gt;

&lt;p&gt;Consider a delegation tree with a maximum permitted depth.&lt;/p&gt;

&lt;p&gt;If a caller can register a child agent and claim that its depth is zero, the broker may evaluate that child against the wrong position in the hierarchy.&lt;/p&gt;

&lt;p&gt;Similarly, accepting caller-supplied parent or root identifiers without validating them against stored relationships can undermine the intended delegation boundaries.&lt;/p&gt;

&lt;p&gt;The broker owns identity and hierarchy&lt;/p&gt;

&lt;p&gt;agent-broker applies three related rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent identity UUIDs are issued by the broker during registration rather than accepted from callers.&lt;/li&gt;
&lt;li&gt;Delegation depth, parent ID, and root ID are derived by the broker from the referenced parent's stored state.&lt;/li&gt;
&lt;li&gt;Callers attempting to supply protected fields such as "depth", "rootId", "budgetKey", or "agentId" during child registration receive a "BrokerArgumentError".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These decisions make the broker responsible for establishing identity and hierarchy instead of trusting clients to describe their own permissions.&lt;/p&gt;

&lt;p&gt;The distinction is fundamental: clients can request an operation, but they should not be allowed to redefine the security context in which that operation is evaluated.&lt;/p&gt;

&lt;p&gt;This is one part of the broker's trust-boundary design, not a claim that these rules alone constitute a complete security model.&lt;/p&gt;

&lt;p&gt;The relevant decisions are recorded in ADR-0012.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Broader Design Principle&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These three problems look unrelated at first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composite keys can collapse distinct resources into the same identifier.&lt;/li&gt;
&lt;li&gt;A missing budget pool can be recreated by an otherwise ordinary Redis operation.&lt;/li&gt;
&lt;li&gt;Caller-controlled hierarchy metadata can undermine permission enforcement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But each problem has the same underlying structure: the system is relying on an assumption that may not hold when inputs are adversarial or state changes unexpectedly.&lt;/p&gt;

&lt;p&gt;The response is to make the assumption explicit and enforce the relevant invariant at the layer that owns it.&lt;/p&gt;

&lt;p&gt;For key construction, that means unambiguous serialization.&lt;/p&gt;

&lt;p&gt;For reservation resolution, that means checking shared state within the atomic script before applying a refund.&lt;/p&gt;

&lt;p&gt;For delegation, that means deriving identity and hierarchy from broker-controlled state rather than trusting caller-provided values.&lt;/p&gt;

&lt;p&gt;These mechanisms do not eliminate every possible failure. They make specific failure modes harder to trigger and easier to detect.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;agent-broker is a distributed concurrency and resilience library for coordinating agent permissions, budget reservations, and delegation.&lt;/p&gt;

&lt;p&gt;Building it highlighted an important lesson: correctness depends on more than the happy path. It also depends on how a system represents identity, serializes resource names, and behaves when shared state no longer matches expectations.&lt;/p&gt;

&lt;p&gt;Length-prefixed Redis keys, missing-pool detection during reservation resolution, and broker-controlled delegation metadata are three examples of turning those concerns into explicit architectural rules.&lt;/p&gt;

&lt;p&gt;The most valuable design decisions are often the ones that remove ambiguity before it becomes a production incident.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>redis</category>
      <category>distributedsystems</category>
      <category>backend</category>
    </item>
    <item>
      <title>Reengineering Family Finance: Building Financial Integrity Into Kith</title>
      <dc:creator>Oluwaseyi Ogunsola</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:27:49 +0000</pubDate>
      <link>https://dev.to/oluwaseyi_ogunsola_691294/reengineering-family-finance-building-financial-integrity-into-kith-1lf0</link>
      <guid>https://dev.to/oluwaseyi_ogunsola_691294/reengineering-family-finance-building-financial-integrity-into-kith-1lf0</guid>
      <description>&lt;p&gt;Families manage money and responsibilities in ways that often resemble small organizations. They coordinate recurring contributions, fund shared projects, track obligations, resolve disputes, and follow up with members who miss payments.&lt;/p&gt;

&lt;p&gt;Yet these activities frequently happen through group chats, spreadsheets, handwritten records, and informal agreements. The problem isn't simply that these tools lack automation. It's that they make it difficult to maintain a reliable financial history, coordinate responsibilities, and establish accountability as a family grows.&lt;/p&gt;

&lt;p&gt;Kith was built to address that problem: bringing structured financial records, automated workflows, and accountable collaboration to family finance without imposing the complexity of enterprise software.&lt;/p&gt;

&lt;p&gt;Building it required more than creating forms and database endpoints. Financial coordination introduces difficult engineering requirements around concurrent writes, duplicate submissions, historical integrity, scheduled work, and recoverable notifications.&lt;/p&gt;

&lt;p&gt;This article explores the architectural decisions behind those requirements.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Financial History Must Remain Auditable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In an ordinary CRUD application, updating a record in place is often sufficient. In a financial system, silently changing a confirmed transaction can undermine the history that other decisions depend on.&lt;/p&gt;

&lt;p&gt;Kith uses an append-oriented ledger model. Once an entry is confirmed, corrections are represented through new entries linked to the original rather than by silently replacing the original transaction.&lt;/p&gt;

&lt;p&gt;The relationship between an original entry and its correction is cryptographically linked, providing a mechanism for connecting the financial history and detecting inconsistencies according to the implemented verification rules.&lt;/p&gt;

&lt;p&gt;Database-level immutability enforcement provides another layer of protection. The database, rather than application conventions alone, prevents confirmed entries from being modified in place through the protected database operations.&lt;/p&gt;

&lt;p&gt;This distinction matters because application-level validation cannot protect a record against every other database access path. Integrity rules are stronger when they are enforced at the layer responsible for storing the data.&lt;/p&gt;

&lt;p&gt;The objective is straightforward: preserve the original financial event, make corrections explicit, and maintain an auditable history of how the record evolved.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Idempotency and Duplicate-Submission Defense&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Financial applications must account for unreliable networks, repeated button clicks, client retries, and requests that succeed even when the client never receives the response.&lt;/p&gt;

&lt;p&gt;Consider a contribution request that reaches the server and commits successfully. If the response is lost, the client may retry. Without duplicate protection, the same logical contribution could be recorded twice.&lt;/p&gt;

&lt;p&gt;Kith uses client-supplied idempotency keys to associate retries with the original logical operation.&lt;/p&gt;

&lt;p&gt;The important part is not merely checking whether a key already exists. Concurrent requests can both pass an application-level check before either inserts its record. That check-then-insert pattern leaves a race condition.&lt;/p&gt;

&lt;p&gt;Kith's duplicate-submission handling has been hardened to close that race, so the idempotency mechanism must be evaluated as a complete concurrency-safe operation rather than as a preliminary lookup.&lt;/p&gt;

&lt;p&gt;A separate ten-minute heuristic identifies likely accidental duplicate contributions. This heuristic serves a different purpose from idempotency: it can warn users about suspiciously similar submissions without automatically rejecting every legitimate repeated contribution.&lt;/p&gt;

&lt;p&gt;The distinction is important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Idempotency identifies repeated attempts at the same logical operation.&lt;/li&gt;
&lt;li&gt;Duplicate heuristics identify separate operations that may represent an accidental repeat.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping these mechanisms separate avoids confusing network retries with legitimate contributions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Atomic State Transitions With PostgreSQL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Financial coordination involves operations that update several related records. Dispute resolution, invitation acceptance, and converting an event into a recurring pool are examples where partial completion can leave the application in an inconsistent state.&lt;/p&gt;

&lt;p&gt;If two requests modify related data concurrently, individually valid operations can still produce an invalid combined result.&lt;/p&gt;

&lt;p&gt;Kith addresses these cases using PostgreSQL transactions and atomic RPC functions for critical multi-step writes. Where appropriate, row locking coordinates competing operations so that they cannot independently proceed on stale assumptions about the same protected state.&lt;/p&gt;

&lt;p&gt;The objective is to keep related changes within a single transactional boundary.&lt;/p&gt;

&lt;p&gt;For example, a critical state transition should not update one record successfully and then leave its related records unchanged because a subsequent operation failed.&lt;/p&gt;

&lt;p&gt;Moving these transitions into the database reduces the number of application-level steps that can interleave with competing requests and makes the consistency boundary explicit.&lt;/p&gt;

&lt;p&gt;This approach does not eliminate the need to reason about isolation levels, lock ordering, or transaction failures. It provides a mechanism for enforcing the invariants that those operations require.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Proxy Membership: Designing for Families With Different Technical Abilities&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not every family member can or wants to use a mobile application.&lt;/p&gt;

&lt;p&gt;A household may need to record contributions or assign responsibilities for a baby, an elderly relative, or someone who does not use digital tools independently.&lt;/p&gt;

&lt;p&gt;Kith addresses this through proxy membership.&lt;/p&gt;

&lt;p&gt;An authorized administrator can manage participation on behalf of a proxy member while maintaining an explicit relationship between the proxy and the accountable manager.&lt;/p&gt;

&lt;p&gt;Actions performed on behalf of a proxy are separately recorded, preserving accountability without requiring every participant to operate an account independently.&lt;/p&gt;

&lt;p&gt;This is more than a convenience feature. It introduces a distinct authorization question: who is performing an action, and on whose behalf is that action being performed?&lt;/p&gt;

&lt;p&gt;Maintaining that distinction allows the application to support family circumstances without treating every action as if the affected member performed it personally.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unattended Automation With BullMQ&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Recurring financial activities should not depend on an administrator remembering to trigger every operation manually.&lt;/p&gt;

&lt;p&gt;Kith uses nine Redis-backed BullMQ queues and scheduled jobs to coordinate recurring pools, reminders, notifications, cleanup tasks, and member-engagement updates.&lt;/p&gt;

&lt;p&gt;Recurring pool management&lt;/p&gt;

&lt;p&gt;Recurring pools require predictable lifecycle transitions. Kith generates upcoming cycles three months ahead of time and automates scheduled opening and closing.&lt;/p&gt;

&lt;p&gt;Where configured, unpaid balances can carry forward into the next cycle as adjusted targets rather than being represented as payments that never occurred.&lt;/p&gt;

&lt;p&gt;This preserves the distinction between an outstanding obligation and an actual contribution.&lt;/p&gt;

&lt;p&gt;Recoverable notifications&lt;/p&gt;

&lt;p&gt;Notification delivery introduces an external dependency. A job can be processed successfully by the application while the downstream email or push provider rejects the delivery or becomes unavailable.&lt;/p&gt;

&lt;p&gt;Kith uses an outbox-oriented workflow to record delivery state and a recovery sweep that runs every five minutes to identify and re-enqueue eligible stuck deliveries.&lt;/p&gt;

&lt;p&gt;This makes failures observable and creates a recovery path for transient problems instead of silently discarding work.&lt;/p&gt;

&lt;p&gt;It does not mean every external delivery is guaranteed to succeed. Permanent provider failures and other terminal errors still require appropriate failure handling.&lt;/p&gt;

&lt;p&gt;Scheduled maintenance&lt;/p&gt;

&lt;p&gt;Background workers also handle overdue-payment reminders, task-status updates, invitation cleanup, and member-engagement recomputation.&lt;/p&gt;

&lt;p&gt;Moving this work out of user-facing requests keeps routine operations from depending on a user remaining online or an administrator manually initiating each step.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Security Beyond Authentication&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Financial records require protection at several levels, not just a login screen.&lt;/p&gt;

&lt;p&gt;Kith includes authorization and validation mechanisms designed to reduce the risk of information disclosure and unauthorized changes.&lt;/p&gt;

&lt;p&gt;Tenant isolation&lt;/p&gt;

&lt;p&gt;Workspace membership checks return a uniform not-found response when access cannot be established. This avoids revealing whether a particular resource exists to an unauthorized requester.&lt;/p&gt;

&lt;p&gt;File integrity validation&lt;/p&gt;

&lt;p&gt;Payment proofs, task evidence, and milestone photographs undergo file-signature validation using actual file contents rather than relying solely on client-declared content types.&lt;/p&gt;

&lt;p&gt;This helps detect files whose contents do not match their claimed formats.&lt;/p&gt;

&lt;p&gt;Administrative safeguards&lt;/p&gt;

&lt;p&gt;The application prevents the last remaining active workspace administrator from being removed or demoted.&lt;/p&gt;

&lt;p&gt;Administrative dashboard access is also restricted through IP allowlisting and timing-safe Basic Authentication verification.&lt;/p&gt;

&lt;p&gt;These controls address different risks: authorization mistakes, misleading file metadata, accidental loss of administrative access, and unauthorized access to privileged interfaces.&lt;/p&gt;

&lt;p&gt;They should be understood as layers of protection rather than as a claim that any single mechanism makes the entire application secure.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;Kith began with a familiar problem: families need to coordinate shared finances and responsibilities, but informal tools become difficult to manage as those relationships grow more complex.&lt;/p&gt;

&lt;p&gt;Solving that problem required engineering beyond ordinary CRUD operations. Financial history needed explicit integrity rules, concurrent writes needed safe state transitions, repeated requests needed duplicate protection, and background workflows needed observable failure handling and recovery.&lt;/p&gt;

&lt;p&gt;The broader lesson is that trustworthy financial software depends on making important invariants explicit and enforcing them at the appropriate layer.&lt;/p&gt;

&lt;p&gt;For Kith, the goal is to combine that engineering discipline with an interface and workflow that remain accessible to the people using it: families coordinating money, responsibilities, and trust.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>node</category>
      <category>backend</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Not All AI Provider Failures Are the Same: Building a Fault-Aware LLM Fallback System</title>
      <dc:creator>Oluwaseyi Ogunsola</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:21:19 +0000</pubDate>
      <link>https://dev.to/oluwaseyi_ogunsola_691294/not-all-ai-provider-failures-are-the-same-building-a-fault-aware-llm-fallback-system-31a3</link>
      <guid>https://dev.to/oluwaseyi_ogunsola_691294/not-all-ai-provider-failures-are-the-same-building-a-fault-aware-llm-fallback-system-31a3</guid>
      <description>&lt;p&gt;Not All AI Provider Failures Are the Same: Building a Fault-Aware LLM Fallback System&lt;/p&gt;

&lt;p&gt;A common approach to multi-provider LLM integration is straightforward: send a request to a provider, catch an error if it fails, and try the next available key or provider.&lt;/p&gt;

&lt;p&gt;That works for some failures, but treating every error identically introduces unnecessary problems.&lt;/p&gt;

&lt;p&gt;An invalid API key, a provider-wide outage, a deprecated model identifier, and a malformed application request are not the same failure. They have different causes and require different responses.&lt;/p&gt;

&lt;p&gt;For example, if a provider returns an HTTP 503 because its service is temporarily unavailable, cooling down a perfectly valid API key does not solve the underlying problem. It simply reduces the key's availability after a failure that was unrelated to its validity.&lt;/p&gt;

&lt;p&gt;In StudyHub, I built a multi-provider LLM routing system that classifies provider failures before deciding whether to cool a key, switch providers, invalidate a cached model, or stop the request.&lt;/p&gt;

&lt;p&gt;This article explores that design.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why Failure Classification Matters&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A fallback system needs to answer two questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What most likely caused the failure?&lt;/li&gt;
&lt;li&gt;Which action is appropriate for that failure?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without classification, the router can waste valid credentials, repeatedly request models that no longer exist, or continue retrying requests that will fail regardless of which provider receives them.&lt;/p&gt;

&lt;p&gt;StudyHub represents provider failures through a custom "ProviderCallError" that carries diagnostic information such as status codes, provider identifiers, and parsed error details.&lt;/p&gt;

&lt;p&gt;The system then classifies failures into four categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"KEY_FAULT"&lt;/li&gt;
&lt;li&gt;"PROVIDER_TRANSIENT"&lt;/li&gt;
&lt;li&gt;"BAD_MODEL"&lt;/li&gt;
&lt;li&gt;"NON_RETRYABLE"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The taxonomy determines how the routing system should respond.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;KEY_FAULT: Cool the Affected Key&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The "KEY_FAULT" category represents failures associated with the credential or account state.&lt;/p&gt;

&lt;p&gt;In the current classification rules, these include HTTP statuses 401, 402, 403, and 429.&lt;/p&gt;

&lt;p&gt;Examples include invalid credentials, account restrictions, billing problems, and rate limits associated with the account or key.&lt;/p&gt;

&lt;p&gt;When the router classifies a failure this way, it cools the affected key and attempts another eligible key or provider.&lt;/p&gt;

&lt;p&gt;The intention is to avoid repeatedly sending requests through a credential that is currently unable to serve them.&lt;/p&gt;

&lt;p&gt;However, status codes are signals rather than universal explanations. A 429 response, for example, may represent a key-specific rate limit, an account-level quota, or another provider-specific restriction.&lt;/p&gt;

&lt;p&gt;For that reason, the classification should be understood in the context of the provider's API behavior and the error information available to the implementation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PROVIDER_TRANSIENT: Change Providers Without Penalizing the Key&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The "PROVIDER_TRANSIENT" category handles failures such as HTTP 500, 502, 503, and 504, along with qualifying network-level connection failures.&lt;/p&gt;

&lt;p&gt;These failures commonly indicate temporary provider or network problems rather than invalid credentials.&lt;/p&gt;

&lt;p&gt;When such a failure is classified as transient, StudyHub advances to another provider without unnecessarily cooling the current key.&lt;/p&gt;

&lt;p&gt;This distinction helps preserve credential availability during external outages.&lt;/p&gt;

&lt;p&gt;It also illustrates why retry behavior should follow the cause of a failure rather than treating every exception as evidence that the current key is unusable.&lt;/p&gt;

&lt;p&gt;The exact handling still depends on the provider's response semantics and the router's classification rules.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;BAD_MODEL: Invalidate the Model, Not the Credential&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Providers change their model catalogues. Models may be renamed, removed, deprecated, or made unavailable to a particular endpoint.&lt;/p&gt;

&lt;p&gt;A model identifier that worked yesterday may therefore fail even though the provider and credential remain usable.&lt;/p&gt;

&lt;p&gt;StudyHub classifies certain HTTP 400 responses as "BAD_MODEL" when structured error information indicates that the requested model is invalid, missing, or decommissioned.&lt;/p&gt;

&lt;p&gt;The classifier examines fields such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"error.type"&lt;/li&gt;
&lt;li&gt;"error.code"&lt;/li&gt;
&lt;li&gt;"error.message"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It then checks for known indicators of an unavailable model.&lt;/p&gt;

&lt;p&gt;When a response matches the classification rules, the router evicts the affected model from the shared Redis discovery cache rather than cooling the key.&lt;/p&gt;

&lt;p&gt;This prevents other application instances from continuing to select the same cached model identifier after the system has determined that it is no longer usable.&lt;/p&gt;

&lt;p&gt;The distinction is important: invalidating a model and invalidating a credential are different operations because they address different underlying resources.&lt;/p&gt;

&lt;p&gt;The classification also needs to remain conservative. Not every HTTP 400 means that a model has disappeared; malformed payloads and other client-side errors can produce the same status.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;NON_RETRYABLE: Fail Fast When Fallback Is Unlikely to Help&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final category, "NON_RETRYABLE", acts as the conservative default for errors that do not match the known classifications.&lt;/p&gt;

&lt;p&gt;The reasoning is that some failures originate in the application's request construction rather than in the provider's availability.&lt;/p&gt;

&lt;p&gt;If the application sends a malformed payload, rotating through several providers may simply repeat the same mistake.&lt;/p&gt;

&lt;p&gt;In those cases, failing fast avoids wasting requests and obscuring the original error behind a chain of unsuccessful fallback attempts.&lt;/p&gt;

&lt;p&gt;This policy depends on the classifier being accurate. An unfamiliar provider error may be misclassified as non-retryable even when another provider could have handled the request.&lt;/p&gt;

&lt;p&gt;Unknown errors should therefore remain observable, and the taxonomy should evolve as new provider response patterns are understood.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Normalizing Python Request Exceptions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A practical implementation challenge is that Python's "requests" library exposes failures through different exception types.&lt;/p&gt;

&lt;p&gt;An HTTP error may include a response with a status code and structured body. A connection failure or timeout may occur without any HTTP response at all.&lt;/p&gt;

&lt;p&gt;StudyHub uses a helper, "_wrap_request_exception", to normalize these failures into a common representation.&lt;/p&gt;

&lt;p&gt;For HTTP errors, the wrapper extracts the response status code and attempts to parse the response body as JSON so the classifier can inspect structured error details.&lt;/p&gt;

&lt;p&gt;For network failures, the wrapper captures the exception information even when no response was received. A fallback regular expression, "_NETWORK_MESSAGE_FALLBACK_RE", can inspect the error message when necessary.&lt;/p&gt;

&lt;p&gt;Normalizing the information allows the classification layer to reason about failures consistently without requiring every routing decision to understand the details of each Python exception type.&lt;/p&gt;

&lt;p&gt;The goal is to separate exception handling from the policy that determines what the router should do next.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sharing Failure State Across Application Instances&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A failure classification is less useful if every application instance maintains an independent view of provider health.&lt;/p&gt;

&lt;p&gt;Suppose one instance identifies a key as temporarily unusable. If another instance immediately retries that same key because it has not observed the first instance's state change, the system can generate unnecessary failures.&lt;/p&gt;

&lt;p&gt;StudyHub centralizes runtime state in Redis using dedicated key families for cooldowns and provider blacklists, including prefixes such as:&lt;/p&gt;

&lt;p&gt;"sh:1:ai:cooldown:"&lt;/p&gt;

&lt;p&gt;"sh:1:ai:blacklist:"&lt;/p&gt;

&lt;p&gt;This gives multiple instances a shared mechanism for observing relevant failure state.&lt;/p&gt;

&lt;p&gt;The system also provides an environment-variable-controlled switch, "AI_PROVIDER_REDIS_STATE_ENABLED", that can disable Redis-backed provider-state tracking and fall back to process-local in-memory state.&lt;/p&gt;

&lt;p&gt;That fallback can preserve some operation when Redis is unavailable, but it changes the coordination model. Individual processes may then hold different cooldown and blacklist information.&lt;/p&gt;

&lt;p&gt;In other words, the fallback favors continued local operation over consistent shared state. It should not be mistaken for equivalent distributed coordination.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Trade-Offs Behind Fault-Aware Routing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The classification system improves the router's ability to respond appropriately, but it also introduces design responsibilities.&lt;/p&gt;

&lt;p&gt;Classification must evolve&lt;/p&gt;

&lt;p&gt;Providers do not necessarily use status codes and error bodies in identical ways. A classifier that works for one provider may need additional rules for another.&lt;/p&gt;

&lt;p&gt;Fallback is not always the correct answer&lt;/p&gt;

&lt;p&gt;A second provider may not support the requested model, modality, or request format. The router must still respect provider compatibility and request constraints.&lt;/p&gt;

&lt;p&gt;Shared state introduces its own dependency&lt;/p&gt;

&lt;p&gt;Redis allows instances to coordinate cooldowns and blacklists, but Redis availability and the behavior of the local-state fallback must be considered explicitly.&lt;/p&gt;

&lt;p&gt;Diagnostics must remain useful&lt;/p&gt;

&lt;p&gt;When the classifier cannot confidently identify a failure, preserving diagnostic information and monitoring unknown errors helps prevent silent misclassification.&lt;/p&gt;

&lt;p&gt;These trade-offs are part of building a reliable routing layer. Provider fallback is not simply a list of API keys; it is a policy for responding to different failure conditions.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;Multi-provider LLM routing becomes more reliable when the system distinguishes the reasons requests fail.&lt;/p&gt;

&lt;p&gt;StudyHub's failure taxonomy separates credential-related problems, transient provider failures, invalid model identifiers, and errors that should stop further fallback attempts.&lt;/p&gt;

&lt;p&gt;Each category leads to a different response: cool the relevant key, switch providers without penalizing the credential, invalidate a stale model entry, or fail fast.&lt;/p&gt;

&lt;p&gt;Combined with normalized Python exceptions and Redis-backed shared state, this design makes provider failure handling more deliberate and observable.&lt;/p&gt;

&lt;p&gt;The broader lesson is that resilience does not come from retrying everything. It comes from understanding what failed, choosing a response appropriate to that failure, and preserving enough information to investigate cases the system does not yet understand.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
