feat(routing): tag/metadata conditional routing - #686

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag
Jul 1, 2026
Merged

feat(routing): tag/metadata conditional routing#686
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds tag/metadata-conditional routing: a routing model's targets can carry tags, and a request selects among them by the routing tags it carries. Closes the conditional/tag-routing gap vs LiteLLM / Portkey / Kong (routing gap list #873).

How

Request tags come from an out-of-band header, x-aisix-routing-tags (comma-separated), resolved once by the existing ClientContext extractor alongside client IP / User-Agent. They're read from the headers, never the body, so they never reach the upstream request payload.

Filtering happens in the shared resolve_attempt_models, as a pre-filter that narrows the target set before the configured strategy orders it — so tag routing composes with round-robin / weighted / failover / least-*, rather than being a new strategy. eligible_targets mirrors LiteLLM's tag_based_routing:

  • No target is tagged → tag routing isn't in use; every target is eligible.
  • Request carries tags → targets whose tags intersect it (match-any); if none match, fall back to "default"-tagged targets.
  • Request has no tags → "default"-tagged targets if any, else all.
  • Tags that match nothing with no default configured → a "no target matches tags" error.

RoutingTarget gains an optional tags: Vec<String> (additive, serde-default, so existing configs are unaffected). count_tokens's dispatch now takes the whole ClientContext (it previously took just source_ip), matching the other three routing-capable endpoints.

LiteLLM baseline

Semantics follow litellm/router_strategy/tag_based_routing.py (match-any intersect + default fallback). One deliberate divergence: LiteLLM sources tags from the request body (metadata.tags); we use a header. Our bridges serialize the whole request body (including passthrough extra) to the upstream, so a body metadata field would be forwarded and rejected by providers that constrain metadata (OpenAI requires string values). A header keeps the routing signal out of the upstream payload without a body-stripping layer. The matching/fallback behavior is unchanged from LiteLLM.

Tests

  • Core unit: RoutingTarget tag predicates + tags deserialization.
  • Proxy unit: eligible_targets across all branches (no-tags passthrough, match-any, tagged-no-match→default, untagged→default, untagged-no-default→all, tagged-no-match-no-default→empty), and header parsing.
  • DP E2E (tag-routing-e2e.test.ts): a routing model with eu / us / default targets — x-aisix-routing-tags selects the matching upstream, an untagged request and an unmatched tag both fall back to default, and the header is asserted absent from the upstream body.

Notes

Regenerated the committed resource schemas (routing/model). User-facing docs ship as one consolidated api7/docs routing-strategies page for the whole routing family.

Fixes api7/AISIX-Cloud#927

Summary by CodeRabbit

  • New Features

    • Added tag-based routing for requests, letting traffic be directed by request tags.
    • Support for a default routing target was added for untagged or unmatched requests.
    • Requests can now include routing tags via a new header, and matching behavior is covered end-to-end.
  • Bug Fixes

    • Routing now falls back more predictably when no tag match is found.
    • Untagged requests are handled consistently using default-target behavior when available.

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based routing to RoutingTarget, allowing targets to declare tags and a "default" fallback tag. Client requests can supply routing tags via a new header, which propagate through dispatch call sites into resolve_attempt_models, which now filters eligible targets by tag match-any/default-fallback logic before applying existing strategy ordering. Schemas and e2e tests are updated accordingly.

Changes

Tag-based routing

Layer / File(s)Summary
RoutingTarget tags contract
crates/aisix-core/src/models/routing.rs
Adds optional tags field, DEFAULT_ROUTING_TAG constant, with_tags, has_tags, is_default_target, matches_request_tags, and unit tests.
Client routing-tags extraction
crates/aisix-proxy/src/client_ip.rs
Adds ROUTING_TAGS_HEADER constant, routing_tags field on ClientContext, parse_routing_tags helper wired into the extractor, and unit tests.
Eligibility filtering in resolve_attempt_models
crates/aisix-proxy/src/routing.rs
Adds eligible_targets filter with match-any/default-fallback logic, extends resolve_attempt_models with request_tags, pre-filters targets, returns InvalidRequest on empty match, and adds unit tests.
Dispatch call site wiring
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/count_tokens.rs
Passes ClientContext/client.routing_tags into dispatch and resolve_attempt_models calls across all four endpoint handlers.
Schema and e2e validation
schemas/resources/model.schema.json, schemas/resources/routing.schema.json, tests/e2e/src/cases/tag-routing-e2e.test.ts
Documents tags field in both schemas and adds an e2e test validating matched/unmatched/untagged tag routing and metadata isolation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant ClientContext
participant Dispatch
participant resolve_attempt_models
participant RoutingRegistry
Client->>ClientContext: request with x-aisix-routing-tags header
ClientContext->>ClientContext: parse_routing_tags(header)
ClientContext-->>Dispatch: routing_tags: Vec<String>
Dispatch->>resolve_attempt_models: resolve_attempt_models(..., request_tags)
resolve_attempt_models->>resolve_attempt_models: eligible_targets(routing.targets, request_tags)
alt no eligible targets
resolve_attempt_models-->>Dispatch: ProxyError::InvalidRequest
else eligible targets found
resolve_attempt_models->>RoutingRegistry: pick_targets(eligible subset)
RoutingRegistry-->>resolve_attempt_models: selected targets
resolve_attempt_models-->>Dispatch: attempt models
end
Loading

Possibly related issues

  • api7/AISIX-Cloud#927: Implements the same tag-based routing feature end-to-end, adding RoutingTarget.tags, threading request tags into selection, and default-tag fallback logic.

Possibly related PRs

  • api7/aisix#472: Both modify resolve_attempt_models and its call sites in /v1/messages and chat dispatch flows.
  • api7/aisix#534: Both touch the routing/dispatch flow for /v1/chat/completions, /v1/messages, and /v1/responses, modifying how routing attempts are resolved.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningE2E hits the happy/default path, but it doesn't pin the new no-default/no-match 400 or a multi-tag match-any case, so the core routing behavior is still under-tested.Add one real request with x-aisix-routing-tags: eu,premium and one tagged model set without default that asserts the 400 error path; optional: cover another wired endpoint too.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding tag/metadata-conditional routing in routing logic.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security regressions found: the new routing tags only affect target selection, while auth checks and secret-bearing headers/logging remain unchanged in the touched code.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/routing-tag

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/src/cases/tag-routing-e2e.test.ts (1)

94-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing e2e coverage for the "no match, no default" error path.

The PR states that when tags match nothing and no default target exists, an error is returned, but this test only exercises configurations that always include a tag-default target. Consider adding a scenario with no default-tagged target and a request tag that matches nothing, asserting the request fails (e.g., a 400/invalid-request response).

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts` around lines 94 - 136, The e2e
test in tag-routing should also cover the failure path where no target matches
and no `default` target exists. Extend the existing `selects the tagged target,
defaulting when unmatched or untagged` scenario, or add a new case рядом with
`askWithTags`/`admin.createModel`, that creates a routing config without any
`default`-tagged target, sends a request with an unmatched tag like “apac”, and
asserts the request is rejected with an invalid-request/400-style error. Use the
existing `startOpenAiUpstream`, `createOpenAiModel`, and `waitConfigPropagation`
helpers so the new boundary case stays consistent with the current test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-proxy/src/client_ip.rs`:
- Around line 180-187: The routing-tag parsing in parse_routing_tags currently
preserves case, which can cause exact-match lookups in
RoutingTarget::matches_request_tags and is_default_target to miss values like EU
vs eu. Normalize each parsed tag at the header parse boundary by converting to a
consistent case before collecting, so downstream tag matching behaves
case-insensitively and does not incorrectly fall back to default.
---
Nitpick comments:
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts`:
- Around line 94-136: The e2e test in tag-routing should also cover the failure
path where no target matches and no `default` target exists. Extend the existing
`selects the tagged target, defaulting when unmatched or untagged` scenario, or
add a new case рядом with `askWithTags`/`admin.createModel`, that creates a
routing config without any `default`-tagged target, sends a request with an
unmatched tag like “apac”, and asserts the request is rejected with an
invalid-request/400-style error. Use the existing `startOpenAiUpstream`,
`createOpenAiModel`, and `waitConfigPropagation` helpers so the new boundary
case stays consistent with the current test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f2c66c3-d86a-4dc7-a6c3-d356a838b69f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e82aac and 2dcbf83.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/client_ip.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/tag-routing-e2e.test.ts

Comment threadcrates/aisix-proxy/src/client_ip.rs
@jarvis9443
jarvis9443 merged commit 6ddb3cf into mainJul 1, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/routing-tag branch July 1, 2026 15:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(routing): tag/metadata conditional routing - #686

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag
Jul 1, 2026
Merged

feat(routing): tag/metadata conditional routing#686
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds tag/metadata-conditional routing: a routing model's targets can carry tags, and a request selects among them by the routing tags it carries. Closes the conditional/tag-routing gap vs LiteLLM / Portkey / Kong (routing gap list #873).

How

Request tags come from an out-of-band header, x-aisix-routing-tags (comma-separated), resolved once by the existing ClientContext extractor alongside client IP / User-Agent. They're read from the headers, never the body, so they never reach the upstream request payload.

Filtering happens in the shared resolve_attempt_models, as a pre-filter that narrows the target set before the configured strategy orders it — so tag routing composes with round-robin / weighted / failover / least-*, rather than being a new strategy. eligible_targets mirrors LiteLLM's tag_based_routing:

  • No target is tagged → tag routing isn't in use; every target is eligible.
  • Request carries tags → targets whose tags intersect it (match-any); if none match, fall back to "default"-tagged targets.
  • Request has no tags → "default"-tagged targets if any, else all.
  • Tags that match nothing with no default configured → a "no target matches tags" error.

RoutingTarget gains an optional tags: Vec<String> (additive, serde-default, so existing configs are unaffected). count_tokens's dispatch now takes the whole ClientContext (it previously took just source_ip), matching the other three routing-capable endpoints.

LiteLLM baseline

Semantics follow litellm/router_strategy/tag_based_routing.py (match-any intersect + default fallback). One deliberate divergence: LiteLLM sources tags from the request body (metadata.tags); we use a header. Our bridges serialize the whole request body (including passthrough extra) to the upstream, so a body metadata field would be forwarded and rejected by providers that constrain metadata (OpenAI requires string values). A header keeps the routing signal out of the upstream payload without a body-stripping layer. The matching/fallback behavior is unchanged from LiteLLM.

Tests

  • Core unit: RoutingTarget tag predicates + tags deserialization.
  • Proxy unit: eligible_targets across all branches (no-tags passthrough, match-any, tagged-no-match→default, untagged→default, untagged-no-default→all, tagged-no-match-no-default→empty), and header parsing.
  • DP E2E (tag-routing-e2e.test.ts): a routing model with eu / us / default targets — x-aisix-routing-tags selects the matching upstream, an untagged request and an unmatched tag both fall back to default, and the header is asserted absent from the upstream body.

Notes

Regenerated the committed resource schemas (routing/model). User-facing docs ship as one consolidated api7/docs routing-strategies page for the whole routing family.

Fixes api7/AISIX-Cloud#927

Summary by CodeRabbit

  • New Features

    • Added tag-based routing for requests, letting traffic be directed by request tags.
    • Support for a default routing target was added for untagged or unmatched requests.
    • Requests can now include routing tags via a new header, and matching behavior is covered end-to-end.
  • Bug Fixes

    • Routing now falls back more predictably when no tag match is found.
    • Untagged requests are handled consistently using default-target behavior when available.

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based routing to RoutingTarget, allowing targets to declare tags and a "default" fallback tag. Client requests can supply routing tags via a new header, which propagate through dispatch call sites into resolve_attempt_models, which now filters eligible targets by tag match-any/default-fallback logic before applying existing strategy ordering. Schemas and e2e tests are updated accordingly.

Changes

Tag-based routing

Layer / File(s)Summary
RoutingTarget tags contract
crates/aisix-core/src/models/routing.rs
Adds optional tags field, DEFAULT_ROUTING_TAG constant, with_tags, has_tags, is_default_target, matches_request_tags, and unit tests.
Client routing-tags extraction
crates/aisix-proxy/src/client_ip.rs
Adds ROUTING_TAGS_HEADER constant, routing_tags field on ClientContext, parse_routing_tags helper wired into the extractor, and unit tests.
Eligibility filtering in resolve_attempt_models
crates/aisix-proxy/src/routing.rs
Adds eligible_targets filter with match-any/default-fallback logic, extends resolve_attempt_models with request_tags, pre-filters targets, returns InvalidRequest on empty match, and adds unit tests.
Dispatch call site wiring
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/count_tokens.rs
Passes ClientContext/client.routing_tags into dispatch and resolve_attempt_models calls across all four endpoint handlers.
Schema and e2e validation
schemas/resources/model.schema.json, schemas/resources/routing.schema.json, tests/e2e/src/cases/tag-routing-e2e.test.ts
Documents tags field in both schemas and adds an e2e test validating matched/unmatched/untagged tag routing and metadata isolation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant ClientContext
participant Dispatch
participant resolve_attempt_models
participant RoutingRegistry
Client->>ClientContext: request with x-aisix-routing-tags header
ClientContext->>ClientContext: parse_routing_tags(header)
ClientContext-->>Dispatch: routing_tags: Vec<String>
Dispatch->>resolve_attempt_models: resolve_attempt_models(..., request_tags)
resolve_attempt_models->>resolve_attempt_models: eligible_targets(routing.targets, request_tags)
alt no eligible targets
resolve_attempt_models-->>Dispatch: ProxyError::InvalidRequest
else eligible targets found
resolve_attempt_models->>RoutingRegistry: pick_targets(eligible subset)
RoutingRegistry-->>resolve_attempt_models: selected targets
resolve_attempt_models-->>Dispatch: attempt models
end
Loading

Possibly related issues

  • api7/AISIX-Cloud#927: Implements the same tag-based routing feature end-to-end, adding RoutingTarget.tags, threading request tags into selection, and default-tag fallback logic.

Possibly related PRs

  • api7/aisix#472: Both modify resolve_attempt_models and its call sites in /v1/messages and chat dispatch flows.
  • api7/aisix#534: Both touch the routing/dispatch flow for /v1/chat/completions, /v1/messages, and /v1/responses, modifying how routing attempts are resolved.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningE2E hits the happy/default path, but it doesn't pin the new no-default/no-match 400 or a multi-tag match-any case, so the core routing behavior is still under-tested.Add one real request with x-aisix-routing-tags: eu,premium and one tagged model set without default that asserts the 400 error path; optional: cover another wired endpoint too.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding tag/metadata-conditional routing in routing logic.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security regressions found: the new routing tags only affect target selection, while auth checks and secret-bearing headers/logging remain unchanged in the touched code.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/routing-tag

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/src/cases/tag-routing-e2e.test.ts (1)

94-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing e2e coverage for the "no match, no default" error path.

The PR states that when tags match nothing and no default target exists, an error is returned, but this test only exercises configurations that always include a tag-default target. Consider adding a scenario with no default-tagged target and a request tag that matches nothing, asserting the request fails (e.g., a 400/invalid-request response).

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts` around lines 94 - 136, The e2e
test in tag-routing should also cover the failure path where no target matches
and no `default` target exists. Extend the existing `selects the tagged target,
defaulting when unmatched or untagged` scenario, or add a new case рядом with
`askWithTags`/`admin.createModel`, that creates a routing config without any
`default`-tagged target, sends a request with an unmatched tag like “apac”, and
asserts the request is rejected with an invalid-request/400-style error. Use the
existing `startOpenAiUpstream`, `createOpenAiModel`, and `waitConfigPropagation`
helpers so the new boundary case stays consistent with the current test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-proxy/src/client_ip.rs`:
- Around line 180-187: The routing-tag parsing in parse_routing_tags currently
preserves case, which can cause exact-match lookups in
RoutingTarget::matches_request_tags and is_default_target to miss values like EU
vs eu. Normalize each parsed tag at the header parse boundary by converting to a
consistent case before collecting, so downstream tag matching behaves
case-insensitively and does not incorrectly fall back to default.
---
Nitpick comments:
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts`:
- Around line 94-136: The e2e test in tag-routing should also cover the failure
path where no target matches and no `default` target exists. Extend the existing
`selects the tagged target, defaulting when unmatched or untagged` scenario, or
add a new case рядом with `askWithTags`/`admin.createModel`, that creates a
routing config without any `default`-tagged target, sends a request with an
unmatched tag like “apac”, and asserts the request is rejected with an
invalid-request/400-style error. Use the existing `startOpenAiUpstream`,
`createOpenAiModel`, and `waitConfigPropagation` helpers so the new boundary
case stays consistent with the current test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f2c66c3-d86a-4dc7-a6c3-d356a838b69f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e82aac and 2dcbf83.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/client_ip.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/tag-routing-e2e.test.ts

Comment threadcrates/aisix-proxy/src/client_ip.rs
@jarvis9443
jarvis9443 merged commit 6ddb3cf into mainJul 1, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/routing-tag branch July 1, 2026 15:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(routing): tag/metadata conditional routing - #686

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag
Jul 1, 2026
Merged

feat(routing): tag/metadata conditional routing#686
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds tag/metadata-conditional routing: a routing model's targets can carry tags, and a request selects among them by the routing tags it carries. Closes the conditional/tag-routing gap vs LiteLLM / Portkey / Kong (routing gap list #873).

How

Request tags come from an out-of-band header, x-aisix-routing-tags (comma-separated), resolved once by the existing ClientContext extractor alongside client IP / User-Agent. They're read from the headers, never the body, so they never reach the upstream request payload.

Filtering happens in the shared resolve_attempt_models, as a pre-filter that narrows the target set before the configured strategy orders it — so tag routing composes with round-robin / weighted / failover / least-*, rather than being a new strategy. eligible_targets mirrors LiteLLM's tag_based_routing:

  • No target is tagged → tag routing isn't in use; every target is eligible.
  • Request carries tags → targets whose tags intersect it (match-any); if none match, fall back to "default"-tagged targets.
  • Request has no tags → "default"-tagged targets if any, else all.
  • Tags that match nothing with no default configured → a "no target matches tags" error.

RoutingTarget gains an optional tags: Vec<String> (additive, serde-default, so existing configs are unaffected). count_tokens's dispatch now takes the whole ClientContext (it previously took just source_ip), matching the other three routing-capable endpoints.

LiteLLM baseline

Semantics follow litellm/router_strategy/tag_based_routing.py (match-any intersect + default fallback). One deliberate divergence: LiteLLM sources tags from the request body (metadata.tags); we use a header. Our bridges serialize the whole request body (including passthrough extra) to the upstream, so a body metadata field would be forwarded and rejected by providers that constrain metadata (OpenAI requires string values). A header keeps the routing signal out of the upstream payload without a body-stripping layer. The matching/fallback behavior is unchanged from LiteLLM.

Tests

  • Core unit: RoutingTarget tag predicates + tags deserialization.
  • Proxy unit: eligible_targets across all branches (no-tags passthrough, match-any, tagged-no-match→default, untagged→default, untagged-no-default→all, tagged-no-match-no-default→empty), and header parsing.
  • DP E2E (tag-routing-e2e.test.ts): a routing model with eu / us / default targets — x-aisix-routing-tags selects the matching upstream, an untagged request and an unmatched tag both fall back to default, and the header is asserted absent from the upstream body.

Notes

Regenerated the committed resource schemas (routing/model). User-facing docs ship as one consolidated api7/docs routing-strategies page for the whole routing family.

Fixes api7/AISIX-Cloud#927

Summary by CodeRabbit

  • New Features

    • Added tag-based routing for requests, letting traffic be directed by request tags.
    • Support for a default routing target was added for untagged or unmatched requests.
    • Requests can now include routing tags via a new header, and matching behavior is covered end-to-end.
  • Bug Fixes

    • Routing now falls back more predictably when no tag match is found.
    • Untagged requests are handled consistently using default-target behavior when available.

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based routing to RoutingTarget, allowing targets to declare tags and a "default" fallback tag. Client requests can supply routing tags via a new header, which propagate through dispatch call sites into resolve_attempt_models, which now filters eligible targets by tag match-any/default-fallback logic before applying existing strategy ordering. Schemas and e2e tests are updated accordingly.

Changes

Tag-based routing

Layer / File(s)Summary
RoutingTarget tags contract
crates/aisix-core/src/models/routing.rs
Adds optional tags field, DEFAULT_ROUTING_TAG constant, with_tags, has_tags, is_default_target, matches_request_tags, and unit tests.
Client routing-tags extraction
crates/aisix-proxy/src/client_ip.rs
Adds ROUTING_TAGS_HEADER constant, routing_tags field on ClientContext, parse_routing_tags helper wired into the extractor, and unit tests.
Eligibility filtering in resolve_attempt_models
crates/aisix-proxy/src/routing.rs
Adds eligible_targets filter with match-any/default-fallback logic, extends resolve_attempt_models with request_tags, pre-filters targets, returns InvalidRequest on empty match, and adds unit tests.
Dispatch call site wiring
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/count_tokens.rs
Passes ClientContext/client.routing_tags into dispatch and resolve_attempt_models calls across all four endpoint handlers.
Schema and e2e validation
schemas/resources/model.schema.json, schemas/resources/routing.schema.json, tests/e2e/src/cases/tag-routing-e2e.test.ts
Documents tags field in both schemas and adds an e2e test validating matched/unmatched/untagged tag routing and metadata isolation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant ClientContext
participant Dispatch
participant resolve_attempt_models
participant RoutingRegistry
Client->>ClientContext: request with x-aisix-routing-tags header
ClientContext->>ClientContext: parse_routing_tags(header)
ClientContext-->>Dispatch: routing_tags: Vec<String>
Dispatch->>resolve_attempt_models: resolve_attempt_models(..., request_tags)
resolve_attempt_models->>resolve_attempt_models: eligible_targets(routing.targets, request_tags)
alt no eligible targets
resolve_attempt_models-->>Dispatch: ProxyError::InvalidRequest
else eligible targets found
resolve_attempt_models->>RoutingRegistry: pick_targets(eligible subset)
RoutingRegistry-->>resolve_attempt_models: selected targets
resolve_attempt_models-->>Dispatch: attempt models
end
Loading

Possibly related issues

  • api7/AISIX-Cloud#927: Implements the same tag-based routing feature end-to-end, adding RoutingTarget.tags, threading request tags into selection, and default-tag fallback logic.

Possibly related PRs

  • api7/aisix#472: Both modify resolve_attempt_models and its call sites in /v1/messages and chat dispatch flows.
  • api7/aisix#534: Both touch the routing/dispatch flow for /v1/chat/completions, /v1/messages, and /v1/responses, modifying how routing attempts are resolved.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningE2E hits the happy/default path, but it doesn't pin the new no-default/no-match 400 or a multi-tag match-any case, so the core routing behavior is still under-tested.Add one real request with x-aisix-routing-tags: eu,premium and one tagged model set without default that asserts the 400 error path; optional: cover another wired endpoint too.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding tag/metadata-conditional routing in routing logic.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security regressions found: the new routing tags only affect target selection, while auth checks and secret-bearing headers/logging remain unchanged in the touched code.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/routing-tag

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/src/cases/tag-routing-e2e.test.ts (1)

94-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing e2e coverage for the "no match, no default" error path.

The PR states that when tags match nothing and no default target exists, an error is returned, but this test only exercises configurations that always include a tag-default target. Consider adding a scenario with no default-tagged target and a request tag that matches nothing, asserting the request fails (e.g., a 400/invalid-request response).

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts` around lines 94 - 136, The e2e
test in tag-routing should also cover the failure path where no target matches
and no `default` target exists. Extend the existing `selects the tagged target,
defaulting when unmatched or untagged` scenario, or add a new case рядом with
`askWithTags`/`admin.createModel`, that creates a routing config without any
`default`-tagged target, sends a request with an unmatched tag like “apac”, and
asserts the request is rejected with an invalid-request/400-style error. Use the
existing `startOpenAiUpstream`, `createOpenAiModel`, and `waitConfigPropagation`
helpers so the new boundary case stays consistent with the current test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-proxy/src/client_ip.rs`:
- Around line 180-187: The routing-tag parsing in parse_routing_tags currently
preserves case, which can cause exact-match lookups in
RoutingTarget::matches_request_tags and is_default_target to miss values like EU
vs eu. Normalize each parsed tag at the header parse boundary by converting to a
consistent case before collecting, so downstream tag matching behaves
case-insensitively and does not incorrectly fall back to default.
---
Nitpick comments:
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts`:
- Around line 94-136: The e2e test in tag-routing should also cover the failure
path where no target matches and no `default` target exists. Extend the existing
`selects the tagged target, defaulting when unmatched or untagged` scenario, or
add a new case рядом with `askWithTags`/`admin.createModel`, that creates a
routing config without any `default`-tagged target, sends a request with an
unmatched tag like “apac”, and asserts the request is rejected with an
invalid-request/400-style error. Use the existing `startOpenAiUpstream`,
`createOpenAiModel`, and `waitConfigPropagation` helpers so the new boundary
case stays consistent with the current test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f2c66c3-d86a-4dc7-a6c3-d356a838b69f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e82aac and 2dcbf83.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/client_ip.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/tag-routing-e2e.test.ts

Comment threadcrates/aisix-proxy/src/client_ip.rs
@jarvis9443
jarvis9443 merged commit 6ddb3cf into mainJul 1, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/routing-tag branch July 1, 2026 15:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(routing): tag/metadata conditional routing - #686

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag
Jul 1, 2026
Merged

feat(routing): tag/metadata conditional routing#686
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds tag/metadata-conditional routing: a routing model's targets can carry tags, and a request selects among them by the routing tags it carries. Closes the conditional/tag-routing gap vs LiteLLM / Portkey / Kong (routing gap list #873).

How

Request tags come from an out-of-band header, x-aisix-routing-tags (comma-separated), resolved once by the existing ClientContext extractor alongside client IP / User-Agent. They're read from the headers, never the body, so they never reach the upstream request payload.

Filtering happens in the shared resolve_attempt_models, as a pre-filter that narrows the target set before the configured strategy orders it — so tag routing composes with round-robin / weighted / failover / least-*, rather than being a new strategy. eligible_targets mirrors LiteLLM's tag_based_routing:

  • No target is tagged → tag routing isn't in use; every target is eligible.
  • Request carries tags → targets whose tags intersect it (match-any); if none match, fall back to "default"-tagged targets.
  • Request has no tags → "default"-tagged targets if any, else all.
  • Tags that match nothing with no default configured → a "no target matches tags" error.

RoutingTarget gains an optional tags: Vec<String> (additive, serde-default, so existing configs are unaffected). count_tokens's dispatch now takes the whole ClientContext (it previously took just source_ip), matching the other three routing-capable endpoints.

LiteLLM baseline

Semantics follow litellm/router_strategy/tag_based_routing.py (match-any intersect + default fallback). One deliberate divergence: LiteLLM sources tags from the request body (metadata.tags); we use a header. Our bridges serialize the whole request body (including passthrough extra) to the upstream, so a body metadata field would be forwarded and rejected by providers that constrain metadata (OpenAI requires string values). A header keeps the routing signal out of the upstream payload without a body-stripping layer. The matching/fallback behavior is unchanged from LiteLLM.

Tests

  • Core unit: RoutingTarget tag predicates + tags deserialization.
  • Proxy unit: eligible_targets across all branches (no-tags passthrough, match-any, tagged-no-match→default, untagged→default, untagged-no-default→all, tagged-no-match-no-default→empty), and header parsing.
  • DP E2E (tag-routing-e2e.test.ts): a routing model with eu / us / default targets — x-aisix-routing-tags selects the matching upstream, an untagged request and an unmatched tag both fall back to default, and the header is asserted absent from the upstream body.

Notes

Regenerated the committed resource schemas (routing/model). User-facing docs ship as one consolidated api7/docs routing-strategies page for the whole routing family.

Fixes api7/AISIX-Cloud#927

Summary by CodeRabbit

  • New Features

    • Added tag-based routing for requests, letting traffic be directed by request tags.
    • Support for a default routing target was added for untagged or unmatched requests.
    • Requests can now include routing tags via a new header, and matching behavior is covered end-to-end.
  • Bug Fixes

    • Routing now falls back more predictably when no tag match is found.
    • Untagged requests are handled consistently using default-target behavior when available.

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based routing to RoutingTarget, allowing targets to declare tags and a "default" fallback tag. Client requests can supply routing tags via a new header, which propagate through dispatch call sites into resolve_attempt_models, which now filters eligible targets by tag match-any/default-fallback logic before applying existing strategy ordering. Schemas and e2e tests are updated accordingly.

Changes

Tag-based routing

Layer / File(s)Summary
RoutingTarget tags contract
crates/aisix-core/src/models/routing.rs
Adds optional tags field, DEFAULT_ROUTING_TAG constant, with_tags, has_tags, is_default_target, matches_request_tags, and unit tests.
Client routing-tags extraction
crates/aisix-proxy/src/client_ip.rs
Adds ROUTING_TAGS_HEADER constant, routing_tags field on ClientContext, parse_routing_tags helper wired into the extractor, and unit tests.
Eligibility filtering in resolve_attempt_models
crates/aisix-proxy/src/routing.rs
Adds eligible_targets filter with match-any/default-fallback logic, extends resolve_attempt_models with request_tags, pre-filters targets, returns InvalidRequest on empty match, and adds unit tests.
Dispatch call site wiring
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/count_tokens.rs
Passes ClientContext/client.routing_tags into dispatch and resolve_attempt_models calls across all four endpoint handlers.
Schema and e2e validation
schemas/resources/model.schema.json, schemas/resources/routing.schema.json, tests/e2e/src/cases/tag-routing-e2e.test.ts
Documents tags field in both schemas and adds an e2e test validating matched/unmatched/untagged tag routing and metadata isolation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant ClientContext
participant Dispatch
participant resolve_attempt_models
participant RoutingRegistry
Client->>ClientContext: request with x-aisix-routing-tags header
ClientContext->>ClientContext: parse_routing_tags(header)
ClientContext-->>Dispatch: routing_tags: Vec<String>
Dispatch->>resolve_attempt_models: resolve_attempt_models(..., request_tags)
resolve_attempt_models->>resolve_attempt_models: eligible_targets(routing.targets, request_tags)
alt no eligible targets
resolve_attempt_models-->>Dispatch: ProxyError::InvalidRequest
else eligible targets found
resolve_attempt_models->>RoutingRegistry: pick_targets(eligible subset)
RoutingRegistry-->>resolve_attempt_models: selected targets
resolve_attempt_models-->>Dispatch: attempt models
end
Loading

Possibly related issues

  • api7/AISIX-Cloud#927: Implements the same tag-based routing feature end-to-end, adding RoutingTarget.tags, threading request tags into selection, and default-tag fallback logic.

Possibly related PRs

  • api7/aisix#472: Both modify resolve_attempt_models and its call sites in /v1/messages and chat dispatch flows.
  • api7/aisix#534: Both touch the routing/dispatch flow for /v1/chat/completions, /v1/messages, and /v1/responses, modifying how routing attempts are resolved.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningE2E hits the happy/default path, but it doesn't pin the new no-default/no-match 400 or a multi-tag match-any case, so the core routing behavior is still under-tested.Add one real request with x-aisix-routing-tags: eu,premium and one tagged model set without default that asserts the 400 error path; optional: cover another wired endpoint too.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding tag/metadata-conditional routing in routing logic.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security regressions found: the new routing tags only affect target selection, while auth checks and secret-bearing headers/logging remain unchanged in the touched code.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/routing-tag

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/src/cases/tag-routing-e2e.test.ts (1)

94-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing e2e coverage for the "no match, no default" error path.

The PR states that when tags match nothing and no default target exists, an error is returned, but this test only exercises configurations that always include a tag-default target. Consider adding a scenario with no default-tagged target and a request tag that matches nothing, asserting the request fails (e.g., a 400/invalid-request response).

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts` around lines 94 - 136, The e2e
test in tag-routing should also cover the failure path where no target matches
and no `default` target exists. Extend the existing `selects the tagged target,
defaulting when unmatched or untagged` scenario, or add a new case рядом with
`askWithTags`/`admin.createModel`, that creates a routing config without any
`default`-tagged target, sends a request with an unmatched tag like “apac”, and
asserts the request is rejected with an invalid-request/400-style error. Use the
existing `startOpenAiUpstream`, `createOpenAiModel`, and `waitConfigPropagation`
helpers so the new boundary case stays consistent with the current test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-proxy/src/client_ip.rs`:
- Around line 180-187: The routing-tag parsing in parse_routing_tags currently
preserves case, which can cause exact-match lookups in
RoutingTarget::matches_request_tags and is_default_target to miss values like EU
vs eu. Normalize each parsed tag at the header parse boundary by converting to a
consistent case before collecting, so downstream tag matching behaves
case-insensitively and does not incorrectly fall back to default.
---
Nitpick comments:
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts`:
- Around line 94-136: The e2e test in tag-routing should also cover the failure
path where no target matches and no `default` target exists. Extend the existing
`selects the tagged target, defaulting when unmatched or untagged` scenario, or
add a new case рядом with `askWithTags`/`admin.createModel`, that creates a
routing config without any `default`-tagged target, sends a request with an
unmatched tag like “apac”, and asserts the request is rejected with an
invalid-request/400-style error. Use the existing `startOpenAiUpstream`,
`createOpenAiModel`, and `waitConfigPropagation` helpers so the new boundary
case stays consistent with the current test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f2c66c3-d86a-4dc7-a6c3-d356a838b69f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e82aac and 2dcbf83.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/client_ip.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/tag-routing-e2e.test.ts

Comment threadcrates/aisix-proxy/src/client_ip.rs
@jarvis9443
jarvis9443 merged commit 6ddb3cf into mainJul 1, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/routing-tag branch July 1, 2026 15:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(routing): tag/metadata conditional routing - #686

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag
Jul 1, 2026
Merged

feat(routing): tag/metadata conditional routing#686
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds tag/metadata-conditional routing: a routing model's targets can carry tags, and a request selects among them by the routing tags it carries. Closes the conditional/tag-routing gap vs LiteLLM / Portkey / Kong (routing gap list #873).

How

Request tags come from an out-of-band header, x-aisix-routing-tags (comma-separated), resolved once by the existing ClientContext extractor alongside client IP / User-Agent. They're read from the headers, never the body, so they never reach the upstream request payload.

Filtering happens in the shared resolve_attempt_models, as a pre-filter that narrows the target set before the configured strategy orders it — so tag routing composes with round-robin / weighted / failover / least-*, rather than being a new strategy. eligible_targets mirrors LiteLLM's tag_based_routing:

  • No target is tagged → tag routing isn't in use; every target is eligible.
  • Request carries tags → targets whose tags intersect it (match-any); if none match, fall back to "default"-tagged targets.
  • Request has no tags → "default"-tagged targets if any, else all.
  • Tags that match nothing with no default configured → a "no target matches tags" error.

RoutingTarget gains an optional tags: Vec<String> (additive, serde-default, so existing configs are unaffected). count_tokens's dispatch now takes the whole ClientContext (it previously took just source_ip), matching the other three routing-capable endpoints.

LiteLLM baseline

Semantics follow litellm/router_strategy/tag_based_routing.py (match-any intersect + default fallback). One deliberate divergence: LiteLLM sources tags from the request body (metadata.tags); we use a header. Our bridges serialize the whole request body (including passthrough extra) to the upstream, so a body metadata field would be forwarded and rejected by providers that constrain metadata (OpenAI requires string values). A header keeps the routing signal out of the upstream payload without a body-stripping layer. The matching/fallback behavior is unchanged from LiteLLM.

Tests

  • Core unit: RoutingTarget tag predicates + tags deserialization.
  • Proxy unit: eligible_targets across all branches (no-tags passthrough, match-any, tagged-no-match→default, untagged→default, untagged-no-default→all, tagged-no-match-no-default→empty), and header parsing.
  • DP E2E (tag-routing-e2e.test.ts): a routing model with eu / us / default targets — x-aisix-routing-tags selects the matching upstream, an untagged request and an unmatched tag both fall back to default, and the header is asserted absent from the upstream body.

Notes

Regenerated the committed resource schemas (routing/model). User-facing docs ship as one consolidated api7/docs routing-strategies page for the whole routing family.

Fixes api7/AISIX-Cloud#927

Summary by CodeRabbit

  • New Features

    • Added tag-based routing for requests, letting traffic be directed by request tags.
    • Support for a default routing target was added for untagged or unmatched requests.
    • Requests can now include routing tags via a new header, and matching behavior is covered end-to-end.
  • Bug Fixes

    • Routing now falls back more predictably when no tag match is found.
    • Untagged requests are handled consistently using default-target behavior when available.

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based routing to RoutingTarget, allowing targets to declare tags and a "default" fallback tag. Client requests can supply routing tags via a new header, which propagate through dispatch call sites into resolve_attempt_models, which now filters eligible targets by tag match-any/default-fallback logic before applying existing strategy ordering. Schemas and e2e tests are updated accordingly.

Changes

Tag-based routing

Layer / File(s)Summary
RoutingTarget tags contract
crates/aisix-core/src/models/routing.rs
Adds optional tags field, DEFAULT_ROUTING_TAG constant, with_tags, has_tags, is_default_target, matches_request_tags, and unit tests.
Client routing-tags extraction
crates/aisix-proxy/src/client_ip.rs
Adds ROUTING_TAGS_HEADER constant, routing_tags field on ClientContext, parse_routing_tags helper wired into the extractor, and unit tests.
Eligibility filtering in resolve_attempt_models
crates/aisix-proxy/src/routing.rs
Adds eligible_targets filter with match-any/default-fallback logic, extends resolve_attempt_models with request_tags, pre-filters targets, returns InvalidRequest on empty match, and adds unit tests.
Dispatch call site wiring
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/count_tokens.rs
Passes ClientContext/client.routing_tags into dispatch and resolve_attempt_models calls across all four endpoint handlers.
Schema and e2e validation
schemas/resources/model.schema.json, schemas/resources/routing.schema.json, tests/e2e/src/cases/tag-routing-e2e.test.ts
Documents tags field in both schemas and adds an e2e test validating matched/unmatched/untagged tag routing and metadata isolation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant ClientContext
participant Dispatch
participant resolve_attempt_models
participant RoutingRegistry
Client->>ClientContext: request with x-aisix-routing-tags header
ClientContext->>ClientContext: parse_routing_tags(header)
ClientContext-->>Dispatch: routing_tags: Vec<String>
Dispatch->>resolve_attempt_models: resolve_attempt_models(..., request_tags)
resolve_attempt_models->>resolve_attempt_models: eligible_targets(routing.targets, request_tags)
alt no eligible targets
resolve_attempt_models-->>Dispatch: ProxyError::InvalidRequest
else eligible targets found
resolve_attempt_models->>RoutingRegistry: pick_targets(eligible subset)
RoutingRegistry-->>resolve_attempt_models: selected targets
resolve_attempt_models-->>Dispatch: attempt models
end
Loading

Possibly related issues

  • api7/AISIX-Cloud#927: Implements the same tag-based routing feature end-to-end, adding RoutingTarget.tags, threading request tags into selection, and default-tag fallback logic.

Possibly related PRs

  • api7/aisix#472: Both modify resolve_attempt_models and its call sites in /v1/messages and chat dispatch flows.
  • api7/aisix#534: Both touch the routing/dispatch flow for /v1/chat/completions, /v1/messages, and /v1/responses, modifying how routing attempts are resolved.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningE2E hits the happy/default path, but it doesn't pin the new no-default/no-match 400 or a multi-tag match-any case, so the core routing behavior is still under-tested.Add one real request with x-aisix-routing-tags: eu,premium and one tagged model set without default that asserts the 400 error path; optional: cover another wired endpoint too.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding tag/metadata-conditional routing in routing logic.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security regressions found: the new routing tags only affect target selection, while auth checks and secret-bearing headers/logging remain unchanged in the touched code.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/routing-tag

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/src/cases/tag-routing-e2e.test.ts (1)

94-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing e2e coverage for the "no match, no default" error path.

The PR states that when tags match nothing and no default target exists, an error is returned, but this test only exercises configurations that always include a tag-default target. Consider adding a scenario with no default-tagged target and a request tag that matches nothing, asserting the request fails (e.g., a 400/invalid-request response).

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts` around lines 94 - 136, The e2e
test in tag-routing should also cover the failure path where no target matches
and no `default` target exists. Extend the existing `selects the tagged target,
defaulting when unmatched or untagged` scenario, or add a new case рядом with
`askWithTags`/`admin.createModel`, that creates a routing config without any
`default`-tagged target, sends a request with an unmatched tag like “apac”, and
asserts the request is rejected with an invalid-request/400-style error. Use the
existing `startOpenAiUpstream`, `createOpenAiModel`, and `waitConfigPropagation`
helpers so the new boundary case stays consistent with the current test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-proxy/src/client_ip.rs`:
- Around line 180-187: The routing-tag parsing in parse_routing_tags currently
preserves case, which can cause exact-match lookups in
RoutingTarget::matches_request_tags and is_default_target to miss values like EU
vs eu. Normalize each parsed tag at the header parse boundary by converting to a
consistent case before collecting, so downstream tag matching behaves
case-insensitively and does not incorrectly fall back to default.
---
Nitpick comments:
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts`:
- Around line 94-136: The e2e test in tag-routing should also cover the failure
path where no target matches and no `default` target exists. Extend the existing
`selects the tagged target, defaulting when unmatched or untagged` scenario, or
add a new case рядом with `askWithTags`/`admin.createModel`, that creates a
routing config without any `default`-tagged target, sends a request with an
unmatched tag like “apac”, and asserts the request is rejected with an
invalid-request/400-style error. Use the existing `startOpenAiUpstream`,
`createOpenAiModel`, and `waitConfigPropagation` helpers so the new boundary
case stays consistent with the current test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f2c66c3-d86a-4dc7-a6c3-d356a838b69f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e82aac and 2dcbf83.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/client_ip.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/tag-routing-e2e.test.ts

Comment threadcrates/aisix-proxy/src/client_ip.rs
@jarvis9443
jarvis9443 merged commit 6ddb3cf into mainJul 1, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/routing-tag branch July 1, 2026 15:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(routing): tag/metadata conditional routing - #686

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag
Jul 1, 2026
Merged

feat(routing): tag/metadata conditional routing#686
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds tag/metadata-conditional routing: a routing model's targets can carry tags, and a request selects among them by the routing tags it carries. Closes the conditional/tag-routing gap vs LiteLLM / Portkey / Kong (routing gap list #873).

How

Request tags come from an out-of-band header, x-aisix-routing-tags (comma-separated), resolved once by the existing ClientContext extractor alongside client IP / User-Agent. They're read from the headers, never the body, so they never reach the upstream request payload.

Filtering happens in the shared resolve_attempt_models, as a pre-filter that narrows the target set before the configured strategy orders it — so tag routing composes with round-robin / weighted / failover / least-*, rather than being a new strategy. eligible_targets mirrors LiteLLM's tag_based_routing:

  • No target is tagged → tag routing isn't in use; every target is eligible.
  • Request carries tags → targets whose tags intersect it (match-any); if none match, fall back to "default"-tagged targets.
  • Request has no tags → "default"-tagged targets if any, else all.
  • Tags that match nothing with no default configured → a "no target matches tags" error.

RoutingTarget gains an optional tags: Vec<String> (additive, serde-default, so existing configs are unaffected). count_tokens's dispatch now takes the whole ClientContext (it previously took just source_ip), matching the other three routing-capable endpoints.

LiteLLM baseline

Semantics follow litellm/router_strategy/tag_based_routing.py (match-any intersect + default fallback). One deliberate divergence: LiteLLM sources tags from the request body (metadata.tags); we use a header. Our bridges serialize the whole request body (including passthrough extra) to the upstream, so a body metadata field would be forwarded and rejected by providers that constrain metadata (OpenAI requires string values). A header keeps the routing signal out of the upstream payload without a body-stripping layer. The matching/fallback behavior is unchanged from LiteLLM.

Tests

  • Core unit: RoutingTarget tag predicates + tags deserialization.
  • Proxy unit: eligible_targets across all branches (no-tags passthrough, match-any, tagged-no-match→default, untagged→default, untagged-no-default→all, tagged-no-match-no-default→empty), and header parsing.
  • DP E2E (tag-routing-e2e.test.ts): a routing model with eu / us / default targets — x-aisix-routing-tags selects the matching upstream, an untagged request and an unmatched tag both fall back to default, and the header is asserted absent from the upstream body.

Notes

Regenerated the committed resource schemas (routing/model). User-facing docs ship as one consolidated api7/docs routing-strategies page for the whole routing family.

Fixes api7/AISIX-Cloud#927

Summary by CodeRabbit

  • New Features

    • Added tag-based routing for requests, letting traffic be directed by request tags.
    • Support for a default routing target was added for untagged or unmatched requests.
    • Requests can now include routing tags via a new header, and matching behavior is covered end-to-end.
  • Bug Fixes

    • Routing now falls back more predictably when no tag match is found.
    • Untagged requests are handled consistently using default-target behavior when available.

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based routing to RoutingTarget, allowing targets to declare tags and a "default" fallback tag. Client requests can supply routing tags via a new header, which propagate through dispatch call sites into resolve_attempt_models, which now filters eligible targets by tag match-any/default-fallback logic before applying existing strategy ordering. Schemas and e2e tests are updated accordingly.

Changes

Tag-based routing

Layer / File(s)Summary
RoutingTarget tags contract
crates/aisix-core/src/models/routing.rs
Adds optional tags field, DEFAULT_ROUTING_TAG constant, with_tags, has_tags, is_default_target, matches_request_tags, and unit tests.
Client routing-tags extraction
crates/aisix-proxy/src/client_ip.rs
Adds ROUTING_TAGS_HEADER constant, routing_tags field on ClientContext, parse_routing_tags helper wired into the extractor, and unit tests.
Eligibility filtering in resolve_attempt_models
crates/aisix-proxy/src/routing.rs
Adds eligible_targets filter with match-any/default-fallback logic, extends resolve_attempt_models with request_tags, pre-filters targets, returns InvalidRequest on empty match, and adds unit tests.
Dispatch call site wiring
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/count_tokens.rs
Passes ClientContext/client.routing_tags into dispatch and resolve_attempt_models calls across all four endpoint handlers.
Schema and e2e validation
schemas/resources/model.schema.json, schemas/resources/routing.schema.json, tests/e2e/src/cases/tag-routing-e2e.test.ts
Documents tags field in both schemas and adds an e2e test validating matched/unmatched/untagged tag routing and metadata isolation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant ClientContext
participant Dispatch
participant resolve_attempt_models
participant RoutingRegistry
Client->>ClientContext: request with x-aisix-routing-tags header
ClientContext->>ClientContext: parse_routing_tags(header)
ClientContext-->>Dispatch: routing_tags: Vec<String>
Dispatch->>resolve_attempt_models: resolve_attempt_models(..., request_tags)
resolve_attempt_models->>resolve_attempt_models: eligible_targets(routing.targets, request_tags)
alt no eligible targets
resolve_attempt_models-->>Dispatch: ProxyError::InvalidRequest
else eligible targets found
resolve_attempt_models->>RoutingRegistry: pick_targets(eligible subset)
RoutingRegistry-->>resolve_attempt_models: selected targets
resolve_attempt_models-->>Dispatch: attempt models
end
Loading

Possibly related issues

  • api7/AISIX-Cloud#927: Implements the same tag-based routing feature end-to-end, adding RoutingTarget.tags, threading request tags into selection, and default-tag fallback logic.

Possibly related PRs

  • api7/aisix#472: Both modify resolve_attempt_models and its call sites in /v1/messages and chat dispatch flows.
  • api7/aisix#534: Both touch the routing/dispatch flow for /v1/chat/completions, /v1/messages, and /v1/responses, modifying how routing attempts are resolved.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningE2E hits the happy/default path, but it doesn't pin the new no-default/no-match 400 or a multi-tag match-any case, so the core routing behavior is still under-tested.Add one real request with x-aisix-routing-tags: eu,premium and one tagged model set without default that asserts the 400 error path; optional: cover another wired endpoint too.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding tag/metadata-conditional routing in routing logic.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security regressions found: the new routing tags only affect target selection, while auth checks and secret-bearing headers/logging remain unchanged in the touched code.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/routing-tag

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/src/cases/tag-routing-e2e.test.ts (1)

94-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing e2e coverage for the "no match, no default" error path.

The PR states that when tags match nothing and no default target exists, an error is returned, but this test only exercises configurations that always include a tag-default target. Consider adding a scenario with no default-tagged target and a request tag that matches nothing, asserting the request fails (e.g., a 400/invalid-request response).

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts` around lines 94 - 136, The e2e
test in tag-routing should also cover the failure path where no target matches
and no `default` target exists. Extend the existing `selects the tagged target,
defaulting when unmatched or untagged` scenario, or add a new case рядом with
`askWithTags`/`admin.createModel`, that creates a routing config without any
`default`-tagged target, sends a request with an unmatched tag like “apac”, and
asserts the request is rejected with an invalid-request/400-style error. Use the
existing `startOpenAiUpstream`, `createOpenAiModel`, and `waitConfigPropagation`
helpers so the new boundary case stays consistent with the current test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-proxy/src/client_ip.rs`:
- Around line 180-187: The routing-tag parsing in parse_routing_tags currently
preserves case, which can cause exact-match lookups in
RoutingTarget::matches_request_tags and is_default_target to miss values like EU
vs eu. Normalize each parsed tag at the header parse boundary by converting to a
consistent case before collecting, so downstream tag matching behaves
case-insensitively and does not incorrectly fall back to default.
---
Nitpick comments:
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts`:
- Around line 94-136: The e2e test in tag-routing should also cover the failure
path where no target matches and no `default` target exists. Extend the existing
`selects the tagged target, defaulting when unmatched or untagged` scenario, or
add a new case рядом with `askWithTags`/`admin.createModel`, that creates a
routing config without any `default`-tagged target, sends a request with an
unmatched tag like “apac”, and asserts the request is rejected with an
invalid-request/400-style error. Use the existing `startOpenAiUpstream`,
`createOpenAiModel`, and `waitConfigPropagation` helpers so the new boundary
case stays consistent with the current test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f2c66c3-d86a-4dc7-a6c3-d356a838b69f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e82aac and 2dcbf83.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/client_ip.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/tag-routing-e2e.test.ts

Comment threadcrates/aisix-proxy/src/client_ip.rs
@jarvis9443
jarvis9443 merged commit 6ddb3cf into mainJul 1, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/routing-tag branch July 1, 2026 15:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(routing): tag/metadata conditional routing - #686

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag
Jul 1, 2026
Merged

feat(routing): tag/metadata conditional routing#686
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds tag/metadata-conditional routing: a routing model's targets can carry tags, and a request selects among them by the routing tags it carries. Closes the conditional/tag-routing gap vs LiteLLM / Portkey / Kong (routing gap list #873).

How

Request tags come from an out-of-band header, x-aisix-routing-tags (comma-separated), resolved once by the existing ClientContext extractor alongside client IP / User-Agent. They're read from the headers, never the body, so they never reach the upstream request payload.

Filtering happens in the shared resolve_attempt_models, as a pre-filter that narrows the target set before the configured strategy orders it — so tag routing composes with round-robin / weighted / failover / least-*, rather than being a new strategy. eligible_targets mirrors LiteLLM's tag_based_routing:

  • No target is tagged → tag routing isn't in use; every target is eligible.
  • Request carries tags → targets whose tags intersect it (match-any); if none match, fall back to "default"-tagged targets.
  • Request has no tags → "default"-tagged targets if any, else all.
  • Tags that match nothing with no default configured → a "no target matches tags" error.

RoutingTarget gains an optional tags: Vec<String> (additive, serde-default, so existing configs are unaffected). count_tokens's dispatch now takes the whole ClientContext (it previously took just source_ip), matching the other three routing-capable endpoints.

LiteLLM baseline

Semantics follow litellm/router_strategy/tag_based_routing.py (match-any intersect + default fallback). One deliberate divergence: LiteLLM sources tags from the request body (metadata.tags); we use a header. Our bridges serialize the whole request body (including passthrough extra) to the upstream, so a body metadata field would be forwarded and rejected by providers that constrain metadata (OpenAI requires string values). A header keeps the routing signal out of the upstream payload without a body-stripping layer. The matching/fallback behavior is unchanged from LiteLLM.

Tests

  • Core unit: RoutingTarget tag predicates + tags deserialization.
  • Proxy unit: eligible_targets across all branches (no-tags passthrough, match-any, tagged-no-match→default, untagged→default, untagged-no-default→all, tagged-no-match-no-default→empty), and header parsing.
  • DP E2E (tag-routing-e2e.test.ts): a routing model with eu / us / default targets — x-aisix-routing-tags selects the matching upstream, an untagged request and an unmatched tag both fall back to default, and the header is asserted absent from the upstream body.

Notes

Regenerated the committed resource schemas (routing/model). User-facing docs ship as one consolidated api7/docs routing-strategies page for the whole routing family.

Fixes api7/AISIX-Cloud#927

Summary by CodeRabbit

  • New Features

    • Added tag-based routing for requests, letting traffic be directed by request tags.
    • Support for a default routing target was added for untagged or unmatched requests.
    • Requests can now include routing tags via a new header, and matching behavior is covered end-to-end.
  • Bug Fixes

    • Routing now falls back more predictably when no tag match is found.
    • Untagged requests are handled consistently using default-target behavior when available.

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based routing to RoutingTarget, allowing targets to declare tags and a "default" fallback tag. Client requests can supply routing tags via a new header, which propagate through dispatch call sites into resolve_attempt_models, which now filters eligible targets by tag match-any/default-fallback logic before applying existing strategy ordering. Schemas and e2e tests are updated accordingly.

Changes

Tag-based routing

Layer / File(s)Summary
RoutingTarget tags contract
crates/aisix-core/src/models/routing.rs
Adds optional tags field, DEFAULT_ROUTING_TAG constant, with_tags, has_tags, is_default_target, matches_request_tags, and unit tests.
Client routing-tags extraction
crates/aisix-proxy/src/client_ip.rs
Adds ROUTING_TAGS_HEADER constant, routing_tags field on ClientContext, parse_routing_tags helper wired into the extractor, and unit tests.
Eligibility filtering in resolve_attempt_models
crates/aisix-proxy/src/routing.rs
Adds eligible_targets filter with match-any/default-fallback logic, extends resolve_attempt_models with request_tags, pre-filters targets, returns InvalidRequest on empty match, and adds unit tests.
Dispatch call site wiring
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/count_tokens.rs
Passes ClientContext/client.routing_tags into dispatch and resolve_attempt_models calls across all four endpoint handlers.
Schema and e2e validation
schemas/resources/model.schema.json, schemas/resources/routing.schema.json, tests/e2e/src/cases/tag-routing-e2e.test.ts
Documents tags field in both schemas and adds an e2e test validating matched/unmatched/untagged tag routing and metadata isolation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant ClientContext
participant Dispatch
participant resolve_attempt_models
participant RoutingRegistry
Client->>ClientContext: request with x-aisix-routing-tags header
ClientContext->>ClientContext: parse_routing_tags(header)
ClientContext-->>Dispatch: routing_tags: Vec<String>
Dispatch->>resolve_attempt_models: resolve_attempt_models(..., request_tags)
resolve_attempt_models->>resolve_attempt_models: eligible_targets(routing.targets, request_tags)
alt no eligible targets
resolve_attempt_models-->>Dispatch: ProxyError::InvalidRequest
else eligible targets found
resolve_attempt_models->>RoutingRegistry: pick_targets(eligible subset)
RoutingRegistry-->>resolve_attempt_models: selected targets
resolve_attempt_models-->>Dispatch: attempt models
end
Loading

Possibly related issues

  • api7/AISIX-Cloud#927: Implements the same tag-based routing feature end-to-end, adding RoutingTarget.tags, threading request tags into selection, and default-tag fallback logic.

Possibly related PRs

  • api7/aisix#472: Both modify resolve_attempt_models and its call sites in /v1/messages and chat dispatch flows.
  • api7/aisix#534: Both touch the routing/dispatch flow for /v1/chat/completions, /v1/messages, and /v1/responses, modifying how routing attempts are resolved.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningE2E hits the happy/default path, but it doesn't pin the new no-default/no-match 400 or a multi-tag match-any case, so the core routing behavior is still under-tested.Add one real request with x-aisix-routing-tags: eu,premium and one tagged model set without default that asserts the 400 error path; optional: cover another wired endpoint too.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding tag/metadata-conditional routing in routing logic.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security regressions found: the new routing tags only affect target selection, while auth checks and secret-bearing headers/logging remain unchanged in the touched code.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/routing-tag

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/src/cases/tag-routing-e2e.test.ts (1)

94-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing e2e coverage for the "no match, no default" error path.

The PR states that when tags match nothing and no default target exists, an error is returned, but this test only exercises configurations that always include a tag-default target. Consider adding a scenario with no default-tagged target and a request tag that matches nothing, asserting the request fails (e.g., a 400/invalid-request response).

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts` around lines 94 - 136, The e2e
test in tag-routing should also cover the failure path where no target matches
and no `default` target exists. Extend the existing `selects the tagged target,
defaulting when unmatched or untagged` scenario, or add a new case рядом with
`askWithTags`/`admin.createModel`, that creates a routing config without any
`default`-tagged target, sends a request with an unmatched tag like “apac”, and
asserts the request is rejected with an invalid-request/400-style error. Use the
existing `startOpenAiUpstream`, `createOpenAiModel`, and `waitConfigPropagation`
helpers so the new boundary case stays consistent with the current test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-proxy/src/client_ip.rs`:
- Around line 180-187: The routing-tag parsing in parse_routing_tags currently
preserves case, which can cause exact-match lookups in
RoutingTarget::matches_request_tags and is_default_target to miss values like EU
vs eu. Normalize each parsed tag at the header parse boundary by converting to a
consistent case before collecting, so downstream tag matching behaves
case-insensitively and does not incorrectly fall back to default.
---
Nitpick comments:
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts`:
- Around line 94-136: The e2e test in tag-routing should also cover the failure
path where no target matches and no `default` target exists. Extend the existing
`selects the tagged target, defaulting when unmatched or untagged` scenario, or
add a new case рядом with `askWithTags`/`admin.createModel`, that creates a
routing config without any `default`-tagged target, sends a request with an
unmatched tag like “apac”, and asserts the request is rejected with an
invalid-request/400-style error. Use the existing `startOpenAiUpstream`,
`createOpenAiModel`, and `waitConfigPropagation` helpers so the new boundary
case stays consistent with the current test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f2c66c3-d86a-4dc7-a6c3-d356a838b69f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e82aac and 2dcbf83.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/client_ip.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/tag-routing-e2e.test.ts

Comment threadcrates/aisix-proxy/src/client_ip.rs
@jarvis9443
jarvis9443 merged commit 6ddb3cf into mainJul 1, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/routing-tag branch July 1, 2026 15:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(routing): tag/metadata conditional routing - #686

Merged
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag
Jul 1, 2026
Merged

feat(routing): tag/metadata conditional routing#686
jarvis9443 merged 1 commit into
mainfrom
feat/routing-tag

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds tag/metadata-conditional routing: a routing model's targets can carry tags, and a request selects among them by the routing tags it carries. Closes the conditional/tag-routing gap vs LiteLLM / Portkey / Kong (routing gap list #873).

How

Request tags come from an out-of-band header, x-aisix-routing-tags (comma-separated), resolved once by the existing ClientContext extractor alongside client IP / User-Agent. They're read from the headers, never the body, so they never reach the upstream request payload.

Filtering happens in the shared resolve_attempt_models, as a pre-filter that narrows the target set before the configured strategy orders it — so tag routing composes with round-robin / weighted / failover / least-*, rather than being a new strategy. eligible_targets mirrors LiteLLM's tag_based_routing:

  • No target is tagged → tag routing isn't in use; every target is eligible.
  • Request carries tags → targets whose tags intersect it (match-any); if none match, fall back to "default"-tagged targets.
  • Request has no tags → "default"-tagged targets if any, else all.
  • Tags that match nothing with no default configured → a "no target matches tags" error.

RoutingTarget gains an optional tags: Vec<String> (additive, serde-default, so existing configs are unaffected). count_tokens's dispatch now takes the whole ClientContext (it previously took just source_ip), matching the other three routing-capable endpoints.

LiteLLM baseline

Semantics follow litellm/router_strategy/tag_based_routing.py (match-any intersect + default fallback). One deliberate divergence: LiteLLM sources tags from the request body (metadata.tags); we use a header. Our bridges serialize the whole request body (including passthrough extra) to the upstream, so a body metadata field would be forwarded and rejected by providers that constrain metadata (OpenAI requires string values). A header keeps the routing signal out of the upstream payload without a body-stripping layer. The matching/fallback behavior is unchanged from LiteLLM.

Tests

  • Core unit: RoutingTarget tag predicates + tags deserialization.
  • Proxy unit: eligible_targets across all branches (no-tags passthrough, match-any, tagged-no-match→default, untagged→default, untagged-no-default→all, tagged-no-match-no-default→empty), and header parsing.
  • DP E2E (tag-routing-e2e.test.ts): a routing model with eu / us / default targets — x-aisix-routing-tags selects the matching upstream, an untagged request and an unmatched tag both fall back to default, and the header is asserted absent from the upstream body.

Notes

Regenerated the committed resource schemas (routing/model). User-facing docs ship as one consolidated api7/docs routing-strategies page for the whole routing family.

Fixes api7/AISIX-Cloud#927

Summary by CodeRabbit

  • New Features

    • Added tag-based routing for requests, letting traffic be directed by request tags.
    • Support for a default routing target was added for untagged or unmatched requests.
    • Requests can now include routing tags via a new header, and matching behavior is covered end-to-end.
  • Bug Fixes

    • Routing now falls back more predictably when no tag match is found.
    • Untagged requests are handled consistently using default-target behavior when available.

@coderabbitai

coderabbitaiBot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based routing to RoutingTarget, allowing targets to declare tags and a "default" fallback tag. Client requests can supply routing tags via a new header, which propagate through dispatch call sites into resolve_attempt_models, which now filters eligible targets by tag match-any/default-fallback logic before applying existing strategy ordering. Schemas and e2e tests are updated accordingly.

Changes

Tag-based routing

Layer / File(s)Summary
RoutingTarget tags contract
crates/aisix-core/src/models/routing.rs
Adds optional tags field, DEFAULT_ROUTING_TAG constant, with_tags, has_tags, is_default_target, matches_request_tags, and unit tests.
Client routing-tags extraction
crates/aisix-proxy/src/client_ip.rs
Adds ROUTING_TAGS_HEADER constant, routing_tags field on ClientContext, parse_routing_tags helper wired into the extractor, and unit tests.
Eligibility filtering in resolve_attempt_models
crates/aisix-proxy/src/routing.rs
Adds eligible_targets filter with match-any/default-fallback logic, extends resolve_attempt_models with request_tags, pre-filters targets, returns InvalidRequest on empty match, and adds unit tests.
Dispatch call site wiring
crates/aisix-proxy/src/chat.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs, crates/aisix-proxy/src/count_tokens.rs
Passes ClientContext/client.routing_tags into dispatch and resolve_attempt_models calls across all four endpoint handlers.
Schema and e2e validation
schemas/resources/model.schema.json, schemas/resources/routing.schema.json, tests/e2e/src/cases/tag-routing-e2e.test.ts
Documents tags field in both schemas and adds an e2e test validating matched/unmatched/untagged tag routing and metadata isolation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant ClientContext
participant Dispatch
participant resolve_attempt_models
participant RoutingRegistry
Client->>ClientContext: request with x-aisix-routing-tags header
ClientContext->>ClientContext: parse_routing_tags(header)
ClientContext-->>Dispatch: routing_tags: Vec<String>
Dispatch->>resolve_attempt_models: resolve_attempt_models(..., request_tags)
resolve_attempt_models->>resolve_attempt_models: eligible_targets(routing.targets, request_tags)
alt no eligible targets
resolve_attempt_models-->>Dispatch: ProxyError::InvalidRequest
else eligible targets found
resolve_attempt_models->>RoutingRegistry: pick_targets(eligible subset)
RoutingRegistry-->>resolve_attempt_models: selected targets
resolve_attempt_models-->>Dispatch: attempt models
end
Loading

Possibly related issues

  • api7/AISIX-Cloud#927: Implements the same tag-based routing feature end-to-end, adding RoutingTarget.tags, threading request tags into selection, and default-tag fallback logic.

Possibly related PRs

  • api7/aisix#472: Both modify resolve_attempt_models and its call sites in /v1/messages and chat dispatch flows.
  • api7/aisix#534: Both touch the routing/dispatch flow for /v1/chat/completions, /v1/messages, and /v1/responses, modifying how routing attempts are resolved.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningE2E hits the happy/default path, but it doesn't pin the new no-default/no-match 400 or a multi-tag match-any case, so the core routing behavior is still under-tested.Add one real request with x-aisix-routing-tags: eu,premium and one tagged model set without default that asserts the 400 error path; optional: cover another wired endpoint too.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: adding tag/metadata-conditional routing in routing logic.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security regressions found: the new routing tags only affect target selection, while auth checks and secret-bearing headers/logging remain unchanged in the touched code.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/routing-tag

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/src/cases/tag-routing-e2e.test.ts (1)

94-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing e2e coverage for the "no match, no default" error path.

The PR states that when tags match nothing and no default target exists, an error is returned, but this test only exercises configurations that always include a tag-default target. Consider adding a scenario with no default-tagged target and a request tag that matches nothing, asserting the request fails (e.g., a 400/invalid-request response).

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx}: "Tests must cover boundary cases (empty values, min/max), invalid inputs, combination scenarios, and extreme cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts` around lines 94 - 136, The e2e
test in tag-routing should also cover the failure path where no target matches
and no `default` target exists. Extend the existing `selects the tagged target,
defaulting when unmatched or untagged` scenario, or add a new case рядом with
`askWithTags`/`admin.createModel`, that creates a routing config without any
`default`-tagged target, sends a request with an unmatched tag like “apac”, and
asserts the request is rejected with an invalid-request/400-style error. Use the
existing `startOpenAiUpstream`, `createOpenAiModel`, and `waitConfigPropagation`
helpers so the new boundary case stays consistent with the current test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-proxy/src/client_ip.rs`:
- Around line 180-187: The routing-tag parsing in parse_routing_tags currently
preserves case, which can cause exact-match lookups in
RoutingTarget::matches_request_tags and is_default_target to miss values like EU
vs eu. Normalize each parsed tag at the header parse boundary by converting to a
consistent case before collecting, so downstream tag matching behaves
case-insensitively and does not incorrectly fall back to default.
---
Nitpick comments:
In `@tests/e2e/src/cases/tag-routing-e2e.test.ts`:
- Around line 94-136: The e2e test in tag-routing should also cover the failure
path where no target matches and no `default` target exists. Extend the existing
`selects the tagged target, defaulting when unmatched or untagged` scenario, or
add a new case рядом with `askWithTags`/`admin.createModel`, that creates a
routing config without any `default`-tagged target, sends a request with an
unmatched tag like “apac”, and asserts the request is rejected with an
invalid-request/400-style error. Use the existing `startOpenAiUpstream`,
`createOpenAiModel`, and `waitConfigPropagation` helpers so the new boundary
case stays consistent with the current test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3f2c66c3-d86a-4dc7-a6c3-d356a838b69f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e82aac and 2dcbf83.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/routing.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/client_ip.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/routing.rs
  • schemas/resources/model.schema.json
  • schemas/resources/routing.schema.json
  • tests/e2e/src/cases/tag-routing-e2e.test.ts

Comment threadcrates/aisix-proxy/src/client_ip.rs
@jarvis9443
jarvis9443 merged commit 6ddb3cf into mainJul 1, 2026
10 checks passed
@jarvis9443
jarvis9443 deleted the feat/routing-tag branch July 1, 2026 15:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@jarvis9443