How Model Associations Enable Intelligent Channel Selection in AxonHub
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/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. Unlikechannel_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/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 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/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/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
UpdatedAttimestamp updates - The model's
UpdatedAttimestamp 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/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:
// 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/unstable/internal/objects/model.go), invoking MatchAssociations from [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
resolveAssociationsandbiz.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
FallbackToChannelsOnModelNotFoundwhen 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/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/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/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".
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →