# How Model Associations Enable Intelligent Channel Selection in AxonHub

> AxonHub's model associations route requests intelligently using rules and regex for dynamic channel selection. Explore how this eliminates hard-coded provider logic.

- Repository: [Loop/axonhub](https://github.com/looplj/axonhub)
- Tags: deep-dive
- Published: 2026-03-06

---

**AxonHub uses declarative ModelAssociation definitions to route model requests to specific provider channels based on matching rules, priorities, and regex patterns, enabling dynamic and intelligent channel selection without hard-coded provider logic.**

Model associations serve as the routing engine behind AxonHub's intelligent channel selection, transforming static model definitions into dynamic routing policies. When a client requests a model, the platform evaluates these associations to determine which provider channels should handle the request. This article examines how the `looplj/axonhub` repository implements this mechanism through the `ModelAssociation` type, the `DefaultSelector`, and the association matching engine.

## The Foundation: ModelAssociation Structure

Every `Model` in AxonHub stores its routing logic in the `Settings.Associations` field, which contains a slice of `*objects.ModelAssociation` objects. According to the source in [[`internal/objects/model.go`](https://github.com/looplj/axonhub/blob/main/internal/objects/model.go)](https://github.com/looplj/axonhub/blob/unstable/internal/objects/model.go#L39-L59), these associations link a model identifier to one or more provider channels through declarative rules.

Each association specifies a `Type` field that determines the matching strategy, along with type-specific configuration structs such as `ChannelModelAssociation`, `ChannelRegexAssociation`, and `RegexAssociation`. This architecture allows the platform to support everything from exact model ID matches to complex regex patterns across tagged channel groups.

## The Six Association Types for Channel Routing

AxonHub supports six distinct association types that provide granular control over channel selection. The matching engine processes these in sequence to build the final candidate list:

- **`channel_model`** – Matches a concrete model ID within a specific channel ID. This creates a hard affinity between a model and a single provider.

- **`channel_regex`** – Applies a regex pattern against model IDs, but only within a specified channel. This is useful when a channel hosts multiple model variants following a naming convention.

- **`regex`** – Matches model IDs across all enabled channels using a regex pattern, with optional exclusion rules. This provides global routing rules that apply platform-wide.

- **`model`** – Performs a direct model ID match evaluated against every channel. Unlike `channel_model`, this does not restrict the search to a specific channel.

- **`channel_tags_model`** – Routes a specific model ID to any channel whose metadata tags match a defined set. This enables dynamic routing based on channel capabilities (e.g., "gpu-enabled" or "eu-region").

- **`channel_tags_regex`** – Applies a regex pattern to channels filtered by their tags, combining pattern matching with tag-based channel selection.

## The Selection Pipeline: From Request to Channel Resolution

When a client request arrives, the **DefaultSelector** orchestrates the resolution process. Located in [[`internal/server/orchestrator/candidates.go`](https://github.com/looplj/axonhub/blob/main/internal/server/orchestrator/candidates.go)](https://github.com/looplj/axonhub/blob/unstable/internal/server/orchestrator/candidates.go), this component translates high-level model requests into concrete channel candidates through a multi-stage pipeline.

### Loading Model Definitions

The selector first loads the AxonHub `Model` by the requested ID using `ModelService.GetModelByModelID`. If the model exists and contains associations, the selector proceeds to candidate resolution. If no model is found, the system can optionally fallback to legacy channel selection when `FallbackToChannelsOnModelNotFound` is enabled (see [[`candidates.go`](https://github.com/looplj/axonhub/blob/main/candidates.go) lines 70-75](https://github.com/looplj/axonhub/blob/unstable/internal/server/orchestrator/candidates.go#L70-L75)).

### The Association Matching Engine

The core resolution logic resides in `resolveAssociations`, which delegates to `biz.MatchAssociations` defined in [[`internal/server/biz/model_association_matcher.go`](https://github.com/looplj/axonhub/blob/main/internal/server/biz/model_association_matcher.go)](https://github.com/looplj/axonhub/blob/unstable/internal/server/biz/model_association_matcher.go#L60-L68). This function iterates over the model's associations in order, applying type-specific handlers such as `matchChannelModel`, `matchChannelRegex`, and `matchRegex` (see lines 8-33 for the channel model implementation).

The matcher returns a slice of `ModelChannelConnection` objects containing the matched channel, model entries, and association priority. It automatically deduplicates identical *(channel, model)* pairs and respects the `Disabled` flag on associations.

## Priority, Deduplication, and Caching Strategies

Each `ModelAssociation` carries an integer `Priority` field that determines precedence when multiple associations match the same channel. The `DefaultSelector` preserves the original association order while tagging each connection with its source priority. The default UI interprets lower priority numbers as higher precedence, allowing administrators to create fallback rules that only activate when primary channels are unavailable.

To optimize performance, the selector implements a caching layer in [[`candidates.go`](https://github.com/looplj/axonhub/blob/main/candidates.go)](https://github.com/looplj/axonhub/blob/unstable/internal/server/orchestrator/candidates.go) (lines 80-100). The cache stores resolved candidates per model ID and invalidates entries when:
- The number of enabled channels changes
- A channel's `UpdatedAt` timestamp updates
- The model's `UpdatedAt` timestamp updates
- The 5-minute cache TTL expires

This ensures that channel selection remains responsive while avoiding expensive regex matching on every request.

## Fallback Mechanisms for Unmatched Models

When a requested model lacks explicit associations or does not exist in the AxonHub registry, the system provides a safety net through the `FallbackToChannelsOnModelNotFound` setting. Defined in [[`internal/server/biz/system.go`](https://github.com/looplj/axonhub/blob/main/internal/server/biz/system.go)](https://github.com/looplj/axonhub/blob/unstable/internal/server/biz/system.go), this boolean flag enables legacy channel selection behavior that routes requests based on channel capabilities rather than model-specific associations.

This fallback ensures high availability during migrations or when integrating new models that have not yet been fully configured with routing rules.

## Implementing Intelligent Channel Selection

The following example demonstrates how to define a model with multiple associations and resolve channel candidates using the AxonHub selector:

```go
// 1️⃣ Define a model with two associations (high‑priority specific channel, then a regex fallback)
model := &objects.Model{
    ModelID: "gpt‑4‑custom",
    Settings: &objects.ModelSettings{
        Associations: []*objects.ModelAssociation{
            // Prefer channel 42 for this exact model
            {Type: "channel_model", Priority: 0, ChannelModel: &objects.ChannelModelAssociation{ChannelID: 42, ModelID: "gpt‑4‑custom"}},
            // If not present, any channel that offers a model matching ^gpt‑4
            {Type: "regex", Priority: 10, Regex: &objects.RegexAssociation{Pattern: "^gpt-4"}},
        },
    },
}

// 2️⃣ Store the model (skipped here – assume it is persisted)

// 3️⃣ Resolve candidates for a request
req := &llm.Request{Model: "gpt‑4‑custom"}
selector := orchestrator.NewDefaultSelector(channelSvc, modelSvc, systemSvc)
candidates, _ := selector.Select(context.Background(), req)

// 4️⃣ Result – first candidate will be channel 42, second (if needed) any channel matching the regex.
fmt.Println(candidates[0].Channel.Name) // → "OpenAI‑42"

```

The `Select` method triggers the full resolution pipeline: loading the model from [[`internal/objects/model.go`](https://github.com/looplj/axonhub/blob/main/internal/objects/model.go)](https://github.com/looplj/axonhub/blob/unstable/internal/objects/model.go), invoking `MatchAssociations` from [[`internal/server/biz/model_association_matcher.go`](https://github.com/looplj/axonhub/blob/main/internal/server/biz/model_association_matcher.go)](https://github.com/looplj/axonhub/blob/unstable/internal/server/biz/model_association_matcher.go), and returning prioritized `ModelChannelConnection` objects.

## Summary

- **Model associations** act as declarative routing policies stored in `Model.Settings.Associations`, enabling dynamic channel selection without provider-specific hard-coding.
- **Six association types** (`channel_model`, `channel_regex`, `regex`, `model`, `channel_tags_model`, `channel_tags_regex`) provide granular control from exact matches to tag-based routing.
- **DefaultSelector** orchestrates resolution through `resolveAssociations` and `biz.MatchAssociations`, handling deduplication, priority ordering, and caching.
- **Performance optimization** includes a 5-minute TTL cache with invalidation triggers on channel or model updates, ensuring efficient regex matching.
- **Fallback mechanisms** ensure high availability through `FallbackToChannelsOnModelNotFound` when models lack explicit associations.

## Frequently Asked Questions

### How does AxonHub prioritize when multiple associations match the same channel?

When multiple associations match, the system creates a `ModelChannelConnection` for each match while preserving the original association order. Each connection inherits the `Priority` value from its source association, where lower numbers indicate higher precedence. The default UI orders candidates by this priority field, ensuring that high-priority specific channels (like `channel_model` with priority 0) are preferred over fallback regex rules (like `regex` with priority 10).

### What happens if a requested model has no associations defined?

If a model exists but has no associations, or if the model cannot be found in the AxonHub registry, the system checks the `FallbackToChannelsOnModelNotFound` setting in [[`internal/server/biz/system.go`](https://github.com/looplj/axonhub/blob/main/internal/server/biz/system.go)](https://github.com/looplj/axonhub/blob/unstable/internal/server/biz/system.go). When enabled, the selector falls back to legacy channel selection behavior that routes requests based on general channel capabilities rather than model-specific routing rules, ensuring requests still reach available providers.

### How does the caching mechanism optimize association resolution performance?

The `DefaultSelector` implements a caching layer in [[`internal/server/orchestrator/candidates.go`](https://github.com/looplj/axonhub/blob/main/internal/server/orchestrator/candidates.go)](https://github.com/looplj/axonhub/blob/unstable/internal/server/orchestrator/candidates.go) (lines 80-100) that stores resolved candidates per model ID. This cache invalidates when the number of enabled channels changes, when any channel's `UpdatedAt` timestamp updates, when the model's `UpdatedAt` changes, or when the 5-minute TTL expires. This prevents expensive regex matching on every request while ensuring routing decisions reflect current channel availability.

### Can associations use regular expressions to match model IDs across multiple channels?

Yes, AxonHub supports three regex-based association types defined in [[`internal/objects/model.go`](https://github.com/looplj/axonhub/blob/main/internal/objects/model.go)](https://github.com/looplj/axonhub/blob/unstable/internal/objects/model.go). The `regex` type matches model IDs across all enabled channels with optional exclusion rules, while `channel_regex` restricts pattern matching to a specific channel. Additionally, `channel_tags_regex` applies regex patterns only to channels whose metadata tags match a defined set, enabling powerful routing rules like directing all `^gpt-4.*` models to channels tagged with "openai-compatible".